Files
endorphin-tessera-gui/src/render/software_renderer.rs

447 lines
20 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 把 DisplayList 写入 CPU 像素缓冲。裁剪栈限定矩形、文字和 3D 表面的实际写入区域。
//! RenderScope 约束递归深度;累计像素预算限制嵌套 3D 和世界 UI 的中间表面分配。错误向上传递,不把半幅结果当成成功帧呈现。
//!
use crate::{
display_list::{Color, DisplayList, DrawCommand},
geometry::{Point, Rect},
text::{self, TextStyle},
};
/// 显示列表的 CPU 执行入口,错误在原生 present 前向上传递。
pub struct SoftwareRenderer;
// 预算按渲染线程维护,递归 3D/面板渲染共用同一组计数;不同线程之间不共享额度。
thread_local! {
static RENDER_DEPTH:std::cell::Cell<usize>=const { std::cell::Cell::new(0) };
static PIXEL_UNITS:std::cell::Cell<usize>=const { std::cell::Cell::new(0) };
}
/// 递归渲染的 RAII 深度保护,最外层还重置累计像素工作预算。
pub(crate) struct RenderScope;
impl RenderScope {
/// 进入一次嵌套渲染并检查深度上限;最外层调用重新开始累计像素预算。
pub fn enter() -> Result<Self, &'static str> {
RENDER_DEPTH.with(|depth| {
let current = depth.get();
// 拒绝第 19 层进入,不先增加深度;成功返回的 RenderScope 析构时才对应减一。
if current >= 18 {
return Err("render nesting budget exceeded");
}
// 只有最外层渲染开始才清累计像素数;递归进入不能借此重置预算。
if current == 0 {
PIXEL_UNITS.with(|pixels| pixels.set(0));
}
depth.set(current + 1);
Ok(Self)
})
}
}
impl Drop for RenderScope {
/// 退出一次像素渲染作用域时归还嵌套深度;像素工作量仍按本帧累计,不因退出内层而返还。
fn drop(&mut self) {
RENDER_DEPTH.with(|depth| depth.set(depth.get() - 1));
}
}
/// 累加本帧中间表面工作量,超过预算立即返回错误。
pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> {
PIXEL_UNITS.with(|pixels| {
// 累计申请工作量而非当前存活像素;释放一个中间表面不会返还额度,饱和加法避免溢出绕过限制。
let next = pixels.get().saturating_add(units);
if next > 16_777_216 {
return Err("nested render pixel budget exceeded");
}
pixels.set(next);
Ok(())
})
}
impl SoftwareRenderer {
/// 顺序执行显示列表并维护裁剪栈;3D 和面板可递归进入同一渲染预算域。
pub fn render(
list: &DisplayList,
target: &mut [u32],
width: u32,
height: u32,
) -> Result<(), &'static str> {
// RAII 保护让成功、提前返回和错误传播都恢复深度;target 长度须由调用方保证至少 width×height。
let _scope = RenderScope::enter()?;
let mut canvas = Canvas {
pixels: target,
width,
height,
clip: Rect::new(0.0, 0.0, width as f32, height as f32),
};
let mut clips = Vec::new();
for command in list.commands() {
match command {
DrawCommand::Scene3D { rect, view } => {
// 先将 3D 矩形与当前裁剪和目标尺寸相交;完全不可见时连中间表面都不生成。
let left = rect.x.max(canvas.clip.x).max(0.).floor() as u32;
let top = rect.y.max(canvas.clip.y).max(0.).floor() as u32;
let right = (rect.x + rect.width)
.min(canvas.clip.x + canvas.clip.width)
.min(width as f32)
.ceil()
.max(0.) as u32;
let bottom = (rect.y + rect.height)
.min(canvas.clip.y + canvas.clip.height)
.min(height as f32)
.ceil()
.max(0.) as u32;
if left >= right || top >= bottom || rect.width <= 0. || rect.height <= 0. {
continue;
}
// 3D 中间表面的最长边限制到约 2048 像素,再采样到请求尺寸;相机宽高比仍用原 rect,避免改变透视比例。
let lod = (2048. / rect.width.max(rect.height)).min(1.);
let w = (rect.width * lod).ceil().max(1.) as u32;
let h = (rect.height * lod).ceil().max(1.) as u32;
let surface = crate::scene3d::surface(view, w, h, rect.width / rect.height)?;
for y in top..bottom {
for x in left..right {
// 按目标像素中心反算纹理坐标并钳到有效表面边界;只复制可见区域,不循环整个放大矩形。
let u = (((x as f32 + 0.5 - rect.x) / rect.width) * w as f32)
.clamp(0., (w - 1) as f32)
as u32;
let v = (((y as f32 + 0.5 - rect.y) / rect.height) * h as f32)
.clamp(0., (h - 1) as f32)
as u32;
// 3D 表面是最终 RGB 像素,直接覆盖目标;不走二维 source alpha 混合。
canvas.pixels[y as usize * width as usize + x as usize] =
surface[v as usize * w as usize + u as usize];
}
}
}
DrawCommand::PushClip(rect) => {
// 先保存旧裁剪,再求交;嵌套 PushClip 只能收窄,矩形分离时宽高归零。
clips.push(canvas.clip);
let x = canvas.clip.x.max(rect.x);
let y = canvas.clip.y.max(rect.y);
canvas.clip = Rect::new(
x,
y,
(canvas.clip.x + canvas.clip.width)
.min(rect.x + rect.width)
.max(x)
- x,
(canvas.clip.y + canvas.clip.height)
.min(rect.y + rect.height)
.max(y)
- y,
);
}
DrawCommand::PopClip => {
// 多余 PopClip 在当前实现中被忽略;合法列表仍应由生成层保证配对。
if let Some(clip) = clips.pop() {
canvas.clip = clip;
}
}
DrawCommand::Clear(color) => canvas.clear(*color),
DrawCommand::FillRoundedRect {
rect,
radius,
color,
} => canvas.fill_rounded_rect(*rect, *radius, *color),
DrawCommand::StrokeRoundedRect {
rect,
radius,
width,
color,
} => canvas.stroke_rounded_rect(*rect, *radius, *width, *color),
DrawCommand::WrappedText {
rect,
value,
style,
color,
} => canvas.draw_wrapped_text(*rect, value, *style, *color),
DrawCommand::Text {
origin,
value,
style,
color,
} => canvas.draw_text(*origin, value, *style, *color),
}
}
Ok(())
}
}
/// 借用目标像素切片并维护当前裁剪;图形/文字受裁剪,Clear 明确填满整个目标。
struct Canvas<'a> {
pixels: &'a mut [u32],
width: u32,
height: u32,
clip: Rect,
}
impl Canvas<'_> {
/// 用不透明 RGB 填满目标切片;此命令忽略当前裁剪和输入颜色 alpha。
fn clear(&mut self, color: Color) {
// Clear 填整个提供的目标切片,不受当前 clip 约束;pack 忽略 alpha,清屏色作为不透明 RGB。
self.pixels.fill(pack(color));
}
/// 只遍历裁剪后的矩形像素,按圆角覆盖决定填充区域。
fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
let bounds = self.clamped_bounds(rect);
// 半径钳到非负且不超过半宽/半高;后续用像素中心判定圆角内外,不做边缘面积覆盖积分。
let radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
for y in bounds.1..bounds.3 {
for x in bounds.0..bounds.2 {
if rounded_rect_contains(rect, radius, x as f32 + 0.5, y as f32 + 0.5) {
self.blend_pixel(x, y, color);
}
}
}
}
/// 用外轮廓与内轮廓的差绘制边框,避免把边框当成整个矩形填充。
fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, width: f32, color: Color) {
let bounds = self.clamped_bounds(rect);
// 描边请求先至少一单位再限制为半宽/半高;inner 从四边缩入,最终绘制外轮廓减内轮廓。
let width = width.max(1.0).min(rect.width / 2.0).min(rect.height / 2.0);
let outer_radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
let inner = Rect::new(
rect.x + width,
rect.y + width,
(rect.width - width * 2.0).max(0.0),
(rect.height - width * 2.0).max(0.0),
);
let inner_radius = (outer_radius - width).max(0.0);
for y in bounds.1..bounds.3 {
for x in bounds.0..bounds.2 {
let px = x as f32 + 0.5;
let py = y as f32 + 0.5;
if rounded_rect_contains(rect, outer_radius, px, py)
&& !rounded_rect_contains(inner, inner_radius, px, py)
{
self.blend_pixel(x, y, color);
}
}
}
}
/// 把文字原点转换为局部裁剪区域,交给有界字形绘制,再混合到目标缓冲。
fn draw_text(&mut self, origin: Point, value: &str, style: TextStyle, color: Color) {
// 将文字原点对齐整数像素;转换 clip 到字形局部空间,回调再加原点写回目标。
let x = origin.x.round() as i32;
let y = origin.y.round() as i32;
let clip = Rect::new(
self.clip.x - x as f32,
self.clip.y - y as f32,
self.clip.width,
self.clip.height,
);
text::draw_clipped(value, style, color, None, clip, |dx, dy, pixel| {
self.blend_pixel(x + dx, y + dy, pixel)
});
}
/// 把段落矩形与当前裁剪相交,按受限宽度绘制,再恢复外层裁剪。
fn draw_wrapped_text(&mut self, rect: Rect, value: &str, style: TextStyle, color: Color) {
if rect.width <= 0.0 || rect.height <= 0.0 {
return;
}
// 段落文本临时收窄 clip 到文本框;下方调用完成后恢复 old,后续命令不能继承段落私有裁剪。
let old = self.clip;
let x = old.x.max(rect.x);
let y = old.y.max(rect.y);
self.clip = Rect::new(
x,
y,
((old.x + old.width).min(rect.x + rect.width) - x).max(0.0),
((old.y + old.height).min(rect.y + rect.height) - y).max(0.0),
);
let left = rect.x.round() as i32;
let top = rect.y.round() as i32;
let clip = Rect::new(
self.clip.x - left as f32,
self.clip.y - top as f32,
self.clip.width,
self.clip.height,
);
text::draw_clipped(
value,
style,
color,
// 换行按完整段落宽度排版,而不是按被祖先裁剪后剩余的可见宽度重新排版。
Some(rect.width),
clip,
|dx, dy, pixel| self.blend_pixel(left + dx, top + dy, pixel),
);
self.clip = old;
}
/// 把连续矩形转换成目标缓冲内的整数像素范围,避免在不可见区域进行光栅循环。
fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) {
// 左/上 floor,右/下 ceil 形成保守循环范围;blend_pixel 还会按当前 clip 检查实际整数像素。
(
rect.x.max(self.clip.x).max(0.0).floor() as i32,
rect.y.max(self.clip.y).max(0.0).floor() as i32,
(rect.x + rect.width)
.min(self.clip.x + self.clip.width)
.min(self.width as f32)
.ceil() as i32,
(rect.y + rect.height)
.min(self.clip.y + self.clip.height)
.min(self.height as f32)
.ceil() as i32,
)
}
/// 在画布与当前裁剪范围内进行透明度混合,防止文字/图形写出目标缓冲。
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
// 拒绝画布外、裁剪外与全透明像素;只有通过后才计算线性下标,避免负坐标转 usize。
if x < 0
|| y < 0
|| x >= self.width as i32
|| y >= self.height as i32
|| source.alpha == 0
|| (x as f32) < self.clip.x
|| (y as f32) < self.clip.y
|| (x as f32) >= self.clip.x + self.clip.width
|| (y as f32) >= self.clip.y + self.clip.height
{
return;
}
let index = y as usize * self.width as usize + x as usize;
// 全不透明像素可直接打包覆盖,省去目的通道解码与乘除法。
if source.alpha == 255 {
self.pixels[index] = pack(source);
return;
}
let destination = self.pixels[index];
let dst_red = (destination >> 16) & 0xff;
let dst_green = (destination >> 8) & 0xff;
let dst_blue = destination & 0xff;
// 非预乘源颜色按 source*alpha + destination*(1-alpha) 合成,目标不存 alpha,逐通道整除 255。
let alpha = source.alpha as u32;
let inverse = 255 - alpha;
let red = (source.red as u32 * alpha + dst_red * inverse) / 255;
let green = (source.green as u32 * alpha + dst_green * inverse) / 255;
let blue = (source.blue as u32 * alpha + dst_blue * inverse) / 255;
self.pixels[index] = (red << 16) | (green << 8) | blue;
}
}
/// 测试一个像素采样点是否位于圆角矩形内,角区域使用圆弧距离。
fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
if rect.width <= 0.0 || rect.height <= 0.0 {
return false;
}
if radius <= 0.0 {
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}
// Clamp in local coordinates: adding a large origin to a shrinking tab's
// half-width can otherwise round the lower bound above the upper bound.
let radius = radius.min(rect.width / 2.0).min(rect.height / 2.0);
// 在局部坐标钳到圆角中心可达矩形,距离向量对应最近角圆心;中心带距离为零。
// 先减大原点可降低浮点舍入导致 clamp 上下界反转的风险。
let local_x = x - rect.x;
let local_y = y - rect.y;
let dx = local_x - local_x.clamp(radius, rect.width - radius);
let dy = local_y - local_y.clamp(radius, rect.height - radius);
dx * dx + dy * dy <= radius * radius
}
/// 将颜色通道打包成目标缓冲的整数像素表示。
fn pack(color: Color) -> u32 {
// 像素格式为 0x00RRGGBB,高字节不存透明度;透明度已在混合阶段折算进 RGB。
(color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// 画长文字进入嵌套裁剪,逐像素检查多行与裁剪交集;PopClip后的蓝矩形验证后续命令不沿用旧clip。
fn wrapped_text_uses_multiple_lines_respects_nested_clips_and_restores_the_clip() {
let mut list = DisplayList::default();
list.push(DrawCommand::Clear(Color::rgb(255, 255, 255)));
list.push(DrawCommand::PushClip(Rect::new(12.0, 5.0, 20.0, 40.0)));
list.push(DrawCommand::WrappedText {
rect: Rect::new(10.0, 5.0, 32.0, 40.0),
value: "MMMMMMMMMMMMMMMM".into(),
style: TextStyle::ui(1),
color: Color::rgb(0, 0, 0),
});
list.push(DrawCommand::PopClip);
list.push(DrawCommand::WrappedText {
rect: Rect::new(60.0, 0.0, 0.0, 40.0),
value: "invisible".into(),
style: TextStyle::ui(1),
color: Color::rgb(0, 0, 0),
});
list.push(DrawCommand::FillRoundedRect {
rect: Rect::new(70.0, 50.0, 2.0, 2.0),
radius: 0.0,
color: Color::rgb(0, 0, 255),
});
let mut pixels = vec![0; 80 * 60];
SoftwareRenderer::render(&list, &mut pixels, 80, 60).unwrap();
// 确认文字确实换成多行且每个非背景文字像素都落在嵌套裁剪交集内,蓝色矩形验证裁剪已恢复。
let mut second_line = false;
for y in 0..60 {
for x in 0..80 {
let color = pixels[y * 80 + x];
if color != 0xffffff && color != 0x0000ff {
assert!((12..32).contains(&x) && (5..45).contains(&y));
second_line |= y >= 25;
}
}
}
assert!(second_line);
assert_eq!(pixels[50 * 80 + 70], 0x0000ff);
}
#[test]
/// 在黑底4x4缓冲画2x2红矩形,核对内部像素变红、外部仍黑,覆盖最小坐标/打包路径。
fn renderer_changes_pixels_inside_a_rectangle() {
let mut list = DisplayList::default();
list.push(DrawCommand::Clear(Color::rgb(0, 0, 0)));
list.push(DrawCommand::FillRoundedRect {
rect: Rect::new(1.0, 1.0, 2.0, 2.0),
radius: 0.0,
color: Color::rgb(255, 0, 0),
});
let mut pixels = vec![0; 16];
SoftwareRenderer::render(&list, &mut pixels, 4, 4).unwrap();
assert_eq!(pixels[5], 0x00ff0000);
assert_eq!(pixels[0], 0);
}
#[test]
/// 半透明白字叠黑底必须含中间抗锯齿值且通道不超128;alpha=0的文字完全不改像素。
fn text_edges_are_antialiased_and_respect_opacity() {
let mut list = DisplayList::default();
list.push(DrawCommand::Clear(Color::rgb(0, 0, 0)));
list.push(DrawCommand::Text {
origin: Point::new(2.0, 2.0),
value: "Hello 中文".into(),
style: TextStyle::ui(1).scaled(1.25),
color: Color::rgba(255, 255, 255, 128),
});
let mut pixels = vec![0; 256 * 48];
SoftwareRenderer::render(&list, &mut pixels, 256, 48).unwrap();
assert!(pixels.iter().any(|pixel| *pixel > 0));
// 白字 alpha=128 叠黑底,任何通道都不应超过 128;中间覆盖值证明字形边缘仍有抗锯齿。
assert!(pixels.iter().all(|pixel| (pixel & 255) <= 128));
assert!(pixels.iter().any(|pixel| (1..128).contains(&(pixel & 255))));
let mut invisible = DisplayList::default();
invisible.push(DrawCommand::Text {
origin: Point::new(2.0, 2.0),
value: "Hello 中文".into(),
style: TextStyle::ui(1),
color: Color::rgba(255, 255, 255, 0),
});
let mut pixels = vec![0; 256 * 48];
SoftwareRenderer::render(&invisible, &mut pixels, 256, 48).unwrap();
assert!(pixels.iter().all(|pixel| *pixel == 0));
}
}