517 lines
18 KiB
Rust
517 lines
18 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},
|
|
};
|
|
|
|
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
|
fn invalid(message: &'static str) -> VmError {
|
|
VmError::InvalidArgument(message)
|
|
}
|
|
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
|
fn object(value: Value) -> Result<ObjectRef, VmError> {
|
|
if let Value::Object(object) = value {
|
|
Ok(object)
|
|
} else {
|
|
Err(invalid("paint must return managed objects"))
|
|
}
|
|
}
|
|
/// 把当前源值或文本选择解码成此调用需要的文本表示。
|
|
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 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
|
fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmError> {
|
|
let Value::Number(value) = store.get_attr(object, name)? else {
|
|
return Err(invalid("paint field must be a Number"));
|
|
};
|
|
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 的类型注册表取得绘制结果必须符合的托管类型。
|
|
fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<ObjectRef, VmError> {
|
|
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<RenderCommand, VmError> {
|
|
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")?;
|
|
let color = if kind == "scene3d" {
|
|
String::new()
|
|
} else {
|
|
text(store, command, "color")?
|
|
};
|
|
let result = match kind.as_str() {
|
|
"scene3d" => {
|
|
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" => {
|
|
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")?;
|
|
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 {
|
|
commands: BTreeMap<u64, Vec<RenderCommand>>,
|
|
count: usize,
|
|
}
|
|
type Output = Arc<Mutex<PaintOutput>>;
|
|
#[derive(Debug)]
|
|
/// 检查源 paint 返回列表并将命令保存到当前出现的绘制结果。
|
|
struct Collect {
|
|
desktop: ObjectRef,
|
|
mount: u64,
|
|
output: Output,
|
|
}
|
|
impl NativeCallable for Collect {
|
|
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(1))
|
|
}
|
|
|
|
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
|
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"));
|
|
}
|
|
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::<Result<Vec<_>, _>>()?;
|
|
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 {
|
|
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(0))
|
|
}
|
|
|
|
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
|
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],
|
|
))?;
|
|
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<usize>,
|
|
output: Output,
|
|
}
|
|
impl NativeCallable for Advance {
|
|
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(0))
|
|
}
|
|
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
|
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
|
|
};
|
|
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, "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"))?;
|
|
if crate::native_visual::is_intrinsic(cx.store(), function, "paint", self.desktop)? {
|
|
return Ok(NativeResult::Return(Value::Bool(true)));
|
|
}
|
|
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()))?;
|
|
}
|
|
let collector = cx.allocate(ContextNativeFunction::new(
|
|
"collect source paint",
|
|
Collect {
|
|
desktop: self.desktop,
|
|
mount: id,
|
|
output: self.output.clone(),
|
|
},
|
|
vec![self.desktop],
|
|
))?;
|
|
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 {
|
|
pub frame: Frame,
|
|
pub reads: Vec<endor_core::ReadDependency>,
|
|
pub slices: usize,
|
|
}
|
|
|
|
#[must_use = "pending paint must be resumed or cancelled on its owning VM"]
|
|
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
|
|
pub enum Progress {
|
|
Pending(Box<Work>),
|
|
Complete(Box<Draft>),
|
|
}
|
|
|
|
/// Only unpublished native commands and an owning prepare root cross a pause.
|
|
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
|
|
pub struct Work {
|
|
desktop: ObjectRef,
|
|
prepare: ObjectRef,
|
|
output: Output,
|
|
root: Option<Arc<UiNode>>,
|
|
frame: Frame,
|
|
slices: usize,
|
|
}
|
|
|
|
impl Work {
|
|
/// 继续当前绘制续体,保留其先前收集的命令和所属窗口上下文。
|
|
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
|
|
self.slices += 1;
|
|
let progress = vm.resume_read_only_call(slice);
|
|
self.accept(vm, progress)
|
|
}
|
|
|
|
/// 关闭未完成绘制续体并释放其临时根,不发布半完成命令列表。
|
|
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
|
|
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> {
|
|
match progress {
|
|
Ok(endor_core::ExecutionProgress::Pending { .. }) => {
|
|
Ok(Progress::Pending(Box::new(self)))
|
|
}
|
|
Ok(endor_core::ExecutionProgress::Complete {
|
|
value,
|
|
dependencies,
|
|
..
|
|
}) => {
|
|
vm.release(self.prepare)?;
|
|
vm.release_value(value)?;
|
|
finish(vm, self, dependencies)
|
|
}
|
|
Err(error) => {
|
|
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<Arc<UiNode>>,
|
|
frame: Frame,
|
|
slice: usize,
|
|
) -> Result<Progress> {
|
|
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<endor_core::ReadDependency>) -> Result<Progress> {
|
|
let output = work
|
|
.output
|
|
.lock()
|
|
.map_err(|_| anyhow::anyhow!("paint output poisoned"))?;
|
|
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;
|
|
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 {
|
|
let before = commands.len();
|
|
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;
|
|
crate::scene3d::panel::compose(frame)?;
|
|
let registry = object(vm.get_attr(desktop, "types")?)?;
|
|
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::<Mount>())
|
|
})
|
|
.collect();
|
|
Ok(Progress::Complete(Box::new(Draft {
|
|
frame: work.frame,
|
|
reads,
|
|
slices: work.slices,
|
|
})))
|
|
}
|