Files
endorphin-tessera-gui/src/model/snapshot.rs
T

574 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 从可变托管对象图提取不可变 UiNode,并维护反向依赖与父子失效传播。
//! 缓存只借用可检查的对象身份,不增加托管根。对象释放后必须清理缓存索引;快照仍可持有独立的原生数值数据。
//! 干净子树直接复用 Arc;脏节点重新检查属性、方法、绑定和子项,循环与展开数量在发布前拒绝。
//!
//! 调用路径:UiBridge 的 prepare/刷新入口消费 VM ChangeBatch,调用 invalidate/prune 后按页面根 snapshot。
//! block::snapshot 也可用临时空缓存一次性提取;持久窗口缓存只保留原生 UiNode Arc,不 retain 托管身份。
//! 协议方法/属性改变会使业务快照失效,但相机、布局和绘制各自还有后续阶段的缓存边界。
//!
//! Object-to-visual dependencies. Cached results never own VM references.
use crate::{
block::{self, Kind},
ui_bridge::{UiKind, UiNode},
};
use endor_core::{ChangeBatch, ChangeKey, ObjectRef, ObjectStore, Value, VmError};
use std::{
collections::{HashMap, HashSet, VecDeque},
sync::Arc,
};
#[derive(Debug)]
/// 某个业务组件的视觉快照,以及使它失效的读取依赖和子项身份。
struct Cached {
// 已提取的原生不可变视觉树,窗口/布局可复用 Arc,不能由其中的身份文本操作 VM。
node: Arc<UiNode>,
// 重建时实际登记的反向读取键,remove 必须逐一撤销,避免旧读取持续误失效。
dependencies: Vec<Dependency>,
// 当前组成孩子的借用身份,仅维护父链反向索引,不形成新的托管强边。
children: Vec<ObjectRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// 反向依赖键:区分整个对象内容和精确属性名称,减少无关字段变化引起的失效。
struct Dependency {
// 读取源的借用身份,对象可能已被回收,使用前需由 prune/store 查询处理失效。
object: ObjectRef,
// Some 表示精确字段;None 表示对象内容/方法表等整体依赖,与 ChangeKey 粒度对应。
property: Option<String>,
}
#[derive(Debug, Default)]
/// 视觉快照缓存与反向依赖图。对象身份是借用索引,原生 Arc 数据不拥有 VM 根。
pub struct RenderCache {
// 场景对象身份到原生不可变几何的缓存;相机改变时仍有机会复用同一场景数据。
scenes3d: HashMap<ObjectRef, Arc<crate::scene3d::Scene>>,
// 每个业务 Block 的视觉结果;这里只保存原生快照,不通过 ObjectRef 增加 VM 强引用。
entries: HashMap<ObjectRef, Cached>,
// 读取源 -> 读取它的 Block。属性变动从这里找到首批需要重建的快照。
dependents: HashMap<Dependency, HashSet<ObjectRef>>,
// 子 Block -> 所有父 Block。共享子组件改变后,所有引用该子项的父快照都需要更新。
parents: HashMap<ObjectRef, HashSet<ObjectRef>>,
// 已失效但尚未重建的 Block 集合,同时用于传播去重。
dirty: HashSet<ObjectRef>,
// 最近一次失效原因诊断,供开发工具/统计展示,不决定缓存正确性。
pub last_invalidation: String,
// 本次 snapshot 实际重建的进入次数,按调用重置并用于展开预算。
pub visited: usize,
// 本次直接命中干净缓存的节点数,不等于整个复用子树的展开节点数。
pub reused: usize,
}
impl RenderCache {
/// 把 VM 变化映射到读取过这些属性/对象的视觉节点,再沿父链向上传播脏标记。
pub fn invalidate(&mut self, batch: ChangeBatch) {
// 收集首批受变化影响的业务 Block,随后统一交给父链失效传播。
let mut queue = VecDeque::new();
// 变更日志溢出后无法知道遗漏了哪些写入,保守地将已有快照全部失效。
if batch.rescan_required {
self.last_invalidation = "change journal overflow".into();
queue.extend(self.entries.keys().copied());
} else {
// 非溢出批次逐项按读取粒度匹配,不把无关字段变化扩大为整图失效。
for change in batch.changes {
// 属性变更使用精确名称,Contents 使用整个对象键;与提取时登记的依赖粒度一致。
// 变化键与提取时记录的 Dependency 完全同构,保证属性名与 Contents 不混用。
let key = Dependency {
object: change.object,
property: match change.key {
// 字段写入只匹配该名字的精确读取者。
ChangeKey::Attribute(name) => Some(name),
// 对象 payload/方法内容变化匹配整个对象的依赖。
ChangeKey::Contents => None,
},
};
// 同一个源可被多个业务快照读取,全部加入队列;去重与父链传播在下一入口处理。
if let Some(owners) = self.dependents.get(&key) {
self.last_invalidation = format!("{} {:?}", key.object, key.property);
queue.extend(owners);
}
}
}
self.invalidate_objects(queue);
}
/// 迭代传播显式失效,使用集合避免共享父链重复遍历或循环入队。
pub fn invalidate_objects(&mut self, objects: impl IntoIterator<Item = ObjectRef>) {
// 显式失效入口也复用同一队列算法,调用方可传多个共享节点。
let mut queue: VecDeque<_> = objects.into_iter().collect();
// 广度推进向上的父链,不递归进入 Rust 栈;dirty 同时作为传播去重集合。
while let Some(block) = queue.pop_front() {
// 只有第一次加入 dirty 才继续向父链传播,避免共享祖先被反复展开。
if self.dirty.insert(block)
&& let Some(parents) = self.parents.get(&block)
{
queue.extend(parents);
}
}
}
/// 撤销旧缓存的反向读取边和父子边,为重新登记该节点做准备。
fn remove(&mut self, block: ObjectRef) -> Option<Cached> {
// 先移除本体,若根本没缓存则无需撤边;返回旧 Arc 供调用方比较是否可复用。
let old = self.entries.remove(&block)?;
// 旧边必须先撤销,否则节点重建后仍会被已经不再读取的对象触发失效。
// 撤销旧快照的每个读取反向边,新建依赖可能已不再读取这些对象/属性。
for source in &old.dependencies {
// 只移除当前 Block,不能让其他仍读取该源的快照失去失效通知。
if let Some(owners) = self.dependents.get_mut(source) {
owners.remove(&block);
// 没有读取者时清空索引项,避免死亡/过时依赖键持续积累。
if owners.is_empty() {
self.dependents.remove(source);
}
}
}
// 只撤销当前 Block 对孩子的父边;其他共享父节点仍保留。空集合及时删除以缩小索引。
// 撤销当前父对孩子的索引边,共享孩子仍可能保留其他父。
for child in &old.children {
// 找到孩子所有父集合,仅删除本 Block 的组成关系。
if let Some(parents) = self.parents.get_mut(child) {
parents.remove(&block);
// 孩子已无缓存父时删除反向项,避免无用身份长期占缓存内存。
if parents.is_empty() {
self.parents.remove(child);
}
}
}
Some(old)
}
/// 删除托管堆中已不存在的身份,缓存本身不负责保持业务对象存活。
pub fn prune(&mut self, store: &ObjectStore) {
// 桥接回收/准备入口调用 prune,先丢弃托管场景根已经不存在的原生场景缓存。
self.scenes3d
.retain(|object, _| store.object(*object).is_ok());
// 先收集死亡身份,再修改 entries 与反向索引,避免一边借用遍历一边删除。
let dead: Vec<_> = self
.entries
.keys()
.filter(|v| store.object(**v).is_err())
.copied()
.collect();
// 每个死亡 Block 都需 remove 撤销依赖和父边,不只删除 entries 本体。
for block in dead {
self.remove(block);
// 此处身份已死亡,不会再重建;清除其遗留脏标记,避免弱索引变成长期墓碑。
self.dirty.remove(&block);
}
}
/// 按显式栈遍历托管组成;复用干净子树,重建脏节点,并在完成前检查循环和资源上限。
pub fn snapshot(
&mut self,
store: &ObjectStore,
root: ObjectRef,
) -> Result<Arc<UiNode>, VmError> {
// 按本次提取重置工作统计,缓存内容与 dirty 则跨调用保留。
self.visited = 0;
self.reused = 0;
// 使用进入/退出两阶段的显式 DFS,避免深层组成消耗 Rust 调用栈。
// values 是后序结果栈:每个退出节点消费刚完成的孩子快照,再压入自己。
// 每项为业务 Block 与后序完成标记;false 先展开孩子,true 再消费孩子结果组装父。
let mut work = vec![(root, false)];
// 当前祖先 Data 集合防止组合环,不是整棵图的全局去重,合法共享孩子仍可多次出现。
let mut path = HashSet::new();
// 原生子树后序结果栈,保留兄弟已有结果,父完成时只取尾部本节点孩子。
let mut values = Vec::<Arc<UiNode>>::new();
// 显式 DFS 驱动全部提取,发生错误就拒绝这次快照,不返回半棵树。
while let Some((block, finish)) = work.pop() {
// 入栈时先尝试复用;退出标记表示孩子已形成快照,可以组装父节点。
if !finish {
// 干净且存在条目才能复用;仅不在 dirty 中并不能证明已缓存。
if !self.dirty.contains(&block)
&& let Some(cached) = self.entries.get(&block)
{
self.reused += 1;
values.push(cached.node.clone());
continue;
}
// 计数的是本次实际展开次数;共享节点可复用缓存,脏图展开仍有 10000 次上限。
self.visited += 1;
// 超过实际重建展开预算明确报错,不能按固定节点数截断显示。
if self.visited > 10_000 {
return Err(VmError::ExecutionLimit("expanded Block graph"));
}
}
// 提取前验证实例存储与类型契约;resolve_tracked 同时提供数据以及经过的代理/存储依赖。
store.check_typed_instance_storage(block)?;
// 取得共享转发后的真实 Data 及 Block/Cell 路径,让替换身份和中间单元变化也触发重建。
let (data_ref, raw_data, raw_dependencies) = block::resolve_tracked(store, block)?;
// 托管属性只覆盖本轮副本,提取不改业务对象;后面的三维面板展开也只修改此副本。
let mut data = block::visual_data(store, raw_data)?;
// 把转发链各对象登记为 Contents 依赖,随后再添加精确字段和协议方法键。
let mut dependencies: Vec<_> = raw_dependencies
.into_iter()
.map(|object| Dependency {
object,
property: None,
})
.collect();
// 方法名称查找也构成依赖。即使属性数值未变,协议方法被替换也必须让快照失效。
// 方法可被实例存储阴影,记录末端属性存储中所有受契约约束的方法名。
let storage = *store
.attribute_path(block)?
.last()
.expect("nonempty attribute path");
// 类型方法名也需对实例字段写入失效,以便重建时报告非法阴影而非复用旧合法快照。
for name in store.typed_method_names(block)? {
dependencies.push(Dependency {
object: storage,
property: Some(name),
});
}
// 原生视觉属性按精确字段登记,避免不参与视觉的其他属性写入触发整个节点重建。
// 原生字段存储独立于 Data 结构 payload,按每个公开视觉字段建立精确依赖。
if let Some(properties) = data.properties {
// 只登记该原生 kind 实际解释的字段,不遍历所有业务自定义属性。
for (name, _) in block::native_properties(&data) {
dependencies.push(Dependency {
object: properties,
property: Some(name.into()),
});
}
}
// 二维相机字段进入视口快照依赖,只有 camera 本身的业务变化影响对应视口。
if matches!(data.kind, Kind::CanvasViewport { .. })
&& let Some(camera) = data.camera
{
// 记录三项相机参数的精确读依赖,源控制器等无关字段不使二维视图重建。
for name in ["x", "y", "zoom"] {
dependencies.push(Dependency {
object: camera,
property: Some(name.into()),
});
}
}
// 3D 面板把托管内容映射为子 Block,同时保留尺寸与稳定 slot,供后续布局和出现身份使用。
// 三维提取返回的每块面板 UI 分辨率,顺序必须和替换后的 children 相同。
let mut panel_sizes = Vec::new();
// 只有 Viewport3D 提取三维 View;普通种类不能凭同名 scene 字段触发场景遍历。
let view3d = if matches!(data.kind, Kind::Viewport3D { .. }) {
// 三维视口必须有 Scene3D 业务根;缺失即拒绝本次快照。
let scene = data
.scene3d
.ok_or(VmError::InvalidArgument("missing Scene3D"))?;
// 传入同场景旧原生 Arc 以便只改相机时复用场景;同时收集真实字段读取及面板组成。
let (view, reads, panels) = crate::scene3d_model::snapshot_reusing(
store,
scene,
data.camera
.ok_or(VmError::InvalidArgument("missing 3D camera"))?,
self.scenes3d.get(&scene).cloned(),
)?;
self.scenes3d.insert(scene, view.scene.clone());
dependencies.extend(reads.into_iter().map(|(object, name)| Dependency {
object,
property: Some(name),
}));
data.children = panels
.into_iter()
// 每块面板内容作为 UI 孩子,panel.key 成为稳定 slot;另并行保存 UI 尺寸供布局使用。
.map(|panel| {
panel_sizes.push(panel.size);
crate::block::Child {
slot: panel.key,
block: panel.content,
}
})
.collect();
Some(view)
} else {
None
};
// path 只追踪当前祖先链,因此允许同一业务组件在不同分支多次出现。
// 首次进入登记祖先与展开孩子;finish=true 表示孩子结果已齐,下一步可以组装本节点。
if !finish {
// 同一 Data 在当前祖先链再次出现才是环;与兄弟分支共享区分。
if !path.insert(data_ref) {
return Err(VmError::InvalidArgument("cyclic Block composition"));
}
work.push((block, true));
// 逆序压栈使孩子实际完成顺序仍与组成顺序一致,退出标记排在全部孩子之后。
for child in data.children.iter().rev() {
work.push((child.block, false));
}
continue;
}
path.remove(&data_ref);
// 只有仍有 handler 且 kind 以 pointer 开头的连接提供 pointer_events,断开令牌不再算交互需求。
let pointer_connection = data.subscriptions.iter().any(|&o| {
store
.payload::<crate::event_dispatch::Connection>(o, "Subscription")
.is_ok_and(|s| s.handler.is_some() && s.kind.starts_with("pointer"))
});
// 协议键包含类型身份、实际方法身份及其内容版本,供布局/绘制缓存识别实现变化。
// 协议指纹供后续布局/绘制缓存比较;不能仅凭业务 Block ID 认定实现没变。
let mut protocol_key = String::new();
// 无类型或无 onEvent 时默认没有自定义事件能力,随后按名义方法识别。
let mut custom_event = false;
// 名义类型与其方法表决定实际协议,不从任意同名实例值推断视觉实现。
if let Some(definition) = store.type_of(block)? {
protocol_key.push_str(&definition.to_string());
// 五种视觉/事件协议方法都记录句柄及内容版本,payload 保持句柄不变也会改变指纹。
for name in ["measure", "arrange", "paint", "hitTest", "onEvent"] {
// 仅已解析到的实际方法加入依赖;类型链另登记,负责将来新增/替换方法的失效。
if let Some(method) = store.lookup_method(definition, name)? {
// 使用对象内容版本识别同函数身份下的实现替换,不只把方法名拼入字符串。
let version = store.change_version(method)?;
protocol_key.push_str(&format!("/{method}:{version}"));
dependencies.push(Dependency {
object: method,
property: None,
});
}
}
// 不仅记录最终方法,还沿基类链登记类型内容依赖,使继承关系或方法表变化能触发重建。
// 保存当前解析到的 onEvent,再遍历基类链登记方法表依赖。
let event = store.lookup_method(definition, "onEvent")?;
// 从动态类型向基类推进,每层都属于方法查找的读取来源。
let mut current = Some(definition);
// 继承关系/任一基类表变化都能影响查找结果,不能只观察最终方法函数。
while let Some(definition) = current {
dependencies.push(Dependency {
object: definition,
property: None,
});
// 名义类型 payload 提供 base 链,错误类型不能被当成链尾默默接受。
let item = store.payload::<endor_core::EndorTypeObject>(definition, "Type")?;
current = item.base();
}
custom_event = event
.map(|function| {
crate::ui_bridge::is_intrinsic_event(store, function).map(|native| !native)
})
.transpose()?
.unwrap_or(false);
}
// 订阅连接的取消会改变 handler;记录连接内容依赖,使交互资格能随取消及时更新。
// 另累计 click 监听资格,与 pointer 系列、内建 Button 和源 onEvent 合并。
let mut click_connection = false;
// 所有事件连接的 handler 取消都会写 payload,Contents 依赖让交互标记及时重算。
for connection in &data.subscriptions {
dependencies.push(Dependency {
object: *connection,
property: None,
});
// 从真实 Connection payload 判断事件种类与有效 handler,不能凭连接句柄存在就当作活跃。
let item = store
.payload::<crate::event_dispatch::Connection>(*connection, "Connection")?;
click_connection |= item.kind == "click" && item.handler.is_some();
}
// 逆序压入孩子确保后序结果与 data.children 原顺序一致;只取尾部的本节点孩子,保留兄弟结果。
// 每个孩子在后序栈恰有一个结果;按数量拆尾部,其他兄弟结果留给祖先。
let children = values.split_off(values.len() - data.children.len());
// 将业务描述转为不依赖 VM 存活的原生 UiKind,孩子 Arc 按种类移入一次。
let kind = match &data.kind {
Kind::Text { value, size, color } => {
// 旧 textOf 路径按实际源字段提取字符串。属性路径中间对象用整对象依赖,末端用字段依赖。
// text_binding 兼容路径动态读取其源值,普通文字则采用已解码的 text 字段副本。
let value = if let Some((source, field)) = &data.text_binding {
// 源属性可能经过共享转发链,各中间身份变化都可能改变最后读取的值。
let storage = store.attribute_path(*source)?;
// 中间节点登记 Contents,只有末端属性存储用精确字段名,兼顾转发变更与精确失效。
for (index, object) in storage.iter().enumerate() {
dependencies.push(Dependency {
object: *object,
property: (index + 1 == storage.len()).then(|| field.clone()),
});
}
// 按源 Value 类型转换显示文本,对象 str 还需登记该对象内容依赖。
match store.get_attr(*source, field)? {
Value::Nil => "nil".into(),
Value::Bool(v) => v.to_string(),
Value::Number(v) => v.to_string(),
Value::Text(v) => v,
// 对象字符串化可能依赖其内容,所以除属性本身外还记录被引用对象的内容变化。
Value::Object(v) => {
dependencies.push(Dependency {
object: v,
property: None,
});
store.str(v)?
}
}
} else {
value.clone()
};
UiKind::Text {
value,
size: *size,
color: color.clone(),
}
}
// 矩形只复制自然视觉数据,实际出现宽高由后续布局决定。
Kind::Rect {
width,
height,
radius,
color,
} => UiKind::Rect {
width: *width,
height: *height,
radius: *radius,
color: color.clone(),
},
// 滚动视口快照保存尺寸/轴与原生孩子,不保存每个 Mount 的偏移。
Kind::Scroll { width, height } => UiKind::Scroll {
axis: data.scroll_axis,
width: *width,
height: *height,
padding: data.padding,
gap: data.gap,
children,
},
// 间距叶节点无需组成快照,保留自然尺寸供布局使用。
Kind::Spacer(size) => UiKind::Spacer(*size),
// 三维 View 和面板尺寸与内容孩子一同发布,缺失 View 必须报错。
Kind::Viewport3D { width, height } => UiKind::Viewport3D {
width: *width,
height: *height,
view: view3d.ok_or(VmError::InvalidArgument("missing 3D view"))?,
controls: data.controller.is_some(),
panels: panel_sizes,
children,
},
// 二维有限视口保存已解码 Camera2D 值与 Scene2D 孩子,后续投影不再查询 VM。
Kind::CanvasViewport { width, height } => UiKind::CanvasViewport {
width: *width,
height: *height,
camera: crate::viewport::camera(
store,
data.camera
.ok_or(VmError::InvalidArgument("missing camera"))?,
)?,
children,
},
// 二维场景保留每个孩子的世界位置;普通行列容器则由布局决定孩子坐标。
Kind::Scene2D => UiKind::Scene2D {
positions: data
.children
.iter()
.map(|child| {
// Scene2D 的每个孩子可能是别名,先解析当前 Data 再读取经过属性覆盖的位置。
let (_, child) = block::resolve(store, child.block)?;
Ok(block::visual_data(store, child)?.position)
})
.collect::<Result<Vec<_>, VmError>>()?,
children,
},
// Overlay 各孩子叠放在相同内容区域,不携带 Row/Column 线性 gap 策略。
Kind::Overlay => UiKind::Overlay {
padding: data.padding,
children,
},
// 普通 Row 保留 padding/gap 与有序孩子,位置由布局计算。
Kind::Row => UiKind::Row {
padding: data.padding,
gap: data.gap,
children,
},
// 兼容 canvas Page 与 Scene2D 一样提取业务位置,表面 pan/zoom 另在 UiNode 上保存。
Kind::Page if data.surface.mode == crate::page::Mode::Canvas => UiKind::Canvas {
positions: data
.children
.iter()
.map(|child| {
// 旧 Page canvas 同样取每个出现对应业务子的当前 x/y,不把父排列结果当作世界位置。
let (_, data) = block::resolve(store, child.block)?;
Ok(block::visual_data(store, data)?.position)
})
.collect::<Result<Vec<_>, VmError>>()?,
children,
},
// 旧横向页面模式复用 Row 内容布局,页面表面元信息仍独立保留。
Kind::Page if data.surface.mode == crate::page::Mode::Horizontal => UiKind::Row {
padding: data.padding,
gap: data.gap,
children,
},
// Column 和其余旧页面模式使用纵向内容排列。
Kind::Column | Kind::Page => UiKind::Column {
padding: data.padding,
gap: data.gap,
children,
},
// 按钮作为带默认视觉/事件语义的容器保留孩子与内边距。
Kind::Button => UiKind::Button {
padding: data.padding,
gap: data.gap,
children,
},
};
// 统一外层样式与交互标志。UiKind 只表示具体内容,UiNode 保存所有类别共享的视觉元信息。
let next = UiNode {
surface: matches!(data.kind, Kind::Page).then(|| {
// 仅 Page 附带表面快照,补入公开页面身份文本而非持有 VM 引用。
let mut surface = data.surface.clone();
surface.page = block.to_string();
surface
}),
flex: data.flex,
flex_policy: data.flex_policy,
main_align: data.main_align,
cross_align: data.cross_align,
align_self: data.align_self,
margin: data.margin,
sizing: data.sizing,
padding_edges: data.padding_edges,
border: data.border,
border_color: data.border_color.clone(),
clip_children: data.clip_children,
block_id: block.to_string(),
protocol_key,
slots: data.children.iter().map(|c| c.slot).collect(),
pointer_events: pointer_connection,
interactive: pointer_connection
|| matches!(&data.kind, Kind::Button | Kind::Viewport3D { .. })
|| data.click.is_some()
|| click_connection
|| custom_event,
kind,
};
// 即使本次重算结果相同,也要重新登记依赖。shallow_eq 相同时复用旧 Arc,减少下游无效重建。
// 撤销旧依赖/父边,即使浅层视觉结果相等也必须按本轮读取重新登记。
let old = self.remove(block);
// 结果浅相等才复用旧 Arc,减少下游无效工作;变化结果建立新不可变节点。
let node = if let Some(old) = old.as_ref().filter(|old| old.node.shallow_eq(&next)) {
old.node.clone()
} else {
Arc::new(next)
};
// 同一属性可能由不同提取路径重复读取,排序去重后再建立反向边。
dependencies.sort_unstable();
dependencies.dedup();
// 保留孩子业务身份用于父链反向索引,与 UiKind 内的原生 Arc 拥有关系分开。
let child_refs: Vec<_> = data.children.iter().map(|child| child.block).collect();
// 为每个去重读取键登记当前 Block,供下个 VM ChangeBatch 找到首批失效者。
for source in &dependencies {
self.dependents
.entry(source.clone())
.or_default()
.insert(block);
}
// 每个共享孩子可有多个父,子项失效时需要沿所有父边传播脏标记。
for child in &child_refs {
self.parents.entry(*child).or_default().insert(block);
}
// 节点和全部反向边建立后才安装新条目,最后清 dirty 并把结果交回后序栈。
self.entries.insert(
block,
Cached {
node: node.clone(),
dependencies,
children: child_refs,
},
);
self.dirty.remove(&block);
values.push(node);
}
values
.pop()
.ok_or(VmError::InvalidArgument("empty Block snapshot"))
}
}