418 lines
16 KiB
Rust
418 lines
16 KiB
Rust
//! 把 DisplayList 写入 CPU 像素缓冲。裁剪栈限定矩形、文字和 3D 表面的实际写入区域。
|
|
//! RenderScope 约束递归深度;累计像素预算限制嵌套 3D 和世界 UI 的中间表面分配。错误向上传递,不把半幅结果当成成功帧呈现。
|
|
//!
|
|
use crate::{
|
|
display_list::{Color, DisplayList, DrawCommand},
|
|
geometry::{Point, Rect},
|
|
text::{self, TextStyle},
|
|
};
|
|
|
|
/// 显示列表的 CPU 执行入口,错误在原生 present 前向上传递。
|
|
pub struct SoftwareRenderer;
|
|
|
|
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();
|
|
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 {
|
|
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
|
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> {
|
|
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 } => {
|
|
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;
|
|
}
|
|
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;
|
|
canvas.pixels[y as usize * width as usize + x as usize] =
|
|
surface[v as usize * w as usize + u as usize];
|
|
}
|
|
}
|
|
}
|
|
DrawCommand::PushClip(rect) => {
|
|
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 => {
|
|
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(())
|
|
}
|
|
}
|
|
|
|
/// 借用目标像素切片并维护当前裁剪,所有像素写入都受该上下文限制。
|
|
struct Canvas<'a> {
|
|
pixels: &'a mut [u32],
|
|
width: u32,
|
|
height: u32,
|
|
clip: Rect,
|
|
}
|
|
|
|
impl Canvas<'_> {
|
|
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
|
|
fn clear(&mut self, color: Color) {
|
|
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);
|
|
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) {
|
|
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;
|
|
}
|
|
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) {
|
|
(
|
|
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) {
|
|
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;
|
|
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);
|
|
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 {
|
|
(color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
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]
|
|
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]
|
|
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));
|
|
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));
|
|
}
|
|
}
|