//! 执行源 paint 覆盖并解码受检查的 PaintCommand。 //! 所有绘制回调位于只读域;临时返回对象可分配,但已有模型不可写。命令数量、类型、坐标和颜色在合入帧前检查。 //! 源命令与原生命令按 Mount 绘制顺序合并,随后装配世界 UI 面板。挂起绘制同样需要显式恢复或取消。 //! //! Source paint dispatch produces checked immutable commands. One read-only VM //! resumable call sequences all overrides, sharing one instruction/frame budget. use crate::{ document::{Frame, RenderCommand}, layout::{self, Size}, mount::Mount, object_list, ui_bridge::{MAX_DIMENSION, UiNode, attach_desktop_type}, }; use anyhow::{Result, bail}; use endor_core::{ ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectRef, ObjectStore, Value, Vm, VmError, WeakObjectRef, }; use std::{ collections::{BTreeMap, HashMap}, sync::{Arc, Mutex}, }; /// 统一构造本模块的参数错误,让调用方停止处理无效输入。 fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } /// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。 fn object(value: Value) -> Result { if let Value::Object(object) = value { Ok(object) } else { Err(invalid("paint must return managed objects")) } } /// 读取指定命令字段并要求其值为 Text;不隐式字符串化其他类型。 fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result { if let Value::Text(value) = store.get_attr(object, name)? { Ok(value) } else { Err(invalid("paint field must be text")) } } /// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。 fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result { let Value::Number(value) = store.get_attr(object, name)? else { return Err(invalid("paint field must be a Number")); }; // x/y 允许负数以表达相对原点的绘制;其余长度必须非负。所有数值都必须有限且在尺寸预算内。 let value = value .to_string() .parse::() .map_err(|_| invalid("invalid paint number"))?; if !value.is_finite() || value.abs() > MAX_DIMENSION as f32 || (!matches!(name, "x" | "y") && value < 0.0) { return Err(invalid("paint number exceeds bounds")); } Ok(value) } /// 从当前 desktop 的类型注册表取得绘制结果必须符合的托管类型。 fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result { let registry = object(store.get_attr(desktop, "types")?)?; object( store .dict_get(registry, &Value::Text(name.into()))? .ok_or(invalid("missing paint type"))?, ) } /// 把源方法返回的托管绘制值解码成受限 Rust 命令,检查名义类型、字段和绘制预算。 fn decode( store: &ObjectStore, command: ObjectRef, expected: ObjectRef, ) -> Result { // 使用当前 desktop 注册的 PaintCommand 名义类型;字段形状相似的普通对象也不能冒充命令。 if !store.is_instance(command, expected)? { return Err(invalid("paint commands must be PaintCommand instances")); } let x = number(store, command, "x")?; let y = number(store, command, "y")?; let kind = text(store, command, "kind")?; // 3D 命令使用不可变场景视图,不读取颜色文本;其他命令的颜色由最终 validate 统一校验。 let color = if kind == "scene3d" { String::new() } else { text(store, command, "color")? }; let result = match kind.as_str() { "scene3d" => { // 这里仅接受宿主生成的 PaintView payload,克隆原生 Arc,不把活动的 VM 场景图传到绘制线程。 let snapshot = object(store.get_attr(command, "scene")?)?; RenderCommand::Scene3D { x, y, width: number(store, command, "width")?, height: number(store, command, "height")?, view: store .payload::(snapshot, "immutable 3D paint snapshot")? .0 .clone(), } } "roundedRect" | "strokeRoundedRect" => { let width = number(store, command, "width")?; let height = number(store, command, "height")?; let radius = number(store, command, "radius")?; // 单项数值合法还不足以保证右/下边界合法;额外检查原点与尺寸相加后的范围。 if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 { return Err(invalid("paint rectangle exceeds bounds")); } if kind == "roundedRect" { RenderCommand::RoundedRect { x, y, width, height, radius, color, } } else { RenderCommand::StrokeRoundedRect { x, y, width, height, radius, color, stroke_width: number(store, command, "strokeWidth")?, } } } "text" => { let size = number(store, command, "size")?; // size 是离散文字等级而非任意像素字号,只接受 1 到 8 的整数。 if size.fract() != 0.0 || !(1.0..=8.0).contains(&size) { return Err(invalid("paint text size must be an integer from 1 to 8")); } // 两个换行边界必须同时出现或同时缺失;只缺一个时传播属性错误,防止半定义文本框。 let wrap_bounds = match ( store.get_attr(command, "wrapWidth"), store.get_attr(command, "wrapHeight"), ) { (Err(VmError::AttributeNotFound(_)), Err(VmError::AttributeNotFound(_))) => None, (Ok(_), Ok(_)) => Some([ number(store, command, "wrapWidth")?, number(store, command, "wrapHeight")?, ]), (Err(error), _) | (_, Err(error)) => return Err(error), }; RenderCommand::Text { x, y, value: text(store, command, "text")?, size: size as u32, color, wrap_bounds, } } _ => return Err(invalid("unsupported paint command kind")), }; // 变体字段解码后仍走统一命令校验,覆盖颜色格式、文本以及组合几何合法性。 result .validate() .map_err(|_| invalid("invalid paint command geometry, text or color"))?; Ok(result) } #[derive(Debug, Default)] /// 按出现 ID 收集已检查的源绘制覆盖,随后与原生默认命令合并。 struct PaintOutput { // 按出现 ID 收集覆盖;同一个业务 Block 在多个 Mount 中可产生不同绘制结果。 commands: BTreeMap>, // 全轮源命令累计数,不能靠拆分多个源 DisplayList 绕过 10000 条总预算。 count: usize, } type Output = Arc>; #[derive(Debug)] /// 检查源 paint 返回列表并将命令保存到当前出现的绘制结果。 struct Collect { desktop: ObjectRef, mount: u64, output: Output, } impl NativeCallable for Collect { /// 接收当前出现位置 paint 的唯一 DisplayList 结果。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } /// 验证返回名义类型并完整解码所有 PaintCommand,检查全轮命令预算后才把该出现位置结果写入输出表。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let [Value::Object(result)] = args else { return Err(invalid("paint must return a DisplayList")); }; if !cx.store().is_instance( *result, named_type(cx.store(), self.desktop, "DisplayList")?, )? { return Err(invalid("paint must return a DisplayList")); } // 先验证外层 DisplayList,再取其受控 ObjectList;所有成员解码成功后才写入 output。 let commands = object(cx.store().get_attr(*result, "commands")?)?; let expected = named_type(cx.store(), self.desktop, "PaintCommand")?; let commands = object_list::items(cx.store(), commands)? .iter() .map(|command| decode(cx.store(), *command, expected)) .collect::, _>>()?; // 仅收集结果时持锁,不在调用源 paint 期间持有结果锁;失败不会插入半个命令列表。 let mut output = self .output .lock() .map_err(|_| invalid("paint output poisoned"))?; if output.count + commands.len() > 10_000 { return Err(invalid("paint command budget exceeded")); } output.count += commands.len(); output.commands.insert(self.mount, commands); Ok(NativeResult::Return(Value::Nil)) } } #[derive(Debug)] /// 为当前出现准备 paint 方法、几何和类型检查上下文。 struct Prepare { desktop: ObjectRef, mounts: Vec<(u64, WeakObjectRef)>, output: Output, } impl NativeCallable for Prepare { /// 零参数驱动入口,待绘制 Mount 和输出容器已保存在适配器。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// 建立共享游标与固定外层字节码循环,让 VM 每轮只准备一个出现位置的 paint 调用。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 构造一个共享游标驱动;每轮只准备当前出现,避免一次保留所有出现的临时实参。 let advance = cx.allocate(ContextNativeFunction::new( "next paint occurrence", Advance { desktop: self.desktop, mounts: self.mounts.clone().into(), next: Mutex::new(0), output: self.output.clone(), }, vec![self.desktop], ))?; let function = crate::vm_sequence::repeat_call(cx, advance, "source paint", self.desktop)?; Ok(NativeResult::Invoke { function, arguments: vec![], }) } } #[derive(Debug)] /// 安排下一个出现的绘制调用,保持原始绘制顺序和共享 VM 预算。 struct Advance { desktop: ObjectRef, mounts: Arc<[(u64, WeakObjectRef)]>, next: Mutex, output: Output, } impl NativeCallable for Advance { /// 由外层循环无参调用,下一出现位置由内部游标决定。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// 短锁内取并推进游标,随后验证/准备当前 Mount 的方法与几何;用临时调用链收集结果,完成后才进入下一位置。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 锁只保护取游标与递增;离开此块后释放锁,再做 VM 分配和源调用。 let occurrence = { let mut next = self .next .lock() .map_err(|_| invalid("paint cursor poisoned"))?; let occurrence = self.mounts.get(*next).copied(); *next += usize::from(occurrence.is_some()); occurrence }; // false 是 repeat_call 的停止信号;有出现时即使跳过原生方法也返回 true 继续遍历。 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"))?; let data = cx.store().payload::(mount, "Mount")?.clone(); if !data.mounted { return Err(invalid("paint Mount is detached")); } let definition = cx .store() .type_of(data.block)? .ok_or(invalid("untyped paint target"))?; let function = cx .store() .lookup_method(definition, "paint")? .ok_or(invalid("missing paint method"))?; // 原生默认 paint 留给 finish 生成;仅执行实际覆盖的源方法,避免重复绘制同一出现。 if crate::native_visual::is_intrinsic(cx.store(), function, "paint", self.desktop)? { return Ok(NativeResult::Return(Value::Bool(true))); } // 把已提交 Mount 几何封装为本轮临时 Bounds;新分配参数可初始化,业务模型仍处于只读域。 let bounds = cx.new_object()?; attach_desktop_type(cx, bounds, "Bounds", self.desktop)?; for (name, value) in [ ("x", data.geometry.x), ("y", data.geometry.y), ("width", data.geometry.width), ("height", data.geometry.height), ] { cx.set_attr(bounds, name, Value::Number((value as f64).into()))?; } // 收集器只捕获桌面根和当前出现 ID;输出是原生共享容器,不增加业务图的强引用。 let collector = cx.allocate(ContextNativeFunction::new( "collect source paint", Collect { desktop: self.desktop, mount: id, output: self.output.clone(), }, vec![self.desktop], ))?; // 辅助帧先执行 paint(block, mount, bounds),收集其结果,再返回 true 推进外层游标。 let function = crate::vm_sequence::collect_and_continue( cx, collector, function, vec![ Value::Object(data.block), Value::Object(mount), Value::Object(bounds), ], "paint occurrence", self.desktop, )?; Ok(NativeResult::Invoke { function, arguments: vec![], }) } } /// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。 pub struct Draft { // 已经合并命令的原生帧;Draft 仍需桥接层提交,完成计算不等于发布成功。 pub frame: Frame, // 保留源绘制的业务读取依赖,供下次写入后判定是否必须重绘。 pub reads: Vec, pub slices: usize, } #[must_use = "pending paint must be resumed or cancelled on its owning VM"] /// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。 pub enum Progress { Pending(Box), Complete(Box), } /// Only unpublished native commands and an owning prepare root cross a pause. /// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。 pub struct Work { desktop: ObjectRef, // 跨切片保留的入口根;完成或取消都必须在所属 VM 中显式释放。 prepare: ObjectRef, output: Output, root: Option>, frame: Frame, slices: usize, } impl Work { /// 继续当前绘制续体,保留其先前收集的命令和所属窗口上下文。 pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result { // 恢复保留原执行预算,slice 只决定本次让出的指令配额,不能重置整个绘制上限。 self.slices += 1; let progress = vm.resume_read_only_call(slice); self.accept(vm, progress) } /// 关闭未完成绘制续体并释放其临时根,不发布半完成命令列表。 pub fn cancel(self, vm: &mut Vm) -> Result<()> { // 先执行取消和 release 两个操作,再依次传播错误;取消失败也仍尝试释放入口根。 let cancelled = vm.cancel_read_only_call(); let released = vm.release(self.prepare); cancelled?; released?; Ok(()) } /// 区分源绘制续体是挂起还是完成,保留工作或结束只读域并生成 Draft。 fn accept( self, vm: &mut Vm, progress: Result, ) -> Result { match progress { // 挂起时工作句柄继续持有 frame/output/prepare,不能发布部分输出。 Ok(endor_core::ExecutionProgress::Pending { .. }) => { Ok(Progress::Pending(Box::new(self))) } Ok(endor_core::ExecutionProgress::Complete { value, dependencies, .. }) => { // VM 完成后先释放入口与驱动返回值,再读取纯原生收集结果;返回值本身不作为绘制命令。 vm.release(self.prepare)?; vm.release_value(value)?; finish(vm, self, dependencies) } Err(error) => { // 趁 VM 诊断信息仍可用先生成错误,再取消只读执行并清理临时根。 let diagnostic = crate::endor_host::runtime_error(vm, error); self.cancel(vm)?; Err(diagnostic) } } } } /// 建立本轮只读绘制驱动和结果收集器;源覆盖与原生默认绘制最终保持同一出现顺序。 pub fn start( vm: &mut Vm, desktop: ObjectRef, mounts: Vec<(u64, WeakObjectRef)>, root: Option>, frame: Frame, slice: usize, ) -> Result { // 禁止零切片和嵌套只读/挂起调用;同一 VM 同时只能由一个续体驱动。 anyhow::ensure!(slice > 0, "paint slice must be positive"); anyhow::ensure!( !vm.has_suspended_call() && !vm.object_store().is_read_only(), "cannot start paint while another read-only execution is active" ); let output = Arc::new(Mutex::new(PaintOutput::default())); let prepare = vm.allocate(ContextNativeFunction::new( "prepare source paint", Prepare { desktop, mounts, output: output.clone(), }, vec![desktop], ))?; let work = Work { desktop, prepare, output, root, frame, slices: 1, }; let result = vm.start_read_only_call( prepare, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, slice, ); work.accept(vm, result) } /// 按 Mount 合并源覆盖和原生命令,补齐投影/裁剪表,再递归装配世界 UI 面板。 fn finish(vm: &mut Vm, mut work: Work, reads: Vec) -> Result { let output = work .output .lock() .map_err(|_| anyhow::anyhow!("paint output poisoned"))?; // 是否有源覆盖以 map 是否为空判断;源方法返回空命令列表也算覆盖,会抑制该出现的默认绘制。 let native_only = output.commands.is_empty(); // 没有根时没有节点投影可合并,直接交回已有帧并清空绘制读取,不展开默认命令。 if work.root.is_none() { return Ok(Progress::Complete(Box::new(Draft { frame: work.frame, reads: Vec::new(), slices: work.slices, }))); } let root = work.root.as_deref(); let desktop = work.desktop; let frame = &mut work.frame; // 建立 Block ID 到不可变节点的索引;最终合并顺序仍以 frame.mounts 为准,不能按 map 排序。 let mut nodes = HashMap::new(); let mut pending: Vec<_> = root.into_iter().collect(); 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(); for mount in &frame.mounts { // 记录本出现追加前的长度,用差值为新增命令同步扩展 clip/view/owner 三张平行表。 let before = commands.len(); // 有源覆盖则使用其整个结果(包括空列表);否则用节点和 Mount 几何生成原生默认命令。 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"))?; commands.extend(layout::paint_commands( node, mount.x, mount.y, Size { width: mount.width, height: mount.height, }, )); } 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)); // 混合原生与源命令后再检查总预算,前面的源输出预算不能覆盖这里新增的默认命令。 if commands.len() > 10_000 { bail!("paint command budget exceeded"); } } frame.commands = commands; frame.command_clips = clips; frame.command_views = views; frame.command_mounts = owners; // 统一命令和投影元数据就绪后,再把世界 UI 面板装配进对应 3D 场景。 crate::scene3d::panel::compose(frame)?; let registry = object(vm.get_attr(desktop, "types")?)?; // 排除宿主桌面、类型注册表、Mount 以及已经死亡的临时身份,只保留业务读取。 // 纯原生帧不需要保留这次驱动的读取记录。 let reads = reads .into_iter() .filter(|dependency| { let object = dependency.object(); !native_only && object != desktop && object != registry && vm .object(object) .is_ok_and(|object| !object.payload().as_any().is::()) }) .collect(); Ok(Progress::Complete(Box::new(Draft { frame: work.frame, reads, slices: work.slices, }))) }