diff --git a/src/render/software_renderer.rs b/src/render/software_renderer.rs index df696dc..d432793 100644 --- a/src/render/software_renderer.rs +++ b/src/render/software_renderer.rs @@ -8,6 +8,7 @@ use crate::{ }; /// 显示列表的 CPU 执行入口,错误在原生 present 前向上传递。 +// shell 的窗口呈现和 scene3d::panel 的离屏纹理都使用同一执行器,输入是已转换到目标像素空间的 DisplayList。 pub struct SoftwareRenderer; // 预算按渲染线程维护,递归 3D/面板渲染共用同一组计数;不同线程之间不共享额度。 @@ -19,8 +20,10 @@ thread_local! { pub(crate) struct RenderScope; impl RenderScope { /// 进入一次嵌套渲染并检查深度上限;最外层调用重新开始累计像素预算。 + // SoftwareRenderer::render 与 scene3d::render_aspect 每次进入都建立作用域,面板中的嵌套视口继续累计。 pub fn enter() -> Result { RENDER_DEPTH.with(|depth| { + // 先读取进入前深度;拒绝分支不递增,因此不会产生没有配对 Guard 的计数。 let current = depth.get(); // 拒绝第 19 层进入,不先增加深度;成功返回的 RenderScope 析构时才对应减一。 if current >= 18 { @@ -30,6 +33,7 @@ impl RenderScope { if current == 0 { PIXEL_UNITS.with(|pixels| pixels.set(0)); } + // 仅成功进入后递增;返回的 RenderScope 析构时归还这一层。 depth.set(current + 1); Ok(Self) }) @@ -42,10 +46,12 @@ impl Drop for RenderScope { } } /// 累加本帧中间表面工作量,超过预算立即返回错误。 +// 3D 颜色/深度缓冲和 UI 面板纹理分配前调用;普通目标缓冲由外层分配,不在这里自动计入。 pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> { PIXEL_UNITS.with(|pixels| { // 累计申请工作量而非当前存活像素;释放一个中间表面不会返还额度,饱和加法避免溢出绕过限制。 let next = pixels.get().saturating_add(units); + // 超额时不提交 next,错误向上传递结束本次离屏绘制;预算不是可释放再循环使用的内存额度。 if next > 16_777_216 { return Err("nested render pixel budget exceeded"); } @@ -56,6 +62,7 @@ pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> { impl SoftwareRenderer { /// 顺序执行显示列表并维护裁剪栈;3D 和面板可递归进入同一渲染预算域。 + // 外部窗口和离屏面板调用;此函数不重新验证全部命令,也不负责创建或调整 target 长度。 pub fn render( list: &DisplayList, target: &mut [u32], @@ -64,15 +71,19 @@ impl SoftwareRenderer { ) -> Result<(), &'static str> { // RAII 保护让成功、提前返回和错误传播都恢复深度;target 长度须由调用方保证至少 width×height。 let _scope = RenderScope::enter()?; + // 借用传入像素缓冲建立当前画布,初始 clip 是整张目标;后续命令只修改画布状态或像素。 let mut canvas = Canvas { pixels: target, width, height, clip: Rect::new(0.0, 0.0, width as f32, height as f32), }; + // 保存 PushClip 之前的裁剪,PopClip 恢复上一层;栈只在本次 render 内存活。 let mut clips = Vec::new(); + // 严格按列表顺序执行,后续不透明命令覆盖已有像素,透明命令与当时像素混合。 for command in list.commands() { + // 图像和 3D 各有专用采样路径,其余变体转交 Canvas;坐标不再经过布局或页面相机。 match command { DrawCommand::Image { image, @@ -82,6 +93,7 @@ impl SoftwareRenderer { opacity, tint, } => { + // 图像旋转与透明度在 draw_image 中处理;这里仅解引用命令字段,保持共享图像资源。 canvas.draw_image(image, *origin, *rect, *angle, *opacity, *tint); } DrawCommand::Scene3D { rect, view } => { @@ -98,15 +110,20 @@ impl SoftwareRenderer { .min(height as f32) .ceil() .max(0.) as u32; + // 交集为空或原矩形退化时直接跳过,避免申请无用 3D 表面并发生归一化除零。 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.); + // 按同一 lod 求两个输出轴并向上取整至少一个像素;rect 宽高仍决定相机 aspect。 let w = (rect.width * lod).ceil().max(1.) as u32; let h = (rect.height * lod).ceil().max(1.) as u32; + // 获取不可变缓存像素,光栅化或预算错误立即传播;已经写入 target 的其他命令不会自动回滚。 let surface = crate::scene3d::surface(view, w, h, rect.width / rect.height)?; + // 只遍历裁剪可见的目标行列,来源坐标按完整 rect 映射而不是按裁剪交集重新拉伸。 for y in top..bottom { + // 目标像素中心在低分辨率 3D 表面上取最近邻,LOD 改变采样密度而非布局位置。 for x in left..right { // 按目标像素中心反算纹理坐标并钳到有效表面边界;只复制可见区域,不循环整个放大矩形。 let u = (((x as f32 + 0.5 - rect.x) / rect.width) * w as f32) @@ -124,6 +141,7 @@ impl SoftwareRenderer { 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( @@ -145,6 +163,7 @@ impl SoftwareRenderer { canvas.clip = clip; } } + // Clear 忽略 clip 填满整个切片;生成显示列表时应避免在局部子树中随意插入清屏。 DrawCommand::Clear(color) => canvas.clear(*color), DrawCommand::FillRoundedRect { rect, @@ -177,15 +196,18 @@ impl SoftwareRenderer { /// 借用目标像素切片并维护当前裁剪;图形/文字受裁剪,Clear 明确填满整个目标。 struct Canvas<'a> { + // 原地写入的不透明 0xRRGGBB 目标,生命周期只覆盖一次 render 调用。 pixels: &'a mut [u32], width: u32, height: u32, + // 当前目标空间矩形裁剪;图形扫描先收窄范围,blend_pixel 再检查最终像素坐标。 clip: Rect, } impl Canvas<'_> { /// Inverse-map only clipped destination pixels. Images use straight RGBA; /// bilinear interpolation happens in premultiplied alpha to avoid dark halos. + // render 的 Image 分支调用;对目标可见像素做逆旋转映射,避免正向散点产生空洞。 fn draw_image( &mut self, image: &crate::render::image::Image, @@ -195,9 +217,11 @@ impl Canvas<'_> { opacity: f32, tint: Option, ) { + // 退化尺寸或非有限旋转/透明度没有可用映射;直接返回且不改变像素。 if rect.width <= 0. || rect.height <= 0. || !angle.is_finite() || !opacity.is_finite() { return; } + // angle 是弧度;四个局部矩形角旋转后求目标包围盒,用于限制扫描范围。 let (sin, cos) = angle.sin_cos(); let corners = [ (rect.x, rect.y), @@ -205,8 +229,10 @@ impl Canvas<'_> { (rect.x, rect.y + rect.height), (rect.x + rect.width, rect.y + rect.height), ]; + // 最小值从正无穷、最大值从负无穷初始化,确保首个角点建立真实范围,不误含原点。 let mut lo = Point::new(f32::INFINITY, f32::INFINITY); let mut hi = Point::new(f32::NEG_INFINITY, f32::NEG_INFINITY); + // 绕命令 origin 旋转局部角点;rect.x/y 仍是相对该 origin 的偏移。 for (x, y) in corners { let px = origin.x + cos * x - sin * y; let py = origin.y + sin * x + cos * y; @@ -215,16 +241,23 @@ impl Canvas<'_> { hi.x = hi.x.max(px); hi.y = hi.y.max(py); } + // 旋转后的轴对齐包围盒还可能包含矩形外部像素,下面的 u/v 范围判断继续排除。 let (left, top, right, bottom) = self.clamped_bounds(Rect::new(lo.x, lo.y, hi.x - lo.x, hi.y - lo.y)); + // 只扫描包围盒与当前 clip/画布的交集,不按原始图像分辨率遍历。 for y in top..bottom { for x in left..right { + // 目标像素中心先减 origin,再乘反向旋转矩阵,恢复局部矩形坐标。 let dx = x as f32 + 0.5 - origin.x; let dy = y as f32 + 0.5 - origin.y; + // 减去局部 rect 原点并除宽高,得到采样函数接受的归一化纹理位置。 let u = (cos * dx + sin * dy - rect.x) / rect.width; let v = (-sin * dx + cos * dy - rect.y) / rect.height; + // 旋转包围盒内但局部矩形外的点不能采样;右/下边界为开区间。 if (0. ..1.).contains(&u) && (0. ..1.).contains(&v) { + // 图像采样内部在预乘 alpha 空间插值,返回非预乘颜色供 blend_pixel 合成;整体透明度钳到 [0,1]。 let mut sample = image.sample(u, v, opacity.clamp(0., 1.)); + // 有 tint 时替换 RGB 而保留采样 alpha,把图像当作覆盖遮罩使用,不逐通道乘原图颜色。 if let Some(tint) = tint { sample.red = tint.red; sample.green = tint.green; @@ -237,18 +270,23 @@ impl Canvas<'_> { } /// 用不透明 RGB 填满目标切片;此命令忽略当前裁剪和输入颜色 alpha。 + // render 的 Clear 分支调用;target 若大于 width×height,这里也会填满额外切片,因此调用方应传精确缓冲。 fn clear(&mut self, color: Color) { // Clear 填整个提供的目标切片,不受当前 clip 约束;pack 忽略 alpha,清屏色作为不透明 RGB。 self.pixels.fill(pack(color)); } /// 只遍历裁剪后的矩形像素,按圆角覆盖决定填充区域。 + // render 的 FillRoundedRect 分支调用;圆角内像素交给 blend_pixel,统一透明度与边界判断。 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); + // bounds 顺序为 left/top/right/bottom,使用左闭右开的整数扫描区间。 for y in bounds.1..bounds.3 { for x in bounds.0..bounds.2 { + // 只有落在圆角形状内才混合;clip 外的候选即使被保守范围包含也会被 blend_pixel 拒绝。 if rounded_rect_contains(rect, radius, x as f32 + 0.5, y as f32 + 0.5) { self.blend_pixel(x, y, color); } @@ -257,10 +295,12 @@ impl Canvas<'_> { } /// 用外轮廓与内轮廓的差绘制边框,避免把边框当成整个矩形填充。 + // render 的 StrokeRoundedRect 分支调用;边框占据外圆角形状减去内圆角形状的区域。 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, @@ -268,12 +308,16 @@ impl Canvas<'_> { (rect.width - width * 2.0).max(0.0), (rect.height - width * 2.0).max(0.0), ); + // 内轮廓与缩进矩形配套;宽描边把内区缩到零时,内轮廓判定返回 false。 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) { @@ -284,6 +328,7 @@ impl Canvas<'_> { } /// 把文字原点转换为局部裁剪区域,交给有界字形绘制,再混合到目标缓冲。 + // render 的 Text 分支调用;字形覆盖由 text 模块产生,本模块只处理目标位置、clip 和混合。 fn draw_text(&mut self, origin: Point, value: &str, style: TextStyle, color: Color) { // 将文字原点对齐整数像素;转换 clip 到字形局部空间,回调再加原点写回目标。 let x = origin.x.round() as i32; @@ -294,13 +339,16 @@ impl Canvas<'_> { self.clip.width, self.clip.height, ); + // None 表示没有段落换行宽度;回调收到字形局部像素,平移后仍经 blend_pixel 检查目标边界。 text::draw_clipped(value, style, color, None, clip, |dx, dy, pixel| { self.blend_pixel(x + dx, y + dy, pixel) }); } /// 把段落矩形与当前裁剪相交,按受限宽度绘制,再恢复外层裁剪。 + // render 的 WrappedText 分支调用;与普通文字共用字形路径,但临时加上段落框裁剪。 fn draw_wrapped_text(&mut self, rect: Rect, value: &str, style: TextStyle, color: Color) { + // 空段落框直接跳过,不进入文字排版也不修改当前 clip。 if rect.width <= 0.0 || rect.height <= 0.0 { return; } @@ -314,6 +362,7 @@ impl Canvas<'_> { ((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), ); + // 段落原点对齐整数像素,随后把交集 clip 换到文字局部空间;排版宽度仍保持原框宽。 let left = rect.x.round() as i32; let top = rect.y.round() as i32; let clip = Rect::new( @@ -331,10 +380,12 @@ impl Canvas<'_> { clip, |dx, dy, pixel| self.blend_pixel(left + dx, top + dy, pixel), ); + // 文字回调同步执行完成后恢复外层 clip,后续显示命令不继承段落框限制。 self.clip = old; } /// 把连续矩形转换成目标缓冲内的整数像素范围,避免在不可见区域进行光栅循环。 + // 填充、描边和图像共同使用;完全分离时可产生空的整数区间,调用方 range 循环自然不执行。 fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) { // 左/上 floor,右/下 ceil 形成保守循环范围;blend_pixel 还会按当前 clip 检查实际整数像素。 ( @@ -352,6 +403,7 @@ impl Canvas<'_> { } /// 在画布与当前裁剪范围内进行透明度混合,防止文字/图形写出目标缓冲。 + // 文字覆盖回调、矩形和图像最终都进入此处;3D 表面分支已经自行裁剪,直接复制 RGB。 fn blend_pixel(&mut self, x: i32, y: i32, source: Color) { // 拒绝画布外、裁剪外与全透明像素;只有通过后才计算线性下标,避免负坐标转 usize。 if x < 0 @@ -367,6 +419,7 @@ impl Canvas<'_> { return; } + // 边界检查通过后才将坐标转 usize,目标下标按 width 逐行计算。 let index = y as usize * self.width as usize + x as usize; // 全不透明像素可直接打包覆盖,省去目的通道解码与乘除法。 if source.alpha == 255 { @@ -374,6 +427,7 @@ impl Canvas<'_> { return; } + // 半透明路径拆开已有 RGB,再用源 alpha 与剩余权重逐通道合成;目的缓冲没有单独 alpha。 let destination = self.pixels[index]; let dst_red = (destination >> 16) & 0xff; let dst_green = (destination >> 8) & 0xff; @@ -389,10 +443,13 @@ impl Canvas<'_> { } /// 测试一个像素采样点是否位于圆角矩形内,角区域使用圆弧距离。 +// fill/stroke 的内部形状测试,参数 x/y 是像素中心;radius<=0 退化为普通矩形判断。 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; } @@ -404,12 +461,14 @@ fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool { // 先减大原点可降低浮点舍入导致 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 } /// 将颜色通道打包成目标缓冲的整数像素表示。 +// clear 和全不透明 blend_pixel 直接使用;半透明路径先完成合成再自行打包。 fn pack(color: Color) -> u32 { // 像素格式为 0x00RRGGBB,高字节不存透明度;透明度已在混合阶段折算进 RGB。 (color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32 @@ -422,6 +481,7 @@ mod tests { #[test] /// 画长文字进入嵌套裁剪,逐像素检查多行与裁剪交集;PopClip后的蓝矩形验证后续命令不沿用旧clip。 fn wrapped_text_uses_multiple_lines_respects_nested_clips_and_restores_the_clip() { + // 依次构造白底、祖先 clip、换行文字、PopClip、零宽文字与远处蓝框,用不同命令检测裁剪状态。 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))); @@ -443,13 +503,18 @@ mod tests { radius: 0.0, color: Color::rgb(0, 0, 255), }); + // 目标尺寸覆盖文字和蓝框,初始化后由 Clear 建立统一白背景。 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 { + // 按目标行宽 80 读取 RGB;此测试不依赖具体字体的逐像素位图。 let color = pixels[y * 80 + x]; + // 识别文字及其抗锯齿边缘,检查位置并累计第二行存在性。 if color != 0xffffff && color != 0x0000ff { assert!((12..32).contains(&x) && (5..45).contains(&y)); second_line |= y >= 25; @@ -457,12 +522,14 @@ mod tests { } } assert!(second_line); + // 蓝框在原祖先 clip 外,成功出现说明 PopClip 与段落私有 clip 都已恢复。 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 { @@ -470,6 +537,7 @@ mod tests { radius: 0.0, color: Color::rgb(255, 0, 0), }); + // 4×4 行主序缓冲中下标 5 是 (1,1),下标 0 在矩形外,分别断言覆盖与保留。 let mut pixels = vec![0; 16]; SoftwareRenderer::render(&list, &mut pixels, 4, 4).unwrap(); assert_eq!(pixels[5], 0x00ff0000); @@ -479,6 +547,7 @@ mod tests { #[test] /// 半透明白字叠黑底必须含中间抗锯齿值且通道不超128;alpha=0的文字完全不改像素。 fn text_edges_are_antialiased_and_respect_opacity() { + // 白色字形以 alpha=128 画在黑底,抗锯齿覆盖进一步降低边缘 alpha,输出应包含中间亮度。 let mut list = DisplayList::default(); list.push(DrawCommand::Clear(Color::rgb(0, 0, 0))); list.push(DrawCommand::Text { @@ -487,6 +556,7 @@ mod tests { 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)); @@ -494,6 +564,7 @@ mod tests { assert!(pixels.iter().all(|pixel| (pixel & 255) <= 128)); assert!(pixels.iter().any(|pixel| (1..128).contains(&(pixel & 255)))); + // 另建全透明文字列表,不依赖上一张已绘制缓冲;零 alpha 应在混合入口直接跳过。 let mut invisible = DisplayList::default(); invisible.push(DrawCommand::Text { origin: Point::new(2.0, 2.0), @@ -501,6 +572,7 @@ mod tests { 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)); diff --git a/src/render/source_paint.rs b/src/render/source_paint.rs index a6d49f7..a6c9e01 100644 --- a/src/render/source_paint.rs +++ b/src/render/source_paint.rs @@ -22,11 +22,14 @@ use std::{ }; /// 统一构造本模块的参数错误,让调用方停止处理无效输入。 +// 字段解码、名义类型检查和原生驱动适配器共用此构造,错误回到只读 paint 调用链。 fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } /// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。 +// named_type、decode 和 Collect 提取对象字段时调用;不把标量转换成临时对象来接受错误输入。 fn object(value: Value) -> Result { + // 只有真实托管身份可交给 ObjectStore 查询;具体名义类型由使用这个身份的后续步骤检查。 if let Value::Object(object) = value { Ok(object) } else { @@ -34,6 +37,7 @@ fn object(value: Value) -> Result { } } /// 读取指定命令字段并要求其值为 Text;不隐式字符串化其他类型。 +// decode 读取 kind/color/text 时调用;属性不存在的错误由 get_attr 原样传播。 fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result { if let Value::Text(value) = store.get_attr(object, name)? { Ok(value) @@ -42,7 +46,9 @@ fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result Result { + // 先要求 VM Number,再转换成原生 f32;不接受仅在文本上可解析的字符串。 let Value::Number(value) = store.get_attr(object, name)? else { return Err(invalid("paint field must be a Number")); }; @@ -60,7 +66,9 @@ fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result Result { + // desktop.types 本身必须为托管注册表对象;缺失类型不能静默采用结构匹配。 let registry = object(store.get_attr(desktop, "types")?)?; object( store @@ -69,6 +77,7 @@ fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result crate::bridge::graphics::decode_image(store, command)?, "scene3d" => { // 这里仅接受宿主生成的 PaintView payload,克隆原生 Arc,不把活动的 VM 场景图传到绘制线程。 @@ -104,6 +117,7 @@ fn decode( } } "roundedRect" | "strokeRoundedRect" => { + // 圆角填充与描边共享宽高半径;只有描边分支另外要求 strokeWidth。 let width = number(store, command, "width")?; let height = number(store, command, "height")?; let radius = number(store, command, "radius")?; @@ -111,6 +125,7 @@ fn decode( if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 { return Err(invalid("paint rectangle exceeds bounds")); } + // 同一组几何字段决定两个不同命令变体;填充不能因为缺少 strokeWidth 而失败。 if kind == "roundedRect" { RenderCommand::RoundedRect { x, @@ -143,7 +158,9 @@ fn decode( store.get_attr(command, "wrapWidth"), store.get_attr(command, "wrapHeight"), ) { + // 两个字段都缺失代表没有显式换行边界的文本;单个缺失或其他属性错误不属于该情况。 (Err(VmError::AttributeNotFound(_)), Err(VmError::AttributeNotFound(_))) => None, + // 两个字段存在后重新按数值规则读取,拒绝负长度或非 Number,而非只判断存在性。 (Ok(_), Ok(_)) => Some([ number(store, command, "wrapWidth")?, number(store, command, "wrapHeight")?, @@ -176,11 +193,14 @@ struct PaintOutput { // 全轮源命令累计数,不能靠拆分多个源 DisplayList 绕过 10000 条总预算。 count: usize, } +// Prepare、Advance、Collect 和 Work 共享纯原生输出;锁只保护短时记账,不跨源方法调用。 type Output = Arc>; #[derive(Debug)] /// 检查源 paint 返回列表并将命令保存到当前出现的绘制结果。 struct Collect { + // 当前驱动所属桌面,用它查 DisplayList/PaintCommand 类型,并由原生函数 roots 保持存活。 desktop: ObjectRef, + // 当前出现 ID 是输出键,不是 Block 身份;一个 Block 多次出现允许返回不同画面。 mount: u64, output: Output, } @@ -192,9 +212,11 @@ impl NativeCallable for Collect { /// 验证返回名义类型并完整解码所有 PaintCommand,检查全轮命令预算后才把该出现位置结果写入输出表。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { + // collect_and_continue 把源 paint 的唯一返回值传给收集器;参数数量或外层类型错误都停止本轮。 let [Value::Object(result)] = args else { return Err(invalid("paint must return a DisplayList")); }; + // 外层必须属于本桌面 DisplayList;普通含 commands 字段的对象不等同于名义绘制列表。 if !cx.store().is_instance( *result, named_type(cx.store(), self.desktop, "DisplayList")?, @@ -204,6 +226,7 @@ impl NativeCallable for Collect { // 先验证外层 DisplayList,再取其受控 ObjectList;所有成员解码成功后才写入 output。 let commands = object(cx.store().get_attr(*result, "commands")?)?; let expected = named_type(cx.store(), self.desktop, "PaintCommand")?; + // 先取得受控列表中的对象身份,再逐项解码并 collect;任何单项失败都不会提交当前 Mount 的部分输出。 let commands = object_list::items(cx.store(), commands)? .iter() .map(|command| decode(cx.store(), *command, expected)) @@ -213,9 +236,11 @@ impl NativeCallable for Collect { .output .lock() .map_err(|_| invalid("paint output poisoned"))?; + // 累计的是所有源覆盖的命令数,不能按 Mount 分拆规避一万条上限。 if output.count + commands.len() > 10_000 { return Err(invalid("paint command budget exceeded")); } + // 预算通过后才更新计数并保存完整列表;返回 Nil 只是辅助调用链结果,不是绘制命令。 output.count += commands.len(); output.commands.insert(self.mount, commands); Ok(NativeResult::Return(Value::Nil)) @@ -225,6 +250,7 @@ impl NativeCallable for Collect { /// 为当前出现准备 paint 方法、几何和类型检查上下文。 struct Prepare { desktop: ObjectRef, + // 按帧绘制顺序保存出现 ID 和弱身份;驱动不会因等待绘制而强持有全部 Mount。 mounts: Vec<(u64, WeakObjectRef)>, output: Output, } @@ -247,6 +273,7 @@ impl NativeCallable for Prepare { }, vec![self.desktop], ))?; + // 固定 VM 字节码循环反复调用 advance,true 继续、false 结束;所有源方法共享同一次执行预算。 let function = crate::vm_sequence::repeat_call(cx, advance, "source paint", self.desktop)?; Ok(NativeResult::Invoke { function, @@ -259,7 +286,9 @@ impl NativeCallable for Prepare { /// 安排下一个出现的绘制调用,保持原始绘制顺序和共享 VM 预算。 struct Advance { desktop: ObjectRef, + // Prepare 移交的固定顺序列表,在 VM 切片之间保持不变;游标只向前推进。 mounts: Arc<[(u64, WeakObjectRef)]>, + // 下一个待准备出现的下标;不是已输出命令数,跳过原生 paint 的出现也占一项。 next: Mutex, output: Output, } @@ -276,6 +305,7 @@ impl NativeCallable for Advance { .next .lock() .map_err(|_| invalid("paint cursor poisoned"))?; + // 复制当前项后只在存在该项时递增,列表结束后保持游标不再增长。 let occurrence = self.mounts.get(*next).copied(); *next += usize::from(occurrence.is_some()); occurrence @@ -284,17 +314,22 @@ impl NativeCallable for Advance { let Some((id, weak)) = occurrence else { return Ok(NativeResult::Return(Value::Bool(false))); }; + // 进入本次出现时再升级弱引用;失效视为本帧上下文错误,不跳过后继续发布不完整画面。 let mount = cx .upgrade(weak)? .ok_or(invalid("paint Mount has expired"))?; + // 克隆原生 Mount 描述后再分配临时 Bounds,避免持有跨 VM 可变操作的 store 借用。 let data = cx.store().payload::(mount, "Mount")?.clone(); + // 身份仍存活不代表仍挂载;已经脱离出现树的 Mount 不能继续绘制。 if !data.mounted { return Err(invalid("paint Mount is detached")); } + // 从当前业务 Block 的类型解析 paint,支持源程序覆盖方法;不把旧布局快照当成方法定义缓存。 let definition = cx .store() .type_of(data.block)? .ok_or(invalid("untyped paint target"))?; + // lookup_method 沿类型查找方法;缺失时明确报错,原生默认方法由下方 is_intrinsic 分支识别。 let function = cx .store() .lookup_method(definition, "paint")? @@ -306,6 +341,7 @@ impl NativeCallable for Advance { // 把已提交 Mount 几何封装为本轮临时 Bounds;新分配参数可初始化,业务模型仍处于只读域。 let bounds = cx.new_object()?; attach_desktop_type(cx, bounds, "Bounds", self.desktop)?; + // 按布局绝对坐标填充 x/y/width/height;这些临时属性可初始化,不表示源方法能修改已有业务模型。 for (name, value) in [ ("x", data.geometry.x), ("y", data.geometry.y), @@ -337,6 +373,7 @@ impl NativeCallable for Advance { "paint occurrence", self.desktop, )?; + // 交回 VM 执行这个零参数辅助函数,源方法及收集器仍由 VM 调用栈驱动,不在 Rust 中递归解释。 Ok(NativeResult::Invoke { function, arguments: vec![], @@ -344,44 +381,54 @@ impl NativeCallable for Advance { } } -/// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。 +/// 源绘制完成后的未发布结果:已装配帧、业务读取依赖和调度切片数。 pub struct Draft { // 已经合并命令的原生帧;Draft 仍需桥接层提交,完成计算不等于发布成功。 pub frame: Frame, // 保留源绘制的业务读取依赖,供下次写入后判定是否必须重绘。 pub reads: Vec, + // 包括 start 的第一片和每次 resume;供桥接层记录实际绘制切片数。 pub slices: usize, } #[must_use = "pending paint must be resumed or cancelled on its owning VM"] /// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。 pub enum Progress { + // 运行时暂存此句柄,后续在原 VM 继续或取消,不能当作可提交帧。 Pending(Box), + // 只读调用结束且命令装配完成;UI bridge 仍需更新依赖,endor_host 再发布文档。 Complete(Box), } /// Only unpublished native commands and an owning prepare root cross a pause. /// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。 pub struct Work { + // 所属窗口的桌面根,用于完成时过滤宿主记账读取;prepare 的 VM roots 维持其生命周期。 desktop: ObjectRef, // 跨切片保留的入口根;完成或取消都必须在所属 VM 中显式释放。 prepare: ObjectRef, + // 先前切片已收集的不可变原生命令;恢复续体时延续同一输出,不从头重跑已完成覆盖。 output: Output, + // 布局阶段的不可变视觉根,finish 通过它补齐未被源覆盖的原生绘制。 root: Option>, + // 尚未发布的布局帧,装配命令只修改这个工作副本。 frame: Frame, slices: usize, } impl Work { /// 继续当前绘制续体,保留其先前收集的命令和所属窗口上下文。 + // ui_bridge::resume_paint_frame 传入原 VM 和切片预算,endor_host 保存返回的新 Pending 或处理 Complete。 pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result { // 恢复保留原执行预算,slice 只决定本次让出的指令配额,不能重置整个绘制上限。 self.slices += 1; + // 调用 VM 恢复同一个只读续体,结果统一交 accept 处理,避免开始/恢复使用不同收尾路径。 let progress = vm.resume_read_only_call(slice); self.accept(vm, progress) } /// 关闭未完成绘制续体并释放其临时根,不发布半完成命令列表。 + // ui_bridge 的窗口切换、输入替换或退役清理路径调用;消费 Work,禁止继续恢复同一个句柄。 pub fn cancel(self, vm: &mut Vm) -> Result<()> { // 先执行取消和 release 两个操作,再依次传播错误;取消失败也仍尝试释放入口根。 let cancelled = vm.cancel_read_only_call(); @@ -397,6 +444,7 @@ impl Work { vm: &mut Vm, progress: Result, ) -> Result { + // 三种结果分别保留 Work、生成 Draft 或清理后返回诊断;只有 Complete 才进入 finish。 match progress { // 挂起时工作句柄继续持有 frame/output/prepare,不能发布部分输出。 Ok(endor_core::ExecutionProgress::Pending { .. }) => { @@ -423,6 +471,7 @@ impl Work { } /// 建立本轮只读绘制驱动和结果收集器;源覆盖与原生默认绘制最终保持同一出现顺序。 +// ui_bridge::begin_paint_frame 在布局完成后调用;mounts 顺序来自 frame,root/frame 均是未发布的本轮草稿。 pub fn start( vm: &mut Vm, desktop: ObjectRef, @@ -437,7 +486,9 @@ pub fn start( !vm.has_suspended_call() && !vm.object_store().is_read_only(), "cannot start paint while another read-only execution is active" ); + // 为整轮创建一个结果容器,跨所有 Mount 和所有挂起切片共享。 let output = Arc::new(Mutex::new(PaintOutput::default())); + // 分配有根的驱动入口;显式捕获 desktop,Work 负责完成或取消后释放入口根。 let prepare = vm.allocate(ContextNativeFunction::new( "prepare source paint", Prepare { @@ -447,6 +498,7 @@ pub fn start( }, vec![desktop], ))?; + // 在进入 VM 前绑定输入草稿、输出和入口根;即使第一片马上完成也走同一个 Work::accept。 let work = Work { desktop, prepare, @@ -455,6 +507,7 @@ pub fn start( frame, slices: 1, }; + // 限制总帧深与总指令数,slice 只切分调度时间;源方法不能借多次 resume 重置总预算。 let result = vm.start_read_only_call( prepare, &[], @@ -468,7 +521,9 @@ pub fn start( } /// 按 Mount 合并源覆盖和原生命令,补齐投影/裁剪表,再递归装配世界 UI 面板。 +// 仅由 Work::accept 在 VM Complete 后调用,此时 prepare 已释放;输出容器中只有成功解码的完整覆盖。 fn finish(vm: &mut Vm, mut work: Work, reads: Vec) -> Result { + // 加锁读取整轮收集结果;finish 不再调用源 paint,锁不会跨越新的源执行。 let output = work .output .lock() @@ -483,20 +538,26 @@ fn finish(vm: &mut Vm, mut work: Work, reads: Vec) - slices: work.slices, }))); } + // 根已在上面检查存在;借用 Arc 内节点建立索引,不把原始 UiNode 所有权转入新命令。 let root = work.root.as_deref(); + // 保存桌面身份供末尾依赖过滤,frame 则可单独可变借用以写入新命令表。 let desktop = work.desktop; let frame = &mut work.frame; // 建立 Block ID 到不可变节点的索引;最终合并顺序仍以 frame.mounts 为准,不能按 map 排序。 let mut nodes = HashMap::new(); + // 显式栈遍历视觉树,节点按 Block 文本身份索引;此遍历顺序不决定最终绘制顺序。 let mut pending: Vec<_> = root.into_iter().collect(); + // 每个节点入表并展开其子节点,稍后每个 Mount 用 block_id 查回该节点的默认视觉。 while let Some(node) = pending.pop() { nodes.insert(node.block_id.as_str(), node); pending.extend(node.children().iter().map(|node| node.as_ref())); } + // 四个输出数组同步增长:命令、裁剪、二维变换、出现归属;不能单独过滤其中某一表。 let mut commands = Vec::new(); let mut clips = Vec::new(); let mut views = Vec::new(); let mut owners = Vec::new(); + // 最终顺序由布局帧的出现序列决定,既保留层叠顺序,也允许同一 Block 对应多个 Mount。 for mount in &frame.mounts { // 记录本出现追加前的长度,用差值为新增命令同步扩展 clip/view/owner 三张平行表。 let before = commands.len(); @@ -504,6 +565,7 @@ fn finish(vm: &mut Vm, mut work: Work, reads: Vec) - if let Some(custom) = output.commands.get(&mount.id) { commands.extend(custom.iter().cloned()); } else { + // 无源覆盖时必须找到原生视觉节点;缺失说明布局/视觉投影不一致,不能默默漏画。 let node = nodes .get(mount.block_id.as_str()) .ok_or_else(|| anyhow::anyhow!("paint projection has no node"))?; @@ -517,6 +579,7 @@ fn finish(vm: &mut Vm, mut work: Work, reads: Vec) - }, )); } + // 每条新增命令继承当前出现的裁剪、变换和 Mount ID,后续面板装配依赖这些平行元数据。 clips.extend(std::iter::repeat_n(mount.clip, commands.len() - before)); views.extend(std::iter::repeat_n(mount.view, commands.len() - before)); owners.extend(std::iter::repeat_n(mount.id, commands.len() - before)); @@ -525,18 +588,21 @@ fn finish(vm: &mut Vm, mut work: Work, reads: Vec) - bail!("paint command budget exceeded"); } } + // 全部 Mount 成功合并后再替换草稿帧的旧命令与元数据;尚未发布给窗口。 frame.commands = commands; frame.command_clips = clips; frame.command_views = views; frame.command_mounts = owners; // 统一命令和投影元数据就绪后,再把世界 UI 面板装配进对应 3D 场景。 crate::scene3d::panel::compose(frame)?; + // 类型注册表读取属于宿主检查,过滤它避免把内部类型查找当成业务 paint 依赖。 let registry = object(vm.get_attr(desktop, "types")?)?; // 排除宿主桌面、类型注册表、Mount 以及已经死亡的临时身份,只保留业务读取。 // 纯原生帧不需要保留这次驱动的读取记录。 let reads = reads .into_iter() .filter(|dependency| { + // 对每条读取取所属对象:无源覆盖时全部丢弃,否则剔除宿主桌面、注册表、Mount 和已死亡身份。 let object = dependency.object(); !native_only && object != desktop @@ -546,6 +612,7 @@ fn finish(vm: &mut Vm, mut work: Work, reads: Vec) - .is_ok_and(|object| !object.payload().as_any().is::()) }) .collect(); + // 返回完整但未发布的草稿;桥接层接着记录依赖和切片数,运行时再完成帧提交。 Ok(Progress::Complete(Box::new(Draft { frame: work.frame, reads,