Skip to content

fix(extract): incremental delta() misses active-child changes (Switch.whichChoice, LOD level)#32

Merged
delta9000 merged 2 commits into
mainfrom
fix/incremental-delta-active-child
Jun 28, 2026
Merged

fix(extract): incremental delta() misses active-child changes (Switch.whichChoice, LOD level)#32
delta9000 merged 2 commits into
mainfrom
fix/incremental-delta-active-child

Conversation

@delta9000

Copy link
Copy Markdown
Owner

Summary

A bug class in the incremental SceneExtractor::delta() channel: a scene change updates correctly for full-snapshot consumers (cpuraster re-extracts every frame) but is invisible to incremental delta() consumers (the OpenGL PoC), which render stale state. delta() only re-walks a subtree on DirtyChildren; a change is missed when it alters rendering but sits on a grouping node in neither geomDeps_ nor materialDeps_ and isn't classified DirtyChildren.

Found by differential testing: render the same animated scene through poc --animate (incremental delta) and cpuraster --animate (full-snapshot oracle); where the oracle animates but the PoC is frozen → bug.

Two members fixed

Member Why missed Fix
Switch.whichChoice settable field classified DirtyField classifyDirty maps whichChoiceDirtyChildren
LOD level (camera motion) level is computed from the camera, not a settable field, so it never reaches classifyDirty (the cascade field-observer only fires for fields with a setter; level_changed is outputOnly) ViewDependentSystem calls new X3DExecutionContext::markActiveChildChanged() on a level flip

Regression tests: scene_extractor_t8_test.cpp cases 5 (Switch flip 0→1→−1) and 6 (LOD Viewpoint d=10→d=1 swap).

Also

  • Time-origin contract documented (system-time.md + finding TIME-ORIGIN-1): the SDK never bakes a time origin — tick(now) lets the consumer choose. App-relative feeding (both renderers) gives Castle Engine's timeOriginAtLoad behaviour by default; strict epoch feeding gives literal SFTime semantics.
  • Findings: SW-DELTA-1, LOD-DELTA-1 (+ the stale 'Switch audited clean' note corrected to scope it to full-snapshot).

Known residual (deferred, documented in LOD-DELTA-1)

LOD under an animated parent transform or multi-path USE stays stale in delta()ViewDependentSystem's per-node level uses the first-path/identity transform (M2C-1); the accurate level is per-path in the extractor walk. Same per-path view-dependent gap as Billboard orientation (already deferred).

Cleared by the harness (not bugs): Material color/transparency, light intensity, Coordinate morph, nested Switch.

Split out of #31 (OpenGL renderer work); these are pure SDK fixes with no dependency on the poc changes.

classifyDirty mapped a Switch.whichChoice change to DirtyField, but a Switch is in
neither geomDeps_ nor materialDeps_, so SceneExtractor::delta() ignored it — no
subtree re-walk. Incremental consumers (the OpenGL PoC) never saw the active-child
swap; only full-snapshot consumers (cpuraster, which re-extracts every frame) did.
The extractor already reads whichChoice on a full walk, so only the INCREMENTAL
channel was affected — the prior 'Switch audited clean' note covered fullSnapshot.

Fix: classifyDirty maps whichChoice -> DirtyChildren, so delta() re-walks the Switch
subtree and emits removed(old child)+added(new child). Found via a Switch +
IntegerSequencer demo rendered through the PoC's delta() path.

- runtime/events/X3DExecutionContext.hpp: whichChoice -> DirtyChildren
- scene_extractor_t8_test.cpp: case 5 (flip 0->1->-1)
- findings.yaml: SW-DELTA-1 (fixed); TIME-ORIGIN-1 documents the app-relative
  tick(now) time-origin contract (= Castle's timeOriginAtLoad by default)
- system-time.md: 'Time origin' section
…ra motion)

Sibling of SW-DELTA-1 for VIEW-DEPENDENT (vs settable-field) active-child
selection, found by differential testing the OpenGL PoC (incremental delta())
against cpuraster (full snapshot): a moving viewer over a static LOD swapped the
level in cpuraster but stayed frozen in the PoC. The rendered LOD level is
computed from the camera, not a settable field, so it never reaches classifyDirty.

Fix: ViewDependentSystem calls the new X3DExecutionContext::markActiveChildChanged
when an LOD's announced level flips, so delta() re-walks the LOD subtree and swaps
the active child.

- X3DExecutionContext.hpp: markActiveChildChanged(node) -> DirtyChildren|DirtyBounds
- ViewDependentSystem.hpp: mark on level flip
- scene_extractor_t8_test.cpp: case 6 (move Viewpoint d=10 -> d=1 -> swap)
- findings.yaml: LOD-DELTA-1 (camera-motion fixed; LOD under an animated parent
  transform / multi-path USE remains per-path-deferred, same root as Billboard)
@delta9000 delta9000 merged commit 31d7001 into main Jun 28, 2026
12 of 14 checks passed
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