622 lines
30 KiB
Rust
622 lines
30 KiB
Rust
//! 执行源 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},
|
||
};
|
||
|
||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||
// 字段解码、名义类型检查和原生驱动适配器共用此构造,错误回到只读 paint 调用链。
|
||
fn invalid(message: &'static str) -> VmError {
|
||
VmError::InvalidArgument(message)
|
||
}
|
||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||
// named_type、decode 和 Collect 提取对象字段时调用;不把标量转换成临时对象来接受错误输入。
|
||
fn object(value: Value) -> Result<ObjectRef, VmError> {
|
||
// 只有真实托管身份可交给 ObjectStore 查询;具体名义类型由使用这个身份的后续步骤检查。
|
||
if let Value::Object(object) = value {
|
||
Ok(object)
|
||
} else {
|
||
Err(invalid("paint must return managed objects"))
|
||
}
|
||
}
|
||
/// 读取指定命令字段并要求其值为 Text;不隐式字符串化其他类型。
|
||
// decode 读取 kind/color/text 时调用;属性不存在的错误由 get_attr 原样传播。
|
||
fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<String, VmError> {
|
||
if let Value::Text(value) = store.get_attr(object, name)? {
|
||
Ok(value)
|
||
} else {
|
||
Err(invalid("paint field must be text"))
|
||
}
|
||
}
|
||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||
// decode 的几何、字号和换行尺寸都经此入口;name 同时选择坐标可为负的规则。
|
||
fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmError> {
|
||
// 先要求 VM Number,再转换成原生 f32;不接受仅在文本上可解析的字符串。
|
||
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::<f32>()
|
||
.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 的类型注册表取得绘制结果必须符合的托管类型。
|
||
// Collect 和 decode 使用当前桌面的类型注册表,避免用其他窗口的名义类型接受返回值。
|
||
fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<ObjectRef, VmError> {
|
||
// desktop.types 本身必须为托管注册表对象;缺失类型不能静默采用结构匹配。
|
||
let registry = object(store.get_attr(desktop, "types")?)?;
|
||
object(
|
||
store
|
||
.dict_get(registry, &Value::Text(name.into()))?
|
||
.ok_or(invalid("missing paint type"))?,
|
||
)
|
||
}
|
||
/// 把源方法返回的托管绘制值解码成受限 Rust 命令,检查名义类型、字段和绘制预算。
|
||
// Collect 对返回列表逐项调用;成功值已脱离 VM 对象字段,只有图片/场景等不可变资源引用被保留。
|
||
fn decode(
|
||
store: &ObjectStore,
|
||
command: ObjectRef,
|
||
expected: ObjectRef,
|
||
) -> Result<RenderCommand, VmError> {
|
||
// 使用当前 desktop 注册的 PaintCommand 名义类型;字段形状相似的普通对象也不能冒充命令。
|
||
if !store.is_instance(command, expected)? {
|
||
return Err(invalid("paint commands must be PaintCommand instances"));
|
||
}
|
||
// 所有变体先读取本地绘制原点;不会自动加 Mount 原点,源方法已收到布局 Bounds。
|
||
let x = number(store, command, "x")?;
|
||
let y = number(store, command, "y")?;
|
||
// kind 选择受支持的封闭命令集合,未知文本在 match 末尾返回错误。
|
||
let kind = text(store, command, "kind")?;
|
||
// 场景和图片命令持有独立资源,无需读取通用颜色字段。
|
||
let color = if matches!(kind.as_str(), "scene3d" | "image") {
|
||
String::new()
|
||
} else {
|
||
text(store, command, "color")?
|
||
};
|
||
// 逐变体读取必需字段,随后再统一 validate;不能只凭 PaintCommand 名义类型跳过字段检查。
|
||
let result = match kind.as_str() {
|
||
// 图像共享 graphics 模块的资源、尺寸、旋转和透明度解码规则,不在这里重复实现。
|
||
"image" => crate::bridge::graphics::decode_image(store, command)?,
|
||
"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::<crate::scene3d::PaintView>(snapshot, "immutable 3D paint snapshot")?
|
||
.0
|
||
.clone(),
|
||
}
|
||
}
|
||
"roundedRect" | "strokeRoundedRect" => {
|
||
// 圆角填充与描边共享宽高半径;只有描边分支另外要求 strokeWidth。
|
||
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"));
|
||
}
|
||
// 同一组几何字段决定两个不同命令变体;填充不能因为缺少 strokeWidth 而失败。
|
||
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,
|
||
// 两个字段存在后重新按数值规则读取,拒绝负长度或非 Number,而非只判断存在性。
|
||
(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<u64, Vec<RenderCommand>>,
|
||
// 全轮源命令累计数,不能靠拆分多个源 DisplayList 绕过 10000 条总预算。
|
||
count: usize,
|
||
}
|
||
// Prepare、Advance、Collect 和 Work 共享纯原生输出;锁只保护短时记账,不跨源方法调用。
|
||
type Output = Arc<Mutex<PaintOutput>>;
|
||
#[derive(Debug)]
|
||
/// 检查源 paint 返回列表并将命令保存到当前出现的绘制结果。
|
||
struct Collect {
|
||
// 当前驱动所属桌面,用它查 DisplayList/PaintCommand 类型,并由原生函数 roots 保持存活。
|
||
desktop: ObjectRef,
|
||
// 当前出现 ID 是输出键,不是 Block 身份;一个 Block 多次出现允许返回不同画面。
|
||
mount: u64,
|
||
output: Output,
|
||
}
|
||
impl NativeCallable for Collect {
|
||
/// 接收当前出现位置 paint 的唯一 DisplayList 结果。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(1))
|
||
}
|
||
|
||
/// 验证返回名义类型并完整解码所有 PaintCommand,检查全轮命令预算后才把该出现位置结果写入输出表。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 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")?,
|
||
)? {
|
||
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")?;
|
||
// 先取得受控列表中的对象身份,再逐项解码并 collect;任何单项失败都不会提交当前 Mount 的部分输出。
|
||
let commands = object_list::items(cx.store(), commands)?
|
||
.iter()
|
||
.map(|command| decode(cx.store(), *command, expected))
|
||
.collect::<Result<Vec<_>, _>>()?;
|
||
// 仅收集结果时持锁,不在调用源 paint 期间持有结果锁;失败不会插入半个命令列表。
|
||
let mut output = self
|
||
.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))
|
||
}
|
||
}
|
||
#[derive(Debug)]
|
||
/// 为当前出现准备 paint 方法、几何和类型检查上下文。
|
||
struct Prepare {
|
||
desktop: ObjectRef,
|
||
// 按帧绘制顺序保存出现 ID 和弱身份;驱动不会因等待绘制而强持有全部 Mount。
|
||
mounts: Vec<(u64, WeakObjectRef)>,
|
||
output: Output,
|
||
}
|
||
impl NativeCallable for Prepare {
|
||
/// 零参数驱动入口,待绘制 Mount 和输出容器已保存在适配器。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(0))
|
||
}
|
||
|
||
/// 建立共享游标与固定外层字节码循环,让 VM 每轮只准备一个出现位置的 paint 调用。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 构造一个共享游标驱动;每轮只准备当前出现,避免一次保留所有出现的临时实参。
|
||
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],
|
||
))?;
|
||
// 固定 VM 字节码循环反复调用 advance,true 继续、false 结束;所有源方法共享同一次执行预算。
|
||
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,
|
||
// Prepare 移交的固定顺序列表,在 VM 切片之间保持不变;游标只向前推进。
|
||
mounts: Arc<[(u64, WeakObjectRef)]>,
|
||
// 下一个待准备出现的下标;不是已输出命令数,跳过原生 paint 的出现也占一项。
|
||
next: Mutex<usize>,
|
||
output: Output,
|
||
}
|
||
impl NativeCallable for Advance {
|
||
/// 由外层循环无参调用,下一出现位置由内部游标决定。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(0))
|
||
}
|
||
/// 短锁内取并推进游标,随后验证/准备当前 Mount 的方法与几何;用临时调用链收集结果,完成后才进入下一位置。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 锁只保护取游标与递增;离开此块后释放锁,再做 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"))?;
|
||
// 克隆原生 Mount 描述后再分配临时 Bounds,避免持有跨 VM 可变操作的 store 借用。
|
||
let data = cx.store().payload::<Mount>(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")?
|
||
.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)?;
|
||
// 按布局绝对坐标填充 x/y/width/height;这些临时属性可初始化,不表示源方法能修改已有业务模型。
|
||
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,
|
||
)?;
|
||
// 交回 VM 执行这个零参数辅助函数,源方法及收集器仍由 VM 调用栈驱动,不在 Rust 中递归解释。
|
||
Ok(NativeResult::Invoke {
|
||
function,
|
||
arguments: vec![],
|
||
})
|
||
}
|
||
}
|
||
|
||
/// 源绘制完成后的未发布结果:已装配帧、业务读取依赖和调度切片数。
|
||
pub struct Draft {
|
||
// 已经合并命令的原生帧;Draft 仍需桥接层提交,完成计算不等于发布成功。
|
||
pub frame: Frame,
|
||
// 保留源绘制的业务读取依赖,供下次写入后判定是否必须重绘。
|
||
pub reads: Vec<endor_core::ReadDependency>,
|
||
// 包括 start 的第一片和每次 resume;供桥接层记录实际绘制切片数。
|
||
pub slices: usize,
|
||
}
|
||
|
||
#[must_use = "pending paint must be resumed or cancelled on its owning VM"]
|
||
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
|
||
pub enum Progress {
|
||
// 运行时暂存此句柄,后续在原 VM 继续或取消,不能当作可提交帧。
|
||
Pending(Box<Work>),
|
||
// 只读调用结束且命令装配完成;UI bridge 仍需更新依赖,endor_host 再发布文档。
|
||
Complete(Box<Draft>),
|
||
}
|
||
|
||
/// 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<Arc<UiNode>>,
|
||
// 尚未发布的布局帧,装配命令只修改这个工作副本。
|
||
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<Progress> {
|
||
// 恢复保留原执行预算,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();
|
||
let released = vm.release(self.prepare);
|
||
cancelled?;
|
||
released?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 区分源绘制续体是挂起还是完成,保留工作或结束只读域并生成 Draft。
|
||
fn accept(
|
||
self,
|
||
vm: &mut Vm,
|
||
progress: Result<endor_core::ExecutionProgress, VmError>,
|
||
) -> Result<Progress> {
|
||
// 三种结果分别保留 Work、生成 Draft 或清理后返回诊断;只有 Complete 才进入 finish。
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 建立本轮只读绘制驱动和结果收集器;源覆盖与原生默认绘制最终保持同一出现顺序。
|
||
// ui_bridge::begin_paint_frame 在布局完成后调用;mounts 顺序来自 frame,root/frame 均是未发布的本轮草稿。
|
||
pub fn start(
|
||
vm: &mut Vm,
|
||
desktop: ObjectRef,
|
||
mounts: Vec<(u64, WeakObjectRef)>,
|
||
root: Option<Arc<UiNode>>,
|
||
frame: Frame,
|
||
slice: usize,
|
||
) -> Result<Progress> {
|
||
// 禁止零切片和嵌套只读/挂起调用;同一 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"
|
||
);
|
||
// 为整轮创建一个结果容器,跨所有 Mount 和所有挂起切片共享。
|
||
let output = Arc::new(Mutex::new(PaintOutput::default()));
|
||
// 分配有根的驱动入口;显式捕获 desktop,Work 负责完成或取消后释放入口根。
|
||
let prepare = vm.allocate(ContextNativeFunction::new(
|
||
"prepare source paint",
|
||
Prepare {
|
||
desktop,
|
||
mounts,
|
||
output: output.clone(),
|
||
},
|
||
vec![desktop],
|
||
))?;
|
||
// 在进入 VM 前绑定输入草稿、输出和入口根;即使第一片马上完成也走同一个 Work::accept。
|
||
let work = Work {
|
||
desktop,
|
||
prepare,
|
||
output,
|
||
root,
|
||
frame,
|
||
slices: 1,
|
||
};
|
||
// 限制总帧深与总指令数,slice 只切分调度时间;源方法不能借多次 resume 重置总预算。
|
||
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 面板。
|
||
// 仅由 Work::accept 在 VM Complete 后调用,此时 prepare 已释放;输出容器中只有成功解码的完整覆盖。
|
||
fn finish(vm: &mut Vm, mut work: Work, reads: Vec<endor_core::ReadDependency>) -> Result<Progress> {
|
||
// 加锁读取整轮收集结果;finish 不再调用源 paint,锁不会跨越新的源执行。
|
||
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,
|
||
})));
|
||
}
|
||
// 根已在上面检查存在;借用 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();
|
||
// 有源覆盖则使用其整个结果(包括空列表);否则用节点和 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,
|
||
},
|
||
));
|
||
}
|
||
// 每条新增命令继承当前出现的裁剪、变换和 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));
|
||
// 混合原生与源命令后再检查总预算,前面的源输出预算不能覆盖这里新增的默认命令。
|
||
if commands.len() > 10_000 {
|
||
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
|
||
&& object != registry
|
||
&& vm
|
||
.object(object)
|
||
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
|
||
})
|
||
.collect();
|
||
// 返回完整但未发布的草稿;桥接层接着记录依赖和切片数,运行时再完成帧提交。
|
||
Ok(Progress::Complete(Box::new(Draft {
|
||
frame: work.frame,
|
||
reads,
|
||
slices: work.slices,
|
||
})))
|
||
}
|