Skip to content

fix(viewer): wall-clock scene-ready cap + skip unreachable dirty nodes#484

Merged
wass08 merged 1 commit into
mainfrom
fix/scene-ready-time-cap
Jul 10, 2026
Merged

fix(viewer): wall-clock scene-ready cap + skip unreachable dirty nodes#484
wass08 merged 1 commit into
mainfrom
fix/scene-ready-time-cap

Conversation

@wass08

@wass08 wass08 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Two readiness fixes surfaced by the first successful headless CPU-only bake (which completed in 80s — but exported with 33 items missing, caught by the new skippedItems metadata):

  1. sceneReadyMaxWaitMs Viewer prop — the 180-frame give-up cap silently assumed display-rate frames (~3s interactive). The bake page's timer loop (?disable=draw, fix(viewer): drive the frame loop with a timer when ?disable=draw #483) runs 180 frames in 3.6s of wall time — shorter than a cold item download, so the cap fired and the export raced ahead of content. The prop replaces the frame cap with a wall-clock one; hosts that don't pass it keep the exact current behavior.
  2. Unreachable dirty nodes no longer block readiness — found real prod scene data with two windows whose parentId points at a level that doesn't list them in children (dangling nodes from some old detach flow). Nothing ever renders/builds an unreachable node, so its dirty mark lives forever and held every bake of that scene to the full cap. hasPendingSceneBuildWork now skips nodes not reachable via their parent's children array (or roots).

How to test

  1. bun dev, open /bake/demo_1?disable=postFx,draw — resolves fast, as before.
  2. Editor surfaces: unchanged (no sceneReadyMaxWaitMs passed → same 180-frame cap).
  3. Validated on the 200+-item office scene (?disable=postFx,draw + the companion bake-page change): settles organically in ~2s, exports the complete 35MB artifact, zero skipped items; with an item URL 504'd once, the bake now waits through the retry and includes the item.

Screenshots / screen recording

N/A — readiness timing behavior.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 Generated with Claude Code


Note

Medium Risk
Changes when hosts signal scene-ready (export/capture timing) and ignore dirty marks on graph-unreachable nodes; wrong reachability logic could declare ready while real content is still building, though the existing settled-frame and cap behavior remains.

Overview
Fixes scene readiness getting stuck or firing too early during headless bakes and on scenes with broken graph links.

Unreachable dirty nodes no longer count as pending build work: hasPendingSceneBuildWork treats a node as reachable only if its parent lists it in children or it is in rootNodeIds, so orphaned dirty nodes cannot block onSceneReadyChange until the give-up cap.

Optional wall-clock give-up via new Viewer prop sceneReadyMaxWaitMs: when set, SceneReadyTracker uses elapsed time instead of the default 180-frame cap—intended for timer-driven hosts (e.g. bake page) where 180 frames is shorter in wall time than slow async work like item downloads. Hosts that omit the prop keep the existing frame-based cap.

Reviewed by Cursor Bugbot for commit 00459d0. Bugbot is set up for automated code reviews on this repo. Configure here.

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>
@wass08 wass08 merged commit 5e5b62b into main Jul 10, 2026
2 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 00459d0. Configure here.

const reachable = parent
? (parent as { children?: string[] }).children?.includes(id) === true
: rootNodeIds.includes(id)
if (!reachable) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roots ignored for reachability

Medium Severity

hasPendingSceneBuildWork treats a dirty node as unreachable whenever its parentId resolves but that parent’s children omits the id, without also checking rootNodeIds. SceneRenderer still mounts ids listed at the root, so pending build work on those nodes can be ignored and onSceneReadyChange(true) may fire before geometry finishes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 00459d0. Configure here.

// `sceneReadyMaxWaitMs` to make the cap wall-clock instead.
const capReached = sceneReadyMaxWaitMs
? performance.now() - waitStartRef.current >= sceneReadyMaxWaitMs
: waitedFramesRef.current >= SCENE_READY_MAX_WAIT_FRAMES

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zero max wait ignored

Low Severity

capReached branches on truthy sceneReadyMaxWaitMs, so passing 0 does not select the wall-clock path and falls back to the 180-frame cap. A host that sets sceneReadyMaxWaitMs={0} expecting an immediate give-up gets editor-style frame timing instead.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 00459d0. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant