fix(viewer): wall-clock scene-ready cap + skip unreachable dirty nodes (#484)
Two readiness fixes surfaced by the first successful headless bake of a heavy scene (which exported with 33 items missing): - The 180-FRAME give-up cap silently assumed display-rate frames. The bake page's timer-driven loop (?disable=draw) runs those frames in ~3.6s — shorter than a cold item-model download — so the cap fired and the export raced ahead of real content. New `sceneReadyMaxWaitMs` Viewer prop replaces the frame cap with a wall-clock one on hosts whose frame cadence is decoupled from time; default frame behavior unchanged for interactive surfaces. - hasPendingSceneBuildWork now skips UNREACHABLE dirty nodes: orphans whose parent doesn't list them in `children` (or parentless non-roots) never render — every renderer enumerates the parent's children array — so no system ever builds them or clears their mark. Observed in prod scene data (two dangling windows parented to a level that doesn't list them): they held every bake of that scene to the full readiness cap. Validated: the 200+-item office scene with ?disable=draw settles organically in ~2s and exports the complete artifact; a one-shot 504'd item is now WAITED for through its retry instead of racing the cap. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
aa30899090
commit
5e5b62b09e
@@ -249,11 +249,22 @@ function ToneMappingExposure() {
|
||||
}
|
||||
|
||||
function hasPendingSceneBuildWork() {
|
||||
const { dirtyNodes, nodes } = useScene.getState()
|
||||
const { dirtyNodes, nodes, rootNodeIds } = useScene.getState()
|
||||
|
||||
for (const id of dirtyNodes) {
|
||||
const node = nodes[id]
|
||||
if (!node) continue
|
||||
// Unreachable nodes (orphaned by a broken detach: the parent doesn't list
|
||||
// them in `children`, or no parent and not a root) never render — every
|
||||
// renderer enumerates the parent's `children` array — so no system will
|
||||
// ever build them or clear their mark. They must not hold scene-ready
|
||||
// hostage (observed in prod scene data: two dangling windows kept every
|
||||
// bake of that scene waiting out the full readiness cap).
|
||||
const parent = node.parentId ? nodes[node.parentId as AnyNodeId] : undefined
|
||||
const reachable = parent
|
||||
? (parent as { children?: string[] }).children?.includes(id) === true
|
||||
: rootNodeIds.includes(id)
|
||||
if (!reachable) continue
|
||||
const def = nodeRegistry.get(node.type)
|
||||
if (def?.geometry || def?.capabilities?.floorPlaced || DIRTY_BUILD_KINDS.has(node.type)) {
|
||||
return true
|
||||
@@ -272,13 +283,16 @@ function hasCommittedSceneRoot() {
|
||||
function SceneReadyTracker({
|
||||
onSceneReadyChange,
|
||||
sceneReadyKey,
|
||||
sceneReadyMaxWaitMs,
|
||||
}: {
|
||||
onSceneReadyChange?: (ready: boolean) => void
|
||||
sceneReadyKey?: string | number | null
|
||||
sceneReadyMaxWaitMs?: number
|
||||
}) {
|
||||
const readyRef = useRef(false)
|
||||
const settledFramesRef = useRef(0)
|
||||
const waitedFramesRef = useRef(0)
|
||||
const waitStartRef = useRef<number | null>(null)
|
||||
const onSceneReadyChangeRef = useRef(onSceneReadyChange)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -290,6 +304,7 @@ function SceneReadyTracker({
|
||||
readyRef.current = false
|
||||
settledFramesRef.current = 0
|
||||
waitedFramesRef.current = 0
|
||||
waitStartRef.current = null
|
||||
onSceneReadyChangeRef.current?.(false)
|
||||
}, [sceneReadyKey])
|
||||
|
||||
@@ -297,10 +312,16 @@ function SceneReadyTracker({
|
||||
if (!(onSceneReadyChangeRef.current && !readyRef.current)) return
|
||||
|
||||
waitedFramesRef.current += 1
|
||||
if (
|
||||
waitedFramesRef.current < SCENE_READY_MAX_WAIT_FRAMES &&
|
||||
(!hasCommittedSceneRoot() || hasPendingSceneBuildWork())
|
||||
) {
|
||||
waitStartRef.current ??= performance.now()
|
||||
// Give-up cap so a permanently-dirty node can't block readiness forever.
|
||||
// The frame-count default assumes display-rate frames; a host whose frame
|
||||
// cadence is decoupled from wall time (the headless bake page's timer-driven
|
||||
// loop runs 180 frames in 3.6s — faster than a cold item download) passes
|
||||
// `sceneReadyMaxWaitMs` to make the cap wall-clock instead.
|
||||
const capReached = sceneReadyMaxWaitMs
|
||||
? performance.now() - waitStartRef.current >= sceneReadyMaxWaitMs
|
||||
: waitedFramesRef.current >= SCENE_READY_MAX_WAIT_FRAMES
|
||||
if (!capReached && (!hasCommittedSceneRoot() || hasPendingSceneBuildWork())) {
|
||||
settledFramesRef.current = 0
|
||||
return
|
||||
}
|
||||
@@ -345,6 +366,13 @@ interface ViewerProps {
|
||||
*/
|
||||
sceneReadyKey?: string | number | null
|
||||
onSceneReadyChange?: (ready: boolean) => void
|
||||
/**
|
||||
* Wall-clock give-up cap for scene readiness, replacing the default
|
||||
* frame-count cap. Set it on hosts whose frame cadence is decoupled from
|
||||
* real time (the headless bake page's timer-driven loop runs the default
|
||||
* 180-frame cap in ~3.6s — shorter than a cold item-model download).
|
||||
*/
|
||||
sceneReadyMaxWaitMs?: number
|
||||
/**
|
||||
* Skip the TSL post-processing pipeline (SSGI/denoise/ink/outline) and render
|
||||
* the scene directly. For headless/capture surfaces (the bake page) where
|
||||
@@ -379,6 +407,7 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
||||
isolate,
|
||||
sceneReadyKey,
|
||||
onSceneReadyChange,
|
||||
sceneReadyMaxWaitMs,
|
||||
disablePostFx = false,
|
||||
},
|
||||
ref,
|
||||
@@ -527,7 +556,11 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
||||
<ViewerCamera />
|
||||
<GPUDeviceWatcher />
|
||||
<ToneMappingExposure />
|
||||
<SceneReadyTracker onSceneReadyChange={onSceneReadyChange} sceneReadyKey={sceneReadyKey} />
|
||||
<SceneReadyTracker
|
||||
onSceneReadyChange={onSceneReadyChange}
|
||||
sceneReadyKey={sceneReadyKey}
|
||||
sceneReadyMaxWaitMs={sceneReadyMaxWaitMs}
|
||||
/>
|
||||
|
||||
<ErrorBoundary fallback={null} scope="viewer-scene">
|
||||
{/* <directionalLight position={[10, 10, 5]} intensity={0.5} castShadow
|
||||
|
||||
Reference in New Issue
Block a user