Ver código fonte

fix(cockpit): 分离细模缓存保留与开装门槛

真实页面复验发现已就绪缓存因canStart暂时关闭被清除。缓存改由独立canRetain判断,Cesium适配层明确父对象仍存活且可见;新开装仍使用原门槛,预算、LOD和并发不变。

补充开装暂停但父仍有效、缓存回用、新区域仍阻塞及真正父失效释放回归,独立HEAD先红后绿,相关22+3回归通过。真实320/1200/320机位中69号缓存保留并回用,首内容时间未重置;返回全球释放为0。

仅完成功能复验,不把即时落位的0运动样本当作FPS提升证据;目标低配机整体性能仍待确认。关联 #59。
ChenJZ 3 dias atrás
pai
commit
443a4d3593

+ 1 - 0
src/modules/cockpit/services/cesiumEngine.js

@@ -768,6 +768,7 @@ function createTilesetCullingController(scene, plans, instances, failed, onChang
         const localInstances = []
         let cullingVolume, coarseRendered = false
         const region = createRegionLoadingController({ regions,
+            canRetain: () => !model.isDestroyed() && model.show,
             canStart: () => coarseRendered && model.tilesLoaded && model.show && !model.isDestroyed(),
             beforeRefresh: () => { cullingVolume = camera.frustum.computeCullingVolume(camera.positionWC, camera.directionWC, camera.upWC) },
             onFailure: (r, error) => { failed.push({ url: error.url || r.url, error: String(error.message || error) }); onChange?.({ ready: instances.length, instances, failed }) },

+ 5 - 3
src/modules/cockpit/services/regionLoading.js

@@ -169,6 +169,8 @@ export function contentAdmission(spheres, visible, centerDistance) {
 export function createRegionLoadingController({
     regions, load, add, remove, distance,
     beforeRefresh = () => {}, canStart = () => true, requestRender = () => {}, onFailure = () => {},
+    // Cache validity is independent of a temporary pause in new load admission.
+    canRetain = () => true,
     now = () => performance.now(), setTimer = setTimeout, clearTimer = clearTimeout,
     frontier = null, budget = null, movingWindowMs = DETAIL_FRONTIER_DEFAULTS.moveWindowMs,
     // 相机位姿指纹(入射方给:cesiumEngine 有 camera 对象)。缺省时退回"区域距离指纹",
@@ -484,13 +486,13 @@ export function createRegionLoadingController({
         }
         const wanted = desired()
         const at = now(), moving = at - lastMoveAt < movingWindowMs
-        const parentReady = !pipeline && canStart()
-        const keepReady = parentReady && (moving || wanted.length > 0)
+        const cacheAllowed = !pipeline && canRetain()
+        const keepReady = cacheAllowed && (moving || wanted.length > 0)
         for (const e of entries) {
             const needed = wanted.includes(e.id) && e.phase !== 'failed'
             if (needed) {
                 e.lastWantedAt = at
-                if (e.cached && parentReady) {
+                if (e.cached && cacheAllowed) {
                     e.cached = false; e.instance.preloadWhenHidden = true
                     // Hidden preload must update the new view before old tilesLoaded can be trusted.
                     e.awaitingRender = true

+ 18 - 2
tools/city-massing/verify-region-residency.mjs

@@ -43,12 +43,13 @@ try {
 async function scene(options, run) {
     const costs = options.costs || [1, 1, 1]
     const budget = options.budget || { maxContents: 2, maxDecodedBytes: 2048 }
-    let time = 0, pose = 0, visible = [0], parentReady = true, renderRequested = false
+    let time = 0, pose = 0, visible = [0], parentReady = true, startAllowed = true, renderRequested = false
     const loaded = [], added = [], disposed = [], byId = new Map(), waiting = new Map()
     const control = createRegionLoadingController({
         regions: costs.map((cost, id) => ({ id, contentCount: cost, decodedGeometryBytes: cost * 1024 })),
         budget, distance: r => visible.includes(r.id) ? r.id + 1 : Infinity,
-        readCameraKey: () => String(pose), now: () => time, canStart: () => parentReady,
+        readCameraKey: () => String(pose), now: () => time, canStart: () => parentReady && startAllowed,
+        canRetain: () => parentReady,
         coverageReady: e => e.instance.covered,
         load: r => {
             loaded.push(r.id)
@@ -69,6 +70,7 @@ async function scene(options, run) {
         async resolve(id) { waiting.get(id).resolve(); waiting.delete(id); await flush() },
         async reject(id) { waiting.get(id).reject(new Error('load rejected')); waiting.delete(id); await flush() },
         parent(value) { parentReady = value; control.refresh() },
+        allowStarts(value) { startAllowed = value; control.refresh() },
         renderPending: () => renderRequested,
         clearRenderRequest() { renderRequested = false },
         checkBudget() {
@@ -205,4 +207,18 @@ await scene({ deferredIds: [1] }, async h => {
     assert.deepEqual(h.disposed.slice().sort(), [0, 1], 'Cache and late model are each released once')
     assert.equal(h.control.snapshot().length, 0)
 })
+await scene({}, async h => {
+    await h.view([1])
+    const a = h.byId.get(0)
+    assert.equal(h.control.snapshot().find(e => e.id === 0).cached, true)
+    h.allowStarts(false) // parent is alive/visible, but temporarily not ready for new loads.
+    assert.deepEqual(h.disposed.slice(), [], 'A closed start gate must not invalidate a ready cache')
+    await h.view([0])
+    assert.deepEqual(h.loaded, [0, 1], 'Valid cached A can return while new starts are paused')
+    assert.equal(h.byId.get(0), a)
+    await h.view([2])
+    assert.deepEqual(h.loaded, [0, 1], 'The independent start gate still blocks a new region')
+    h.parent(false)
+    assert.equal(h.control.snapshot().length, 0, 'A genuinely invalid parent releases caches')
+})
 console.log('PASS residency boundaries: hidden state, wakeup, LRU, budgets, pending reservation, failure and release')