38-view-distance-fog.md 26 KB

38: 区内视距雾气

What to build: 在主场景区内加一层只作用于有有效几何深度地物的距离雾:近处地物清晰、远处平滑融入雾色,旋转镜头时不露出远景瑕疵。区内生效、全球与引导态关闭;照到规格的默认参数,并在没有深度能力或运行失败时如实降级、不挡住业务。这是需求②的落地。

Blocked by: 37

Status: resolved Labels: ready-for-agent

契约来源

  • 规格:cockpit-primary-area-guardrails/spec-fog.md(ready-for-agent;由决策票 07 固化)。
  • 决策:cockpit-primary-area-guardrails/issues/07-fog-implementation-spec.md(已 resolved)。
  • 可丢弃原型(只作参考,不进生产):src/modules/cockpit/prototypes/FogPrototype.vue、fog-prototype-stage.js;打开方式见 cockpit-primary-area-guardrails/prototypes/fog-review.md。

验收

  • 单一 Cesium PostProcessStage 深度后处理雾;不引入第二渲染器(ADR 0004 / 0007;决策 06 已裁定废止「深度喂 Three」备选)。
  • 采用规格的 B 方案:只处理有有效几何深度的地物;无有效深度的背景保留原颜色,不被整屏染色。
  • 默认参数按规格固化:世界射线距离 18,000 m 起雾、42,000 m 完全遮蔽、基准雾色 #bdcdd6,中间平滑增浓;两端语义精确(0 处全清、42 km 处全遮蔽,单纯指数衰减不算达标)。
  • 冷白灰、不发绿:最终色相在真实画面里由维护者复核确认(决策 08 的实测反馈;#bdcdd6 是起点不是终值)。维护者 2026-09-28 在真实画面确认通过(生效值仍为规格默认 #bdcdd6);另有可断言的色相判据兜底(见下)。
  • 门控:areaState='locked' 期间生效;纯全球/引导态关闭。进入途中的早期高度渐变只作强度输入,不作为「可揭示精细影像」的充分条件(原型的 60/20 km 高度带已被人工演示判为遮挡过晚)。
  • 生命周期:可提前准备 → 启用 → 调强度 → 退出淡出(基准 0.8 s)→ 取消过期操作 → 禁用 → 销毁;复用处理级对象,离开地图时销毁;连续进出只认最新目标(过期淡出回调不得关掉新的进入)。
  • 对外可观测:是否支持、是否可执行、当前强度、当前参数、失败/降级原因;「就绪」不等于「边缘已遮住」,不提供未经验证的全局「已遮缝」承诺。
  • 无深度支持或运行失败 → 整级禁用 + 如实提示;业务可用优先(近景与业务仍可用),精细层仍可渐显但只有雾可用时才承诺遮缝。
  • 管线视图出现透明对象错误雾化时,允许减弱或暂关自建雾,保留相机限位并提示;正常情况不预先关闭。
  • 三维设备特效随远景雾衰减、不开穿雾例外;若走场景内 billboard 则天然在雾前,若走后处理级则按「先特效、后雾」串联,两级开关独立;面板内报警文字与状态始终清晰。
  • 性能:同机 / 同分辨率 / 同视角 / 相同加载条件下,开启自建雾的 P95 帧耗时增幅 ≤15%;首次启用与进出切换的卡顿单独记录。超预算先优化,仍不达标则降质或关闭并提示。实测 P95 增幅 0%(见下;口径限制已如实标注)。
  • 证据:纯逻辑断言(衰减端点、进出强度推进、降级选择、过期取消)+ 真机目视与性能读数。逻辑断言 24 个闸门全绿;真机目视由维护者 2026-09-28 确认通过;性能读数已实测。
  • 本票实现并验收规格;若实现中必须偏离规格结论,先回写票面并经维护者确认,不静默改口径。

进度:100%

已完成并验收(2026-09-28)。 维护者在真实画面确认「效果没问题,完成验收」: 色相(冷白灰、不发绿)、进出观感、以及"矩形边缘是否被遮住"三项目视判据一次通过。

第二轮(2026-09-29/30):维护者复核后实报「退出时整个地球变白一段时间」并指出修复引出 「雾几乎一瞬间就产生或消失」;根因、修法(时间限速 + 慢起飞行)与最终现场取值见 「维护者第二次现场复核」一节。结论:维护者 2026-09-30 真机调定并确认"目前的效果就行"。

验收闭环的关键一步:维护者实报「之前能看到雾、现在看不见了」, 据此定位并修掉一个读数全对、画面全错的静默失效 bug(下方第 0 项)—— 这一条只有人看画面才能发现,任何"读数断言"都会漏过它。

性能读数(补充实测,口径已标注)

在隐藏标签页限制下(真实渲染循环被冻结,postRender 采不到自然帧), 改用 __cockpitMap.forceRender()(转调 scene.render())逐帧计时,预热后各取 120 帧:

中位 P95 最小 最大
开雾 2.2 ms 3.6 ms 1.7 12.5
关雾 1.8 ms 3.6 ms 1.5 12.6
差值 +0.4 ms 0 ms(0%) — —

P95 增幅 0%,在 15% 预算内。 口径限制(如实标注,不含糊):这量的是 scene.render() 的 CPU 侧调用耗时,不是 GPU 时间,也不是严格意义上的"帧耗时"; 每帧强制渲染也与自然渲染循环的调度不同。它足以说明"这一层没有把帧预算吃掉", 但不能替代一次在可见窗口、固定视角下的自然帧采样。若日后要严格复现: 可见窗口里 startFogPerfSample('with-fog') → 停一会儿 → stopFogPerfSample(), 再用 setFogUserDisabled(true) 采一组基线对比。

已完成项

  1. 最终色相:维护者确认真实画面通过(生效值仍为规格默认 #bdcdd6)。
  2. 观感验收:进入 → 锁定 → 旋转 → 浅俯角全过程,远景与影像边缘的遮挡效果维护者确认通过。 决策 07 记过的失败模式(「雾出现太晚、矩形边缘先暴露」)本次未复现。
  3. 性能:上表实测 P95 增幅 0%(口径限制见上)。

改动

  • 新增 domain/fog.js:衰减曲线(端点精确)、门控、进出强度推进与落态、 过期判定、进入高度带、降级选择、色相判据、「就绪≠已遮住」的读数形状、性能预算判定。 纯函数、不 import cesium。
  • services/cesiumEngine.js:新增 createFogStage / applyFogUniforms / fogStageExecutable / hasFogStage 与那段 FOG_FRAGMENT_SHADER。全仓唯一一处 postProcessStages.add(单一处理级)。
  • services/mapService.js:雾的策略与生命周期唯一所有者(准备/启用/调强度/淡出/取消/禁用/销毁), 门控收口在 setAreaState(票 37 的区域状态一变,雾就跟着走);宿主读数 9 个 data-*-fog-*; DEV 把手 getFogState / setFogTranslucentArtifacts / setFogUserDisabled / startFogPerfSample。
  • services/cockpitConfig.js:map.fog 字段(缺省=规格默认;脏值退回默认并记 issue)。
  • CockpitShell.vue:顶栏雾读数 chip(支持/强度/降级/原因,标题写明"就绪≠已遮住")+ 降级提示色。
  • 文档:docs/cockpit-deployment.md 新增 2.11 区内视距雾;config.example.json 同步该字段。

关键取舍

  1. 曲线在 CPU 侧算,GLSL 只做等价实现:端点语义是本票验收点,fogAttenuation 可脱浏览器断言。 GLSL 那份必须逐片元算,故保留;静态守卫禁止第二份 JS 曲线。
  2. stage.ready 不参与"能不能用"的判定(详见下方第 2 个真 bug)。
  3. 降级上限作用在"写入引擎的强度"上,不改状态机 —— 恢复时不会因为中途被压低而错位。
  4. 不加"等 N 秒判失败"的宽限期:隐藏标签页是正常状态,据此判失败会把正常报成故障。

证据

  • 逻辑闸门 verify/t38-fog.mjs 通过,覆盖:端点精确(0 处恰好 0、42 km 处恰好 1、端点内侧 41,999 m 也须收敛到 ~1,用单纯指数衰减作对照证明判别力)、门控、进出推进与落态、过期取消(连续进出只认最新目标)、 进入高度带只出强度标量、降级(正常不预关闭、默认不碰限位、可逆)、色相判据(基准色通过 / 发绿与深色不通过)、 「就绪≠已遮住」的结构性保证(读数里没有 protected 这类承诺)、性能预算判定、配置参数可调且脏值退回默认。
  • 变异测试 11 处,全部被对应断言打红(一次性脚本跑完即删):① 衰减不保端点;② 门控放宽; ③ 不换 token(过期保护失效);④ 管线视图预关闭;⑤ 色相"不发绿"判据失效;⑥ 谎称已遮缝; ⑦ 预算放宽到 50%;⑧ 引擎出现第二个后处理级;⑨ 宿主不写"未证明遮住边缘";⑩ 雾门控不跟区域状态;⑪ 外壳自己重写曲线。 另有 2 处针对真机 bug 的定向变异(待命报成失败 / null 被当成 0)也被抓住。
  • 真机浏览器验证(DSH 自带浏览器,真实 yarn dev @ 127.0.0.1:8000,真实运行时配置): shader 真的编译并跑通了 —— staged: true、executable: true、ranOnce: true、 strength: 1、phase: locked;参数确认为 18,000/42,000 m、#bdcdd6、fadeIn 900 / fadeOut 800 ms; 读数在连续采样中稳定为 ready(不在 ready/failed 之间跳);coversEdges 恒为 false。
  • 全量回归:24 个零依赖逻辑闸门全绿(t05 守卫面纳入 fog.js,233→236)。

真机验证抓到的四个真 bug(都已修 + 补了回归)

  1. (最隐蔽的一个,2026-09-28 由维护者实报"之前能看到雾、现在看不见了"揪出)动态 uniform 用常量传 → 静默永不生效。 依据 Cesium 1.145 Scene/PostProcessStage.js 源码:
    • 第 460 行:每帧取值走 stage._actualUniforms[name];
    • 第 499 行:actualUniforms[name] = uniforms[name],在 createUniformMap() 里只写一次;
    • 第 479 行:if (defined(stage._uniformMap)) return; → map 一旦建立就永久缓存。

我原来把强度直接写进原始对象(handle.uniforms.effectStrength = …),它绕过了 Cesium 的 setter,于是快照永远是初值 0 —— shader 里 alpha = 0,画面什么都没有。 而所有读数都说"正常":strength: 1、data-map-fog-strength="1"、stage.enabled=true、 ranOnce=true。这就是这个 bug 的可怕之处:内部状态全对,只有画面对不上。

实证(同一页面、同一时刻,改回常量 vs 改回函数两种写法对照):

| 写法 | 服务层读数 / dataset | Cesium 真正喂给 shader | 画面 | | --- | --- | --- | --- | | 常量(修复前) | strength: 1、hostStrength="1" | deliveredToShader: 0(storedKind: "number") | 无雾 | | 函数(修复后) | strength: 1 | deliveredToShader: 1、matches: true(storedKind: "function") | 有雾 |

修复:uniform 一律用 () => state.xxx 函数形式(每帧现取),动态值存在独立的 state 对象里 —— 这也正是可丢弃原型能跑通的原因(它用的就是函数形式)。 同时留了一个可在运行期直接证伪的诊断口 getFogState().uniformDiagnostics (读 _uniformMap 那条每帧真正取值的路径,而不是 _uniforms 原始对象 —— 读后者只会自证清白), 并加静态守卫:uniforms 每个动态字段必须是 () => … 形式(变异测试确认能抓住这个 bug)。

  1. null 被当成 0:内置默认里 fadeInMs/fadeOutMs 是 null("本部署没配"), 而 Number(null) === 0、且 min 允许 0 → 静默变成"0 秒渐变"(雾突然出现/突然消失)。 逻辑闸门当时只喂 {}(字段 undefined)所以漏掉。已修:readNumber 显式挡 null/''; 闸门补上 null/''/显式 0 三种形态。
  2. 待命被报成失败:stage.ready 只在处理级被 enabled 的那次 update() 里才置 true (源码:构造时 _ready=false)。第一版拿它当"能不能用",于是区外待命读数一直是 failed; 更糟的是 ready 会随渲染活动回落 —— 标签页一隐藏就再变 false,读数在「正常 ↔ 失败」之间闪。 已修:failed 只由确凿证据(创建抛错)判定;另外单独报 ranOnce(是否曾跑通过)供排查。
  3. 属性名撞车:chip 原本也用 data-map-fog,与地图宿主的运行期读数同名 —— querySelector('[data-map-fog]') 会命中宿主(整页文本)而不是芯片,断言读到错误元素。 本仓在票 07(data-map-blocked-backend 同名两处)栽过一次。已改为 data-fog-chip / data-fog-state, 并加静态守卫禁止外壳再用 data-map-fog* 命名(守卫已用变异确认有牙)。

顺带修掉的两处并发会话遗留(非本票改动,但挡住了本票的闸门)

  • stores/eventStore.js:JSDoc 缺了开头 /** 那一行,留下一个孤立 * → 整个模块语法错误, t13/t14 直接加载失败。已补回注释开头(内容按上下文如实重建)。
  • stores/eventStore.js:useMapStore 用了但没 import → t14 运行期 ReferenceError。已补 import (mapStore.js 不 import 任何 store,无环;另三个 store 早已用同一模式)。
  • verify/t18-monitoring-panels.mjs:两条断言钉在会漂的写法上—— A3 硬编码 implementedPanelIds().length === 8(票 19 接入后变 11), H9 精确匹配 v-if="hasPanelView(panelId)"(票 19 在同一行尾追加了合法路由条件)。 两处都改成按不变式断言(成员资格 / 前缀匹配),既不漏守也不误伤并行票。

维护者真机复核后的两处报障(2026-09-29,已修)

维护者在真实画面走查后反馈两条,同一个根因:

  1. 进入时最后一小段时间画面会变绿;
  2. 退出时雾气消散太慢,会出现整个地球变白一小段时间。

根因(算术可证):雾的不透明度按「相机到片元的世界射线距离」算,端点 18/42 km。 但相机在高空时,画面上每一个片元都远在 42 km 之外:

相机高度 画面片元距离范围 中心片元不透明度 观感
9.8 km(区内) 9.8–23 km 0.000 正常
20 km 20–47 km 0.060 中心开始解冻
30 km 30–71 km 0.818 中心也发白
≥42 km ≥42 km 1.000 整屏全白

于是:退出时相机从 9.8 km 升向 20,000 km 全景,穿过 42 km 后整屏被涂成雾色(#bdcdd6)="整个地球变白"; 进入时反过来,高空整屏白、降到 42 km 以下时画面中心先解冻、露出一团绿色植被影像 (实测该区域影像 G 通道比 R/B 高约 9.5)="最后一段突然变绿"。 强度只随时间推进,从未随高度收口。

修复:在写入引擎的强度上再乘一层相机高度系数(domain/fog.js 的 fogAltitudeFactor): ≤20 km 为 1、≥42 km 为 0、中间平滑。两端取值有依据、不是拍脑袋 —— altitudeZeroM 取雾自己的全遮蔽端点 42 km(相机高于它时画面上没有任何片元还在保护带内, 雾已无保护意义、只会糊住地球);altitudeFullM 取 20 km(低于它恢复满强度, 而归位视角约 9.8 km 远在下方 → 已验收的区内观感完全不受影响,已用断言钉死)。

真机验证(锁定态下扫描高度):

相机高度 状态机强度 写入 GPU 的强度
9.8 km(区内) 1 1.00(不受影响 ✓)
15 km 1 1.00
25 km 1 0.87
30 km 1 0.57
42 km 1 0(不再全白 ✓)
20,000 km 1 0(不再全白 ✓)

进入(下降)路径同样验证:全景/60 km/42 km 时雾=0 且影像不揭示;30 km 时雾 0.57; 20 km 时雾到 1;全程状态机保持干净(不被污染,返回区内时时序仍正确)。

一处重要连带修正:交接闸门原先读的是状态机强度,那是错的 —— 相机在 60 km 高空(高度系数 0、画面毫无雾)时会被判成"保护成立"而揭示精细层、当场露边。 已改为读有效强度(fogEffectiveStrength(),含高度收口),并加静态守卫 + 变异测试钉死 (把闸门改回状态机强度会被打红)。

为什么不能让状态机也收口:状态机只管进出时序,把高度混进去会让"退出→再进入"的 时间轴错位;两层分开后,恢复时状态机仍是对的。

新增守卫:t38-fog.mjs 增补高度系数一节(端点、区内 9.95 km 必为 1、单调性、 写反退化恒 1、非法高度保守取 1)+ 服务层接线断言(有效强度必须真的乘上系数、 闸门必须用有效强度、调用点数量受控)。6 处定向变异全部打红(含"完全不收口"与 "闸门用状态机强度"两个正是本次报障的形态)。全量回归 27 个闸门全绿。

仍未解决(如实记录):退出时影像收起的时长仍是固定的 700 ms,而镜头阻尼飞行 在 ~33 ms 内就到了 4253 km —— 两者不同步(决策 08 要求「镜头高度与雾同步」)。 本次的高度收口让雾不再变白,但影像 alpha 仍按固定时长慢慢退。 若维护者仍觉得退出时画面奇怪,下一步应把交接的渐隐也改成按高度驱动(而不是固定 ms)。

维护者第二次现场复核(2026-09-29,两处新反馈,已修)

维护者复核第一版高度收口后,指出修复本身引入了新问题,并给出了准确的诊断:

  1. 进入时最后一小段时间画面会变绿是因为雾散的太快了,粗糙地球贴图出现了但是精细地形 还未出现,雾的渐出与场景的渐入没有很好的衔接上;
  2. 现在修改过后,雾气产生和消散的速度又有一点太快了,几乎一瞬间就产生或消失了, 返回全球的手感体感效果还不错,但是退出速度有一点太快了。

反馈 2「几乎一瞬间」——第一版的真实缺陷(我上一轮判断错了)

第一版把瞬时高度系数直接乘进强度。这里有个量级问题,我上一轮只算了"高度系数从 0 到 1 需要多少毫秒",没算相机在雾带里待多久:

量 值
返回全球的 range 跨度 12,000 m → 20,000,000 m(跨 1.7e6 倍)
雾带(altitudeFullM→altitudeZeroM) 20 → 42 km,占上面跨度的 0.0001%
阻尼(τ=110 ms)下穿越整条雾带的耗时 0.15 ms

也就是说:任何"瞬时高度驱动"都必然跳变,与缓动、时长、阻尼都无关 —— 相机开始移动的头一帧就越过了整条带。所以"雾几乎一瞬间就产生或消失"。

修法:给高度系数加时间限速(domain/fog.js 的 approachAltitudeFactor + FOG_DEFAULTS.altitudeSlewMs = 400)。它把"看起来的变化速度"变成结构性保证: 系数每帧最多走 dt / altitudeSlewMs,所以无论相机飞多快,画面上的雾都不可能比这更快地变。 雾的浓淡由时间决定,高度只用来决定"这一刻该不该有雾"。 单帧 dt 另夹 120 ms(切标签页回来不许一步走完)。

反馈 2「退出速度太快」& 第一版的「整球发白」

退出用的是阻尼逼近 range = 20,000,000。阻尼是 1 − e^(−dt/τ) 的指数逼近, 起步最快 —— 头一帧就已上万公里。所以退出"太快",而且那一刻雾还满着 ⇒ 整球发白。

修法:返回全球改走按时长 + 慢起缓动的飞行(当时取 RETURN_FLIGHT_MS = 2000、 returnFlightProgress = smoothstep(t)⁷)。指数 7 是反解出来的,不是随手取的 —— 它由一条不变量决定:

雾彻底淡完的那一刻,相机必须仍在 altitudeZeroM(42 km)以下。 相机一旦高过 42 km,画面上每个片元都超出全遮蔽端点;那一刻只要雾还没散完, 整个地球就被涂成雾色 —— 这正是"整个地球变白"。

反解:雾淡完于 handover.fadeMs + fog.fadeOutMs = 380 + 400 = 780 ms, 此时飞行进度 t = 780/2000 = 0.39、smoothstep(t) = 0.3507; 允许的最大进度 (42 km / sin55° − 12 km) / (20,000 km − 12 km) ≈ 1.97e-3; 解 0.3507^p ≤ 1.97e-3 得 p ≥ 6.8 → 取 7。 实算:p=7 时雾淡完那一刻相机在 18.0 km(再过 101 ms 才穿过 42 km); 若按原先的 smoothstep³,那一刻已在 1062 km —— 照样整球发白。

⚠️ 上面这段推导只在"代码默认时长"下成立,已被后来的现场调定取代。 维护者 2026-09-30 在真机上把三个时长改成 RETURN_FLIGHT_MS = 1000 / handover.fadeMs = 500 / fog.fadeOutMs = 300(并把缓动改成 smoothstep^6.5 混 0.1% 线性项), 明确裁定以现场观感为准 —— 该组合下"雾先淡完"这条约束被有意放宽 (相机约 413 ms 越过 42 km,雾到 800 ms 才淡完,窗口 ≈387 ms,观感已接受)。 因此它不再是硬闸门:t39-handover.mjs 第 12 节改为"把窗口算出来并钉住已接受上限(400 ms)", 超过就红、逼一次确认。详见票 39 的「维护者真机调定与收口」。

反馈 1「变绿」——诊断比我上一轮更准确

维护者的判断是对的,我上一轮把"变绿"归因于"高度收口让雾跳变",那只是表象。 真正原因是几何:雾是距离雾、18 km 起步,而区内相机只有约 9.8 km 高 ——

地面位置 视线距离 雾遮蔽率
脚下(屏幕中心) 9.8 km 0.000
水平 15 km 17.9 km 0.000
水平 25 km 26.9 km 0.634
水平 42.5 km 43.6 km 1.000

雾在结构上盖不住屏幕中心。所以中心露出粗底图还是精细影像,只看精细层 alpha; 渐显期间中心是"粗糙 Natural Earth II(陆地偏绿)↔ 精细影像"的混合, 混合窗口越长,那段绿越明显 —— 维护者说的"雾的渐出与场景的渐入没有衔接上"正是它。

修法(维护者选定"缩短精细影像渐显时长"):handover.fadeMs 700 → 380 ms, 让混合一闪而过。没有去调雾的强度 —— 因为按上表,调强度对屏幕中心根本无效。

一并按维护者现场值调校

  • 第一轮:fog.fadeInMs 900 → 800、fog.fadeOutMs 800 → 400(维护者原话"先尝试淡入 0.8,淡出 0.4")。
  • 退出飞行时长做成可现场调:DEV 把手 __cockpitMap.setReturnFlightMs(ms)。
  • 第二轮(2026-09-30,最终值,以维护者为准):fadeInMs = 900、fadeOutMs = 300、 altitudeFullM = 14000、altitudeZeroM = 3000000、altitudeSlewMs = 600。 代码默认值(FOG_DEFAULTS:800/400、20000/42000、400)保持不变, 供没有现场值的部署沿用;两者差异见部署手册 2.11。

新增守卫与变异

  • t38-fog.mjs:高度系数限速一节(单帧步长恰为 dt/slew、1 ms 内几乎不动、 走完全程 ≥ altitudeSlewMs、超大 dt 被夹 120 ms、slew=0 立即跟到、结果恒在 [0,1]、可配且脏值退回默认); 并把"限速必须真的挂在渲染帧上"钉死(这一条是变异测试抓出来的: 第一版只断言"函数内部调了限速函数",插一句提前 return 就能骗过它 —— 已补"渲染帧必须调用它"+"先推进再比较")。
  • t39-handover.mjs:第 12 节跨模块"退出发白窗口"(见上方说明,已从硬断言改为钉住已接受上限)。
  • cockpitConfig.js:新增 altitudeZeroM > altitudeFullM 的配置守卫 —— 写反会让高度系数恒为 1(等于关掉这层保护),只报 issue 不替改值(两个数各自都合法)。
  • 定向变异 10/10 全部打红(不限速、步长放大 100 倍、去掉 dt 上限、退回阻尼、 不再慢起、时长缩到 300 ms、雾淡出改回 800 ms、影像收起改回 900 ms、 写入改回瞬时系数、限速不接渲染帧);改写后的第 12 节另跑 6/6。

最终状态(2026-09-30)

维护者在真机上完成全部时长/阈值调试并裁定"目前的效果就行""以我为准"。 本票的代码侧(限速、慢起飞行、几何根因、配置守卫)已全部落地并有闸门/变异证据; 部署侧时长是维护者的现场取值,不写进代码默认。 node --check 全通过、两个配置文件 JSON 合法、归一化 issues 为空、全量闸门 29 通过 (2 个红是并发会话的 cockpitApi.js / deviceAssets.js,与本票无关)。

环境限制(如实记录)

  • 本会话浏览器标签页始终 document.hidden=true:rAF 被冻结(实测 1 秒 0 帧)。 因此强度推进会停在中间值(时间轴推进需一帧才追上)。
  • 但验证并未因此停摆:已加 DEV 把手 __cockpitMap.forceRender()(转调 scene.render()), 可显式驱动一帧;配合 immediate 落位,就能在隐藏标签页里稳定复现"强度 1、stage 已启用"的状态, 并读到 uniformDiagnostics —— 根因就是这么定位并证伪/证实的(见上方第 0 项对照表), 性能读数(见「性能读数」一节)也由它取到。
  • 仍然受限的是:自然渲染循环下的帧采样(隐藏页 postRender 不触发,count: 0) 与观感目视 —— 两者分别由上面的替代口径与维护者本人覆盖。

下一步

38 已验收关闭。39(粗→精影像交接)解除阻塞,它消费本票的雾能力读数: getFogState() 的 executable(是否可执行)/ strength(当前强度)/ degrade(降级原因), 以及 params.startDistance/endDistance(保护成立的距离带)。 注意口径:规格与决策 07 反复强调「就绪 ≠ 边缘已遮住」,且本票读数里 coversEdges 恒为 false ——39 不得拿 executable 或 strength 当"可以揭示精细影像"的充分条件, 那是它自己要按画面风险判断的事(决策 08 的三条反馈:进出稍快、冷白灰不发绿、返回时高度与雾同步)。