docs: explain source layout caching and convergence lifecycle
This commit is contained in:
+126
-3
@@ -24,13 +24,16 @@ use std::{
|
||||
|
||||
// Small native trees keep the allocation-free fast path. Larger trees use the
|
||||
// same per-occurrence VM driver so even intrinsic work can yield between nodes.
|
||||
// start 扫描真实协议与 flex 分配后使用此阈值;小原生树可直接求整帧,大树仍需逐出现驱动以便让出。
|
||||
const NATIVE_FAST_PATH_MAX_NODES: usize = 64;
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
// 协议解码、缓存跟踪和驱动适配器共用参数错误构造;VM 错误最终由 Work::accept 转为宿主诊断。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 读取当前 desktop 的类型字典,为布局协议结果提供名义类型检查。
|
||||
/// 读取当前 desktop 的类型字典身份,供测量缓存过滤宿主内部依赖。
|
||||
// Collect 结束测量读取跟踪后调用,用来排除宿主类型注册表依赖,而非将其当业务读取缓存。
|
||||
fn desktop_type_registry(cx: &NativeContext<'_>, desktop: ObjectRef) -> Result<ObjectRef, VmError> {
|
||||
match cx.store().get_attr(desktop, "types")? {
|
||||
Value::Object(object) => Ok(object),
|
||||
@@ -43,22 +46,30 @@ struct Measurement {
|
||||
// 缓存记录借用的业务身份及方法身份,与子测量、约束和依赖版本共同决定复用资格,不增加托管根。
|
||||
owner: ObjectRef,
|
||||
method: ObjectRef,
|
||||
// 缓存复用条件包含所有子出现的测量尺寸;子尺寸变化即使未写当前 Block 属性也会导致不匹配。
|
||||
children: Vec<Size>,
|
||||
// [最小宽,最小高,最大宽,最大高];相同 Block 在不同父约束下不能共用旧测量。
|
||||
constraints: [f32; 4],
|
||||
// 已按出现约束钳制的最终测量输出,不保存源返回对象或为它增加拥有根。
|
||||
size: Size,
|
||||
// 源调用及返回字段解码的精确读取依赖,Step 逐项检查版本/名称有效性后才复用。
|
||||
reads: Vec<endor_core::ReadDependency>,
|
||||
}
|
||||
/// Borrowed identities and versions only: this cache adds no managed roots.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
/// 按 Mount 弱身份保存源测量结果,不为业务对象增加托管根。
|
||||
pub struct Cache {
|
||||
// 键为出现弱身份而非共享 Block;移除出现后由 start 清理,源方法/owner 变化由 Step 再检查。
|
||||
entries: HashMap<WeakObjectRef, Measurement>,
|
||||
// start 每轮清零的源测量缓存统计,原生 LayoutRuntime 自己维护另一套测量记账。
|
||||
pub hits: usize,
|
||||
pub misses: usize,
|
||||
}
|
||||
impl Cache {
|
||||
/// 从测量缓存依赖中移除宿主私有 desktop 记账对象,防止内部同步造成自我失效。
|
||||
// ui_bridge 合入缓存前传入全部窗口私有 desktop,防止另一窗口宿主记账也成为自我失效依赖。
|
||||
pub(crate) fn exclude_host_desktops(&mut self, desktops: &HashSet<ObjectRef>) {
|
||||
// 逐测量项过滤依赖,保留缓存输入与尺寸不变;这里只排除宿主对象,不重新执行 measure。
|
||||
for entry in self.entries.values_mut() {
|
||||
entry
|
||||
.reads
|
||||
@@ -66,11 +77,13 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
/// 移除受影响 owner 的源测量缓存;其他出现继续按其读取版本检查。
|
||||
// presentation 的显式 invalidate 路径按受影响业务 owner 清理;同 Block 的多个出现缓存会一起移除。
|
||||
pub fn invalidate(&mut self, owners: &HashSet<ObjectRef>) {
|
||||
self.entries
|
||||
.retain(|_, entry| !owners.contains(&entry.owner));
|
||||
}
|
||||
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
|
||||
/// 清空所有源测量条目和统计,后续源测量再按当前输入重建。
|
||||
// 小原生树切回直接布局等路径调用;清理源测量条目并重置命中/未命中统计。
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
self.hits = 0;
|
||||
@@ -82,10 +95,12 @@ impl Cache {
|
||||
struct Pending {
|
||||
// 只读子调用的精确读取跟踪区间 token,由 Step 开始、Collect 在结果解码后结束。
|
||||
token: usize,
|
||||
// Step 记下当前 Block 与实际方法,Collect 不再从可能不同的上下文重新猜测缓存身份。
|
||||
owner: ObjectRef,
|
||||
method: ObjectRef,
|
||||
children: Vec<Size>,
|
||||
constraints: [f32; 4],
|
||||
// 本次出现弱身份,测量成功后作为 Cache 键;Pending 本身不额外保活 Mount。
|
||||
key: WeakObjectRef,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
@@ -93,27 +108,35 @@ struct Pending {
|
||||
struct Plan {
|
||||
// 使用出现索引保存树和几何,不为每个位置建立第二份业务对象。
|
||||
nodes: Vec<Arc<UiNode>>,
|
||||
// 按出现下标保存直接孩子列表,和 nodes/sizes/bounds 一起形成平行数组。
|
||||
children: Vec<Vec<usize>>,
|
||||
// 每一轮最新测量值;preferred 则保留自然基准,避免弹性分配反馈后不断漂移。
|
||||
sizes: Vec<Size>,
|
||||
// preferred 是测量偏好,bounds 是父级实际分配;换行/flex 会使两者不同。
|
||||
preferred: Vec<Option<Size>>,
|
||||
bounds: Vec<Option<[f32; 4]>>,
|
||||
// 本轮的原生测量运行时与统计副本,完成后才随 Draft 交回桥接层。
|
||||
runtime: LayoutRuntime,
|
||||
// 是否实际遇到源布局覆盖;大原生树也走驱动,但不因此发布宿主驱动读取为业务依赖。
|
||||
custom: bool,
|
||||
cache: Cache,
|
||||
// 只为真正执行的源 measure 保存收集状态;原生结果或缓存命中不打开新跟踪区间。
|
||||
pending: Vec<Option<Pending>>,
|
||||
constraints: Vec<[f32; 4]>,
|
||||
// Source-arranged dimensions remain exact for this solve, not business state.
|
||||
// 每个出现两个轴的显式分配,Some 形成精确 min=max 约束;不是可写业务尺寸属性。
|
||||
allocations: Vec<[Option<f32>; 2]>,
|
||||
// 源 arrange 或需要 flex/stretch 的原生安排会记录显式孩子分配,供 Settle 反推精确约束。
|
||||
custom_arrange: Vec<bool>,
|
||||
native_arrange: Vec<bool>,
|
||||
custom_measure: Vec<bool>,
|
||||
// 源 measure 与默认结果不同的轴作为本次求解的内容上限;新一轮 start 会重新初始化。
|
||||
measured_limits: Vec<[Option<f32>; 2]>,
|
||||
// 保存几何状态用于检测收敛和重复状态,防止相互依赖布局无限循环。
|
||||
previous: Option<Vec<u32>>,
|
||||
seen: BTreeSet<Vec<u32>>,
|
||||
}
|
||||
// Step/Collect/Settle 共享纯原生求解计划;锁用于短时读写,源 VM 方法不在持锁期间执行。
|
||||
type Shared = Arc<Mutex<Plan>>;
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 源视觉调用当前处于测量还是安排阶段,决定调用方法与结果解码。
|
||||
@@ -133,9 +156,11 @@ impl Phase {
|
||||
#[derive(Debug)]
|
||||
/// 为某次出现准备一个 measure/arrange 调用,选择原生快速结果或真正的源覆盖。
|
||||
struct Step {
|
||||
// 当前步骤共享计划;index/phase 指定一次出现的一次测量或安排。
|
||||
plan: Shared,
|
||||
index: usize,
|
||||
phase: Phase,
|
||||
// Advance 为当前出现升级并显式捕获的强身份,仅当前辅助调用需要保活它。
|
||||
mount: ObjectRef,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
@@ -147,11 +172,14 @@ impl NativeCallable for Step {
|
||||
|
||||
/// 解析当前实际 measure/arrange 实现,决定原生快速路径或源调用,准备本节点输入及依赖收集,避免跳过源覆盖。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
// 先克隆 Mount 描述以跨后续 VM 分配使用;它带当前 Block 和临时布局上下文。
|
||||
let mount = cx.store().payload::<Mount>(self.mount, "Mount")?.clone();
|
||||
// 沿当前业务 Block 的真实类型查方法,不能只按节点种类判断有没有源覆盖。
|
||||
let definition = cx
|
||||
.store()
|
||||
.type_of(mount.block)?
|
||||
.ok_or(invalid("untyped layout target"))?;
|
||||
// phase.method 决定 measure/arrange,缺少协议方法视为错误而不是静默回退。
|
||||
let method = cx
|
||||
.store()
|
||||
.lookup_method(definition, self.phase.method())?
|
||||
@@ -163,11 +191,13 @@ impl NativeCallable for Step {
|
||||
self.phase.method(),
|
||||
self.desktop,
|
||||
)?;
|
||||
// 在短锁内取得当前出现输入并更新计划记账,退出后才构造托管参数及真正源调用。
|
||||
let (node, children, preferred, bounds, native_size) = {
|
||||
let mut plan = self
|
||||
.plan
|
||||
.lock()
|
||||
.map_err(|_| invalid("layout plan poisoned"))?;
|
||||
// Arc 克隆只共享不可变视觉节点,children 则复制本轮已完成的直接子测量。
|
||||
let node = plan.nodes[self.index].clone();
|
||||
let children = plan.children[self.index]
|
||||
.iter()
|
||||
@@ -178,16 +208,19 @@ impl NativeCallable for Step {
|
||||
.iter()
|
||||
.map(|child| plan.preferred[*child].unwrap_or(plan.sizes[*child]))
|
||||
.collect::<Vec<_>>();
|
||||
// measure 不暴露旧安排边界;arrange 必须使用父级已分配的本轮 bounds。
|
||||
let bounds = if matches!(self.phase, Phase::Arrange) {
|
||||
plan.bounds[self.index]
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// 每次 measure 重新记录实际是否源覆盖,Collect 据此决定是否比较默认测量以生成额外内容限制。
|
||||
if matches!(self.phase, Phase::Measure) {
|
||||
plan.custom_measure[self.index] = !intrinsic;
|
||||
}
|
||||
// 原生 measure 直接计算避免源 VM 调用;源 measure 尝试按 Mount、owner、方法、子尺寸、约束和读取版本复用。
|
||||
let native_size = if matches!(self.phase, Phase::Measure) && intrinsic {
|
||||
// 原生快路径也使用当前出现约束,不直接返回脱离父可用空间的自然尺寸。
|
||||
let constraints = plan.constraints[self.index];
|
||||
Some(
|
||||
plan.runtime
|
||||
@@ -199,12 +232,15 @@ impl NativeCallable for Step {
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
// 遇到任一源协议就标记整轮 custom,finish 才会保留过滤后的业务布局读取。
|
||||
if !intrinsic {
|
||||
plan.custom = true;
|
||||
}
|
||||
if matches!(self.phase, Phase::Measure) {
|
||||
// 测量缓存按本次 Mount 弱身份查找,仍需同时验证 owner、方法、孩子输入、约束及依赖。
|
||||
let key = cx.store().downgrade(self.mount)?;
|
||||
let constraints = plan.constraints[self.index];
|
||||
// 只有所有条件同时满足才取旧尺寸;值存在但依赖过期与完全无缓存都走未命中分支。
|
||||
let cached =
|
||||
plan.cache
|
||||
.entries
|
||||
@@ -226,6 +262,7 @@ impl NativeCallable for Step {
|
||||
} else {
|
||||
plan.cache.misses += 1;
|
||||
plan.runtime.measured += 1;
|
||||
// token 先占位为 0;输入 Constraints 初始化完成后才开启精确跟踪并写入真实 token。
|
||||
plan.pending[self.index] = Some(Pending {
|
||||
token: 0,
|
||||
owner: mount.block,
|
||||
@@ -240,6 +277,7 @@ impl NativeCallable for Step {
|
||||
None
|
||||
}
|
||||
};
|
||||
// 每次安排都计数,并记录是否原生及是否需要显式分配,后面的收敛阶段依赖这两个标记。
|
||||
if matches!(self.phase, Phase::Arrange) {
|
||||
plan.runtime.arranged += 1;
|
||||
plan.native_arrange[self.index] = intrinsic;
|
||||
@@ -268,7 +306,9 @@ impl NativeCallable for Step {
|
||||
vec![("width", value(size.width)), ("height", value(size.height))],
|
||||
)?)));
|
||||
}
|
||||
// 构造本桌面名义输入对象:measure 接收 Constraints,arrange 接收已分配 Bounds。
|
||||
let input = match self.phase {
|
||||
// 从 Plan 取当前约束后立刻释放短锁,record 的对象分配发生在锁外。
|
||||
Phase::Measure => {
|
||||
let constraints = self
|
||||
.plan
|
||||
@@ -287,6 +327,7 @@ impl NativeCallable for Step {
|
||||
],
|
||||
)?
|
||||
}
|
||||
// 父级没有分配当前边界时不能调用 arrange,否则孩子无法得到可靠的绝对布局坐标。
|
||||
Phase::Arrange => {
|
||||
let [x, y, width, height] =
|
||||
bounds.ok_or(invalid("arrangement has no allocated bounds"))?;
|
||||
@@ -314,6 +355,7 @@ impl NativeCallable for Step {
|
||||
.ok_or(invalid("missing measurement tracking state"))?
|
||||
.token = token;
|
||||
}
|
||||
// 交回 VM 调用 method(block, mount, input),结果由 Advance 安排的 Collect 统一收集。
|
||||
Ok(NativeResult::Invoke {
|
||||
function: method,
|
||||
arguments: vec![
|
||||
@@ -350,9 +392,11 @@ impl NativeCallable for Collect {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// 原生快路径、缓存包装和源覆盖都必须返回一个托管结果,不能用裸数字或 Nil 代替协议。
|
||||
let [Value::Object(result)] = args else {
|
||||
return Err(invalid("layout must return a managed result"));
|
||||
};
|
||||
// 阶段选择本桌面名义结果类型;后面的字段解码不能替代 is_instance 检查。
|
||||
let expected = match self.phase {
|
||||
Phase::Measure => "MeasureResult",
|
||||
Phase::Arrange => "Arrangement",
|
||||
@@ -364,6 +408,7 @@ impl NativeCallable for Collect {
|
||||
{
|
||||
return Err(invalid("layout returned the wrong result type"));
|
||||
}
|
||||
// 结果已经回到宿主,持锁更新当前计划;这里不再调用源 measure/arrange。
|
||||
let mut plan = self
|
||||
.plan
|
||||
.lock()
|
||||
@@ -383,10 +428,12 @@ impl NativeCallable for Collect {
|
||||
// result supplies an additional content constraint.
|
||||
// 源容器测量若不同于默认原生结果,将相应轴记成额外内容上限;后续碰巧相等也不撤销本轮已选限制。
|
||||
if plan.custom_measure[self.index] && !plan.children[self.index].is_empty() {
|
||||
// 用本轮孩子尺寸重新计算默认测量作比较,只在源容器有孩子时引入额外内容上限。
|
||||
let children = plan.children[self.index]
|
||||
.iter()
|
||||
.map(|child| plan.sizes[*child])
|
||||
.collect();
|
||||
// 原生测量失败时转换成 None,下面两轴都会保守保留源尺寸限制,而不是忽略源返回值。
|
||||
let native = layout::measure_native(
|
||||
&plan.nodes[self.index],
|
||||
children,
|
||||
@@ -396,6 +443,7 @@ impl NativeCallable for Collect {
|
||||
.map(|native| layout::constrained(native, plan.constraints[self.index]));
|
||||
// Equality after remeasurement does not retract a source
|
||||
// limit selected earlier in this solve. New renders start fresh.
|
||||
// 只设置发生差异的轴;此前已记录的限制在本次求解中保持,不因后续尺寸巧合相等而撤销。
|
||||
if !native.is_some_and(|native| native.width == size.width) {
|
||||
plan.measured_limits[self.index][0] = Some(size.width);
|
||||
}
|
||||
@@ -405,6 +453,7 @@ impl NativeCallable for Collect {
|
||||
}
|
||||
// Decoding is part of the subcall dependency region: a source
|
||||
// method may return an existing result whose fields can change.
|
||||
// 原生或缓存命中没有 Pending,不结束不存在的跟踪区间;源测量在解码宽高之后结束,覆盖结果字段读取。
|
||||
let tracked = pending
|
||||
.map(|pending| {
|
||||
cx.store()
|
||||
@@ -412,12 +461,15 @@ impl NativeCallable for Collect {
|
||||
.map(|reads| (pending, reads))
|
||||
})
|
||||
.transpose()?;
|
||||
// 只有真正源测量才尝试写缓存;精确访问记录还需判断是否读取了不稳定的 Mount 内容。
|
||||
if let Some((pending, reads)) = tracked {
|
||||
// 若观察了 Mount 内容/几何等不稳定宿主状态,无法证明跨轮可复用,保守地删除该测量缓存。
|
||||
let mut volatile = false;
|
||||
let registry = desktop_type_registry(cx, self.desktop)?;
|
||||
// accesses 提供内容读取细节,dependencies 则用于后面保留可校验依赖,两者用途不同。
|
||||
for read in &reads.accesses {
|
||||
let object = read.object;
|
||||
// 当前桌面和类型注册表属于宿主内部检查,跳过这类访问而不把它们认成易变业务输入。
|
||||
if object == self.desktop || object == registry {
|
||||
continue;
|
||||
}
|
||||
@@ -429,6 +481,7 @@ impl NativeCallable for Collect {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 观测到 Mount 内容读取就移除旧项,本次仍使用已得到的尺寸,只是不允许下次复用。
|
||||
if volatile {
|
||||
plan.cache.entries.remove(&pending.key);
|
||||
} else {
|
||||
@@ -460,11 +513,13 @@ impl NativeCallable for Collect {
|
||||
// 仅首次填写 preferred;sizes 每轮更新,根测量直接给本轮根 bounds,安排才能从根向下展开。
|
||||
plan.preferred[self.index].get_or_insert(size);
|
||||
plan.sizes[self.index] = size;
|
||||
// 根没有父安排可提供 bounds,直接以本轮测量从 (0,0) 建立根边界,随后正序 arrange 展开孩子。
|
||||
if self.index == 0 {
|
||||
plan.bounds[0] = Some([0.0, 0.0, size.width, size.height]);
|
||||
}
|
||||
}
|
||||
Phase::Arrange => {
|
||||
// Arrangement.children 必须指向受控对象列表;先取全部项,再按稳定 slot 分配到计划孩子。
|
||||
let Value::Object(list) = cx.store().get_attr(*result, "children")? else {
|
||||
return Err(invalid("Arrangement.children must be an ObjectList"));
|
||||
};
|
||||
@@ -484,40 +539,50 @@ impl NativeCallable for Collect {
|
||||
.enumerate()
|
||||
.map(|(index, slot)| (slot, index))
|
||||
.collect();
|
||||
// 记录已安排 slot 防止重复,项数相等加逐项唯一性共同保证每个孩子恰好分配一次。
|
||||
let mut assigned = BTreeSet::new();
|
||||
let expected = desktop_type(cx.store(), "Bounds", self.desktop)?;
|
||||
// 源返回列表可以重新排序,实际孩子用 slot 查表确定,不按列表位置直接对应。
|
||||
for item in items {
|
||||
// 每项需是本桌面 Bounds,之后 slot 和几何字段仍逐项校验。
|
||||
if !cx.store().is_instance(*item, expected)? {
|
||||
return Err(invalid("Arrangement child must be Bounds"));
|
||||
}
|
||||
let Value::Number(slot) = cx.store().get_attr(*item, "slot")? else {
|
||||
return Err(invalid("Arrangement child requires a slot"));
|
||||
};
|
||||
// slot 先要求可转为 i64 整数,再拒绝负值转为 u64;超出 i64 范围的值也不能作为此处身份。
|
||||
let slot = slot
|
||||
.as_i64()
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.ok_or(invalid("invalid arrangement slot"))?;
|
||||
// 只接受当前父节点 slots 表中的身份,表外身份不能混入安排。
|
||||
let position = slots
|
||||
.get(&slot)
|
||||
.copied()
|
||||
.ok_or(invalid("Arrangement references a foreign child slot"))?;
|
||||
// 重复项不能覆盖同一个孩子后遗漏另一个孩子,即使总体列表长度正确也要失败。
|
||||
if !assigned.insert(slot) {
|
||||
return Err(invalid("duplicate arrangement slot"));
|
||||
}
|
||||
// x/y 可负表示相对布局延伸,宽高必须非负且有限;下面还检查右下组合边界。
|
||||
let bounds = [
|
||||
number(cx, *item, "x", true)?,
|
||||
number(cx, *item, "y", true)?,
|
||||
number(cx, *item, "width", false)?,
|
||||
number(cx, *item, "height", false)?,
|
||||
];
|
||||
// 单项数值合法仍可能在相加后超出几何预算,不能只检查四个字段各自范围。
|
||||
if bounds[0] + bounds[2] > MAX_DIMENSION as f32
|
||||
|| bounds[1] + bounds[3] > MAX_DIMENSION as f32
|
||||
{
|
||||
return Err(invalid("arrangement bounds exceed limits"));
|
||||
}
|
||||
// 将 slot 表内的位置转换为 Plan 的全局出现下标,用于写 bounds 与轴分配。
|
||||
let child = children[position];
|
||||
// 源安排或原生弹性分配的差异保留为显式轴分配,下一轮测量使用精确尺寸,确保文字按实际宽度重排。
|
||||
if plan.custom_arrange[self.index] {
|
||||
// 原生主轴 flex 分配即使恰好等于当前测量也显式保存,避免下一次父约束传播撤销已分配空间。
|
||||
if plan.native_arrange[self.index]
|
||||
&& let Some(main) = layout::main_axis(&plan.nodes[self.index])
|
||||
{
|
||||
@@ -526,6 +591,7 @@ impl NativeCallable for Collect {
|
||||
// the next parent-constraint propagation pass.
|
||||
plan.allocations[child][main] = Some(bounds[main + 2]);
|
||||
}
|
||||
// 源或弹性安排改变宽/高时分别设置精确轴分配,未变化且未显式要求的轴仍可按自然测量工作。
|
||||
if bounds[2] != plan.sizes[child].width {
|
||||
plan.allocations[child][0] = Some(bounds[2]);
|
||||
}
|
||||
@@ -533,6 +599,7 @@ impl NativeCallable for Collect {
|
||||
plan.allocations[child][1] = Some(bounds[3]);
|
||||
}
|
||||
}
|
||||
// 保存本轮孩子绝对布局边界;正序 Arrange 随后轮到该孩子时才能取得自己的输入。
|
||||
plan.bounds[child] = Some(bounds);
|
||||
}
|
||||
}
|
||||
@@ -553,6 +620,7 @@ impl NativeCallable for Settle {
|
||||
}
|
||||
/// 由新 bounds 推导下一轮约束与自然尺寸,比较完整布局状态;稳定则结束,循环状态报错,否则继续同帧求解。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
// 收敛计算持 Plan 短锁,后面的临时 Mount 上下文清理放在锁外执行。
|
||||
let repeat = {
|
||||
let mut plan = self
|
||||
.plan
|
||||
@@ -561,29 +629,36 @@ impl NativeCallable for Settle {
|
||||
plan.runtime.settle_passes += 1;
|
||||
// 安排完成后按最终 bounds 从父到子推导下一轮约束,测量限制和源分配可反向影响换行高度。
|
||||
let mut next = plan.constraints.clone();
|
||||
// 按父先子后传播下一轮约束,next[parent] 先确定后才推导直接孩子。
|
||||
for index in 0..plan.nodes.len() {
|
||||
let children = plan.children[index].clone();
|
||||
let [_, _, width, height] =
|
||||
plan.bounds[index].ok_or(invalid("unarranged occurrence"))?;
|
||||
// 先继承本节点下一轮约束,再用源测量限制和实际已分配宽高调整可供孩子使用的上界。
|
||||
let mut available = next[index];
|
||||
// 源测量选择的内容上限优先加入 available,后面实际分配差异也可能继续覆盖该轴。
|
||||
if let Some(width) = plan.measured_limits[index][0] {
|
||||
available[2] = width;
|
||||
}
|
||||
if let Some(height) = plan.measured_limits[index][1] {
|
||||
available[3] = height;
|
||||
}
|
||||
// 父级实际分配与测量不同,孩子的可用宽度应跟随分配,否则换行仍按旧宽度计算。
|
||||
if width != plan.sizes[index].width {
|
||||
available[2] = width;
|
||||
}
|
||||
if height != plan.sizes[index].height {
|
||||
available[3] = height;
|
||||
}
|
||||
// 根据父盒模型与实际宽高计算孩子公共约束基础,随后再考虑各个出现的独立尺寸规则。
|
||||
let base = layout::arranged_child_constraints(
|
||||
&plan.nodes[index],
|
||||
available,
|
||||
Size { width, height },
|
||||
);
|
||||
// child_index 是父内顺序,child 是全局出现下标,两者不能互换用于平行数组。
|
||||
for (child_index, child) in children.into_iter().enumerate() {
|
||||
// 保留未施加精确轴分配的自然约束;交叉轴改变时,撤销旧原生主轴分配需要用它恢复。
|
||||
let natural = layout::occurrence_constraints(
|
||||
&plan.nodes[index],
|
||||
&plan.nodes[child],
|
||||
@@ -601,6 +676,7 @@ impl NativeCallable for Settle {
|
||||
constraints[1] = height;
|
||||
constraints[3] = height;
|
||||
}
|
||||
// 线性布局才有主/交叉轴;交叉轴范围变化可触发文字重排,进而改变主轴自然尺寸。
|
||||
if let Some(main) = layout::main_axis(&plan.nodes[index]) {
|
||||
let cross = 1 - main;
|
||||
// 交叉轴变化可能改变换行及主轴自然高度,清 preferred 让其重新测量;原生派生主轴分配也需暂时撤销。
|
||||
@@ -610,6 +686,7 @@ impl NativeCallable for Settle {
|
||||
// Wrapping or other cross-dependent measurement
|
||||
// invalidates this occurrence's natural main size.
|
||||
plan.preferred[child] = None;
|
||||
// 只撤销原生派生主轴分配,源 arrange 的明确分配仍是权威约束,不能被自动回退覆盖。
|
||||
if plan.native_arrange[index] {
|
||||
// Derived flex sizes must not constrain their
|
||||
// own replacement measurement. Source arrange
|
||||
@@ -623,9 +700,11 @@ impl NativeCallable for Settle {
|
||||
next[child] = constraints;
|
||||
}
|
||||
}
|
||||
// 状态向量用于相邻稳定比较及历史重复检测;capacity 只是预分配提示,不限制最终编码长度。
|
||||
let mut state = Vec::with_capacity(plan.nodes.len() * 14);
|
||||
// 把约束、实测、bounds、分配和首选值编码成几何状态,统一 +0/-0,避免仅符号位差异造成假不收敛。
|
||||
let bits = |value: f32| if value == 0.0 { 0 } else { value.to_bits() };
|
||||
// 编码每次出现的完整参与状态,不能只比较根宽高,否则内部布局仍变化时可能提前停止。
|
||||
for (index, constraints) in next.iter().enumerate() {
|
||||
state.extend(constraints.iter().copied().map(bits));
|
||||
state.extend([
|
||||
@@ -638,11 +717,13 @@ impl NativeCallable for Settle {
|
||||
.into_iter()
|
||||
.map(bits),
|
||||
);
|
||||
// Option 先编码存在标记再编码数值,使 None 和显式 Some(0) 在收敛判断中保持不同。
|
||||
for dimension in plan.allocations[index] {
|
||||
state.push(u32::from(dimension.is_some()));
|
||||
state.push(dimension.map_or(0, bits));
|
||||
}
|
||||
state.push(u32::from(plan.preferred[index].is_some()));
|
||||
// 首选尺寸同样带存在标记;缺失时写两个零占位以维持每项固定字段布局。
|
||||
if let Some(size) = plan.preferred[index] {
|
||||
state.extend([bits(size.width), bits(size.height)]);
|
||||
} else {
|
||||
@@ -653,6 +734,7 @@ impl NativeCallable for Settle {
|
||||
if plan.previous.as_ref() == Some(&state) && plan.constraints == next {
|
||||
false
|
||||
} else {
|
||||
// 与紧邻上轮不同却命中历史状态,说明出现振荡环,返回错误而不是无限重试。
|
||||
if !plan.seen.insert(state.clone()) {
|
||||
return Err(invalid("layout constraints did not converge (cycle)"));
|
||||
}
|
||||
@@ -663,8 +745,10 @@ impl NativeCallable for Settle {
|
||||
true
|
||||
}
|
||||
};
|
||||
// 只有需要新一轮才清理 VM 可观察的临时 Bounds;稳定结束时保留最终安排供后续提交。
|
||||
if repeat {
|
||||
// No stale geometry from a prior pass is observable during measurement.
|
||||
// 逐项升级当前布局 Mount,清除上下文旧 bounds,防止下一轮 measure 观察上一轮安排几何。
|
||||
for weak in self.mounts.iter() {
|
||||
let mount = cx.upgrade(*weak)?.ok_or(invalid("layout Mount expired"))?;
|
||||
if let Some(context) = &cx.store().payload::<Mount>(mount, "Mount")?.layout {
|
||||
@@ -693,6 +777,7 @@ impl NativeCallable for Prepare {
|
||||
}
|
||||
/// 建立按出现位置推进的 Advance 和固定字节码循环,保持同一 VM 预算,避免首次让出前创建整树临时调用对象。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
// Prepare 只建立共享游标适配器和固定循环;不会在首次让出前一次创建全树 Step/Collect。
|
||||
let advance = cx.allocate(ContextNativeFunction::new(
|
||||
"next layout occurrence",
|
||||
Advance {
|
||||
@@ -717,6 +802,7 @@ struct Advance {
|
||||
plan: Shared,
|
||||
mounts: Arc<[WeakObjectRef]>,
|
||||
desktop: ObjectRef,
|
||||
// 每轮 [0,N) 为逆序测量,[N,2N) 为正序安排,2N 专门进入 Settle。
|
||||
next: Mutex<usize>,
|
||||
}
|
||||
impl NativeCallable for Advance {
|
||||
@@ -726,6 +812,7 @@ impl NativeCallable for Advance {
|
||||
}
|
||||
/// 按游标安排自底向上 measure 与自顶向下 arrange,短锁结束后才创建/执行当前节点调用,末尾进入收敛检查。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
// mounts 与 Plan 出现数组同序等长,start 已检查数量,游标借这个长度选择阶段和下标。
|
||||
let count = self.mounts.len();
|
||||
// 游标取值/递增只在短锁内完成,不能持锁执行源协议,否则源调用基类会再次访问同一 Plan。
|
||||
let position = {
|
||||
@@ -734,6 +821,7 @@ impl NativeCallable for Advance {
|
||||
.lock()
|
||||
.map_err(|_| invalid("layout cursor poisoned"))?;
|
||||
let position = *next;
|
||||
// 只在节点步骤范围内递增;末尾 Settle 自己决定重置为 0 或让外层循环停止。
|
||||
if position < count * 2 {
|
||||
*next += 1;
|
||||
}
|
||||
@@ -741,6 +829,7 @@ impl NativeCallable for Advance {
|
||||
};
|
||||
// 每轮恰有 N 个逆序 measure 和 N 个正序 arrange,之后调用 Settle;需要重算时游标归零。
|
||||
if position == count * 2 {
|
||||
// 所有节点完成 measure/arrange 才调用收敛检查;它返回是否需要下一轮,不再增加独立 VM 总预算。
|
||||
let settled = Settle {
|
||||
plan: self.plan.clone(),
|
||||
mounts: self.mounts.clone(),
|
||||
@@ -749,6 +838,7 @@ impl NativeCallable for Advance {
|
||||
let NativeResult::Return(Value::Bool(repeat)) = settled else {
|
||||
return Err(invalid("invalid settling result"));
|
||||
};
|
||||
// 需要继续时重置同一个游标,后续仍回到原 repeat_call 帧执行整轮;false 则结束布局。
|
||||
if repeat {
|
||||
*self
|
||||
.next
|
||||
@@ -763,6 +853,7 @@ impl NativeCallable for Advance {
|
||||
} else {
|
||||
(Phase::Arrange, position - count)
|
||||
};
|
||||
// 只升级当前出现,Step 显式捕获保证这一步的 Mount 活到收集完成,其他候选仍保留弱身份。
|
||||
let mount = cx
|
||||
.upgrade(self.mounts[index])?
|
||||
.ok_or(invalid("layout Mount expired"))?;
|
||||
@@ -778,6 +869,7 @@ impl NativeCallable for Advance {
|
||||
},
|
||||
vec![mount, self.desktop],
|
||||
))?;
|
||||
// 收集器保存同一 index/phase,保证方法结果回写正确出现而不依赖驱动游标当前值。
|
||||
let collector = cx.allocate(ContextNativeFunction::new(
|
||||
"collect layout",
|
||||
Collect {
|
||||
@@ -806,9 +898,11 @@ impl NativeCallable for Advance {
|
||||
|
||||
/// An unpublished layout frame and derived caches. These are merged only after
|
||||
/// the driver completes; they are not another copy of application state.
|
||||
/// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。
|
||||
/// 未发布布局草稿及派生缓存;Work 持有期间仍在求解,Complete 后交桥接层投影与提交。
|
||||
pub struct Draft {
|
||||
// 未投影/未发布的布局草稿;ui_bridge 完成后还要执行滚动、视口投影与挂载提交。
|
||||
pub frame: Frame,
|
||||
// 完整求解统计和原生运行时缓存,与 frame、源 Cache 一起交付,不能单独提前覆盖已发布状态。
|
||||
pub runtime: LayoutRuntime,
|
||||
pub cache: Cache,
|
||||
pub reads: Vec<endor_core::ReadDependency>,
|
||||
@@ -817,6 +911,7 @@ pub struct Draft {
|
||||
#[must_use = "pending layout work must be resumed or cancelled with its owning VM"]
|
||||
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
|
||||
pub enum Progress {
|
||||
// ui_bridge 保存工作句柄和上下文,endor_host 之后驱动恢复或取消,不发布中间几何。
|
||||
Pending(Work),
|
||||
Complete(Draft),
|
||||
}
|
||||
@@ -825,8 +920,10 @@ pub enum Progress {
|
||||
/// The prepare handle owns one root until completion or explicit cancellation.
|
||||
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
|
||||
pub struct Work {
|
||||
// 开始时的原生草稿与缓存状态独立拥有,挂起不携带外部可变引用。
|
||||
state: Draft,
|
||||
plan: Shared,
|
||||
// 驱动入口拥有根,正常完成或 cancel 都需显式 release;Rust drop 不负责 VM 引用计数。
|
||||
prepare: ObjectRef,
|
||||
desktop: ObjectRef,
|
||||
slices: usize,
|
||||
@@ -834,6 +931,7 @@ pub struct Work {
|
||||
|
||||
impl Work {
|
||||
/// 恢复当前 Work 拥有的 VM 续体,继续同一轮求解而不是重放 measure/arrange。
|
||||
// ui_bridge::resume_layout_frame 调用,沿原只读续体继续;slices 统计包含 start 的第一片。
|
||||
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
|
||||
self.slices += 1;
|
||||
let progress = vm.resume_read_only_call(slice);
|
||||
@@ -841,6 +939,7 @@ impl Work {
|
||||
}
|
||||
|
||||
/// 显式结束布局续体和只读域,释放本次尚未提交的临时根。
|
||||
// 桥接层替换或撤销挂起布局时调用;消费 Work 结束续体,派生 Plan 不合入文档。
|
||||
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
|
||||
// 取消与释放入口根均先尝试,再依次传播错误;普通 Rust drop 无法替代 VM 只读域清理。
|
||||
let cancelled = vm.cancel_read_only_call();
|
||||
@@ -856,6 +955,7 @@ impl Work {
|
||||
vm: &mut Vm,
|
||||
progress: Result<endor_core::ExecutionProgress, VmError>,
|
||||
) -> Result<Progress> {
|
||||
// Pending 保存所有本轮状态;Complete 释放入口后汇总,错误先取诊断再尝试取消清理。
|
||||
match progress {
|
||||
Ok(endor_core::ExecutionProgress::Pending { .. }) => Ok(Progress::Pending(self)),
|
||||
Ok(endor_core::ExecutionProgress::Complete {
|
||||
@@ -878,6 +978,7 @@ impl Work {
|
||||
}
|
||||
|
||||
/// 建立只读布局 Plan 与托管驱动函数;小型原生树可走直接路径,大树保留分片推进能力。
|
||||
// ui_bridge::begin_layout_frame 提供本轮结构帧、临时 Mount 与缓存副本,结果由 accept_layout_progress 接续。
|
||||
pub fn start(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
@@ -886,16 +987,19 @@ pub fn start(
|
||||
mut state: Draft,
|
||||
slice: usize,
|
||||
) -> Result<Progress> {
|
||||
// 禁止零切片及同 VM 已有只读执行,不能覆盖另一个挂起续体。
|
||||
anyhow::ensure!(slice > 0, "layout slice must be positive");
|
||||
anyhow::ensure!(
|
||||
!vm.has_suspended_call() && !vm.object_store().is_read_only(),
|
||||
"cannot start layout while another read-only execution is active"
|
||||
);
|
||||
// 分字段借用草稿以准备本轮,cache 命中统计重置但条目先保留供下方筛选。
|
||||
let frame = &mut state.frame;
|
||||
let runtime = &mut state.runtime;
|
||||
let cache = &mut state.cache;
|
||||
cache.hits = 0;
|
||||
cache.misses = 0;
|
||||
// 当前出现弱身份集合用于剪掉退役缓存键;缓存条目内的业务借用身份不会因此增加根。
|
||||
let live: HashSet<_> = mounts.iter().copied().collect();
|
||||
// 先删除不再出现在此结构中的测量项,防止已移除的弱 Mount 身份无限积累。
|
||||
cache.entries.retain(|mount, _| live.contains(mount));
|
||||
@@ -907,12 +1011,14 @@ pub fn start(
|
||||
let mount = vm
|
||||
.upgrade(*weak)?
|
||||
.ok_or_else(|| anyhow::anyhow!("layout Mount expired"))?;
|
||||
// 方法检查错误先保存为 Result,下面总会尝试 release 临时升级的 Mount,再传播检查结果。
|
||||
let result = (|| {
|
||||
let block = vm.payload::<Mount>(mount, "Mount")?.block;
|
||||
let definition = vm
|
||||
.object_store()
|
||||
.type_of(block)?
|
||||
.ok_or(invalid("untyped layout target"))?;
|
||||
// measure 与 arrange 任一源覆盖或缺失都不能走小原生直接路径;缺失在正式步骤中报告协议错误。
|
||||
for name in ["measure", "arrange"] {
|
||||
let Some(method) = vm.object_store().lookup_method(definition, name)? else {
|
||||
return Ok::<_, VmError>(true);
|
||||
@@ -926,8 +1032,10 @@ pub fn start(
|
||||
vm.release(mount)?;
|
||||
custom |= result?;
|
||||
}
|
||||
// 用显式栈建立视觉先序列表,倒序压入孩子保持原兄弟顺序,与结构帧/Mount 的出现顺序对应。
|
||||
let mut nodes = Vec::new();
|
||||
let mut work = vec![root.clone()];
|
||||
// 除了真实源覆盖,flex 或分配孩子的容器也需要收敛驱动,因为安排可能反过来改变测量。
|
||||
while let Some(node) = work.pop() {
|
||||
custom |= node.has_flex() || node.allocates_children();
|
||||
work.extend(node.children().iter().rev().cloned());
|
||||
@@ -937,7 +1045,9 @@ pub fn start(
|
||||
// when every protocol method is native. Use the same settling driver.
|
||||
// 仅小于等于64节点且无自定义/弹性分配的树走直接路径;大原生树仍经同一驱动以便分片让出。
|
||||
if !custom && nodes.len() <= NATIVE_FAST_PATH_MAX_NODES {
|
||||
// 直接路径不使用源测量缓存;保留的原生测量缓存属于 runtime,清空的是这里的源 Cache。
|
||||
cache.clear();
|
||||
// 小原生树一次生成完整帧,无须分配协议结果对象;前面的只读执行排他检查仍然适用。
|
||||
let mut native = runtime.frame(&root)?;
|
||||
// Only the structural pass consumes a published version.
|
||||
// 结构准备已经分配本次发布版本,快速完整测量不能再多消耗一个可见布局版本。
|
||||
@@ -952,6 +1062,7 @@ pub fn start(
|
||||
nodes.len() == mounts.len() && nodes.len() == frame.mounts.len(),
|
||||
"layout structure does not match Mount contexts"
|
||||
);
|
||||
// 按结构帧的 Mount ID 查父出现位置,再构造与 nodes 同下标的孩子索引表。
|
||||
let indices: HashMap<_, _> = frame
|
||||
.mounts
|
||||
.iter()
|
||||
@@ -959,6 +1070,7 @@ pub fn start(
|
||||
.map(|(index, mount)| (mount.id, index))
|
||||
.collect();
|
||||
let mut children = vec![Vec::new(); nodes.len()];
|
||||
// 只记录直接父子关系,根没有父项;协议的子尺寸和稳定 slot 都按这个顺序匹配。
|
||||
for (index, mount) in frame.mounts.iter().enumerate() {
|
||||
if let Some(parent) = mount.parent {
|
||||
children[indices[&parent]].push(index);
|
||||
@@ -966,6 +1078,7 @@ pub fn start(
|
||||
}
|
||||
// 从根向下初始化约束,之后每轮由 Settle 结合真实父分配细化;初始值不是最终精确几何。
|
||||
let mut constraints = vec![runtime.root_constraints(); nodes.len()];
|
||||
// 父约束已在更早下标准备好,再逐孩子推导首次约束,后续实际分配仍可能触发重测。
|
||||
for index in 0..nodes.len() {
|
||||
for (child_index, child) in children[index].iter().enumerate() {
|
||||
constraints[*child] = layout::initial_child_constraints(
|
||||
@@ -976,6 +1089,7 @@ pub fn start(
|
||||
);
|
||||
}
|
||||
}
|
||||
// 以下平行数组均按出现数量初始化,None 代表尚未测量/安排或尚无明确轴分配。
|
||||
let count = nodes.len();
|
||||
// 把运行时/缓存克隆进未发布 Plan,失败或取消时上层可以丢弃派生求解结果而不提交半帧。
|
||||
let plan = Arc::new(Mutex::new(Plan {
|
||||
@@ -997,6 +1111,7 @@ pub fn start(
|
||||
previous: None,
|
||||
seen: BTreeSet::new(),
|
||||
}));
|
||||
// 共享 Plan 由原生适配器持有,desktop 显式作为托管捕获;Work 保存入口拥有根直到完成/取消。
|
||||
let prepare = vm.allocate(ContextNativeFunction::new(
|
||||
"prepare source layout",
|
||||
Prepare {
|
||||
@@ -1006,6 +1121,7 @@ pub fn start(
|
||||
},
|
||||
vec![desktop],
|
||||
))?;
|
||||
// 在执行第一片前绑定草稿、计划和入口,第一片完成或挂起都走相同 accept 分支。
|
||||
let work = Work {
|
||||
state,
|
||||
plan,
|
||||
@@ -1027,6 +1143,7 @@ pub fn start(
|
||||
}
|
||||
|
||||
/// 验证求解已收敛,收集最终几何、读取依赖和缓存,返回尚待桥接层提交的 Draft。
|
||||
// 只由 Work::accept 收到 VM Complete 后调用;驱动已经停止,prepare 根已经释放。
|
||||
fn finish(
|
||||
vm: &mut Vm,
|
||||
mut work: Work,
|
||||
@@ -1035,6 +1152,7 @@ fn finish(
|
||||
) -> Result<Progress> {
|
||||
// 驱动返回值仅为控制流结果,先释放;真正结果保存在原生 Plan,而非返回值中的第二份业务图。
|
||||
vm.release_value(value)?;
|
||||
// 借用最终计划并记录实际切片数,接下来只汇总结果,不再执行源布局或重启求解。
|
||||
let mut plan = work
|
||||
.plan
|
||||
.lock()
|
||||
@@ -1043,6 +1161,7 @@ fn finish(
|
||||
let frame = &mut work.state.frame;
|
||||
// 用最终收敛 bounds 更新 frame 的各出现;这里只生成默认内容命令,后续投影/源 paint 还会重建完整绘制表。
|
||||
frame.commands.clear();
|
||||
// 每次出现必须有最终 bounds,否则拒绝把缺失安排的草稿交给桥接层。
|
||||
for (index, mount) in frame.mounts.iter_mut().enumerate() {
|
||||
let [x, y, width, height] =
|
||||
plan.bounds[index].ok_or_else(|| anyhow::anyhow!("unarranged occurrence"))?;
|
||||
@@ -1050,6 +1169,7 @@ fn finish(
|
||||
mount.y = y;
|
||||
mount.width = width;
|
||||
mount.height = height;
|
||||
// 仅生成可用的默认单条绘制并检查有效性;完整 paint 与平行元数据在桥接投影/源绘制阶段重建。
|
||||
if let Some(command) =
|
||||
layout::paint_native(&plan.nodes[index], x, y, Size { width, height })
|
||||
{
|
||||
@@ -1057,6 +1177,7 @@ fn finish(
|
||||
frame.commands.push(command);
|
||||
}
|
||||
}
|
||||
// 同步取回最终运行时统计和源测量缓存,避免成功帧搭配上轮缓存。
|
||||
work.state.runtime = plan.runtime.clone();
|
||||
work.state.cache = plan.cache.clone();
|
||||
// 大原生树可能使用分片驱动但没有真正源读取,因此无需把宿主驱动读取当业务布局依赖发布。
|
||||
@@ -1064,6 +1185,7 @@ fn finish(
|
||||
work.state.reads.clear();
|
||||
return Ok(Progress::Complete(work.state));
|
||||
}
|
||||
// 源布局存在时才过滤真实读取;类型注册表属于宿主内部协议检查,不能作为业务失效依赖。
|
||||
let registry = match vm.get_attr(work.desktop, "types")? {
|
||||
Value::Object(object) => object,
|
||||
_ => anyhow::bail!("missing type registry"),
|
||||
@@ -1071,6 +1193,7 @@ fn finish(
|
||||
work.state.reads = reads
|
||||
.into_iter()
|
||||
.filter(|dependency| {
|
||||
// 剔除桌面、注册表、Mount 和已死亡身份,剩余依赖交 ui_bridge 做持续布局失效判断。
|
||||
let object = dependency.object();
|
||||
object != work.desktop
|
||||
&& object != registry
|
||||
|
||||
Reference in New Issue
Block a user