merge: resolve manuscript conflicts with latest main comments
This commit is contained in:
@@ -10,6 +10,7 @@ use crate::{
|
||||
/// 显示列表的 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) };
|
||||
@@ -21,9 +22,11 @@ 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));
|
||||
}
|
||||
@@ -33,7 +36,7 @@ impl RenderScope {
|
||||
}
|
||||
}
|
||||
impl Drop for RenderScope {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
/// 退出一次像素渲染作用域时归还嵌套深度;像素工作量仍按本帧累计,不因退出内层而返还。
|
||||
fn drop(&mut self) {
|
||||
RENDER_DEPTH.with(|depth| depth.set(depth.get() - 1));
|
||||
}
|
||||
@@ -41,6 +44,7 @@ impl Drop for RenderScope {
|
||||
/// 累加本帧中间表面工作量,超过预算立即返回错误。
|
||||
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");
|
||||
@@ -58,6 +62,7 @@ impl SoftwareRenderer {
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), &'static str> {
|
||||
// RAII 保护让成功、提前返回和错误传播都恢复深度;target 长度须由调用方保证至少 width×height。
|
||||
let _scope = RenderScope::enter()?;
|
||||
let mut canvas = Canvas {
|
||||
pixels: target,
|
||||
@@ -80,6 +85,7 @@ impl SoftwareRenderer {
|
||||
canvas.draw_image(image, *origin, *rect, *angle, *opacity, *tint);
|
||||
}
|
||||
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)
|
||||
@@ -95,24 +101,28 @@ impl SoftwareRenderer {
|
||||
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);
|
||||
@@ -130,6 +140,7 @@ impl SoftwareRenderer {
|
||||
);
|
||||
}
|
||||
DrawCommand::PopClip => {
|
||||
// 多余 PopClip 在当前实现中被忽略;合法列表仍应由生成层保证配对。
|
||||
if let Some(clip) = clips.pop() {
|
||||
canvas.clip = clip;
|
||||
}
|
||||
@@ -164,7 +175,7 @@ impl SoftwareRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 借用目标像素切片并维护当前裁剪,所有像素写入都受该上下文限制。
|
||||
/// 借用目标像素切片并维护当前裁剪;图形/文字受裁剪,Clear 明确填满整个目标。
|
||||
struct Canvas<'a> {
|
||||
pixels: &'a mut [u32],
|
||||
width: u32,
|
||||
@@ -225,14 +236,16 @@ 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 {
|
||||
@@ -246,6 +259,7 @@ impl Canvas<'_> {
|
||||
/// 用外轮廓与内轮廓的差绘制边框,避免把边框当成整个矩形填充。
|
||||
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(
|
||||
@@ -271,6 +285,7 @@ impl Canvas<'_> {
|
||||
|
||||
/// 把文字原点转换为局部裁剪区域,交给有界字形绘制,再混合到目标缓冲。
|
||||
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(
|
||||
@@ -289,6 +304,7 @@ impl Canvas<'_> {
|
||||
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);
|
||||
@@ -310,6 +326,7 @@ impl Canvas<'_> {
|
||||
value,
|
||||
style,
|
||||
color,
|
||||
// 换行按完整段落宽度排版,而不是按被祖先裁剪后剩余的可见宽度重新排版。
|
||||
Some(rect.width),
|
||||
clip,
|
||||
|dx, dy, pixel| self.blend_pixel(left + dx, top + dy, pixel),
|
||||
@@ -319,6 +336,7 @@ impl Canvas<'_> {
|
||||
|
||||
/// 把连续矩形转换成目标缓冲内的整数像素范围,避免在不可见区域进行光栅循环。
|
||||
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,
|
||||
@@ -335,6 +353,7 @@ impl Canvas<'_> {
|
||||
|
||||
/// 在画布与当前裁剪范围内进行透明度混合,防止文字/图形写出目标缓冲。
|
||||
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
|
||||
// 拒绝画布外、裁剪外与全透明像素;只有通过后才计算线性下标,避免负坐标转 usize。
|
||||
if x < 0
|
||||
|| y < 0
|
||||
|| x >= self.width as i32
|
||||
@@ -349,6 +368,7 @@ impl Canvas<'_> {
|
||||
}
|
||||
|
||||
let index = y as usize * self.width as usize + x as usize;
|
||||
// 全不透明像素可直接打包覆盖,省去目的通道解码与乘除法。
|
||||
if source.alpha == 255 {
|
||||
self.pixels[index] = pack(source);
|
||||
return;
|
||||
@@ -358,6 +378,7 @@ impl Canvas<'_> {
|
||||
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;
|
||||
@@ -379,6 +400,8 @@ fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
||||
// 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);
|
||||
@@ -388,6 +411,7 @@ fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
||||
|
||||
/// 将颜色通道打包成目标缓冲的整数像素表示。
|
||||
fn pack(color: Color) -> u32 {
|
||||
// 像素格式为 0x00RRGGBB,高字节不存透明度;透明度已在混合阶段折算进 RGB。
|
||||
(color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32
|
||||
}
|
||||
|
||||
@@ -396,6 +420,7 @@ 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)));
|
||||
@@ -420,6 +445,7 @@ mod tests {
|
||||
});
|
||||
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 {
|
||||
@@ -435,6 +461,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 在黑底4x4缓冲画2x2红矩形,核对内部像素变红、外部仍黑,覆盖最小坐标/打包路径。
|
||||
fn renderer_changes_pixels_inside_a_rectangle() {
|
||||
let mut list = DisplayList::default();
|
||||
list.push(DrawCommand::Clear(Color::rgb(0, 0, 0)));
|
||||
@@ -450,6 +477,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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)));
|
||||
@@ -462,6 +490,7 @@ mod tests {
|
||||
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))));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user