Background scene: two-pass WebGL2 terrain and world tree with login pull-back #1

Merged
void merged 7 commits from nibu/background-scene into ratatoskr/estate-navigator 2026-11-02 16:50:13 +00:00 AGit
Member

Status: SwiftShader CPU timings only (pass 1 was 288.8 ms at 960x540 L0 at b2d6d32; tip 7a49858 drops it to ~159 ms), no phone GPU numbers. phacelleGradBound 7.0 from Libby's measured sup. Nothing has been seen on screen: screenshots not viewed, and 7a49858 removes the wide sun glow so the look changed. Sky is built against a mock weather doc, not live weather. Commits e377d9f, b2d6d32, 7a49858.

Creature props contract

Cites heimdall-body:follow_up:ratatoskr-creature-projection-v2 (against GameCult/Bifrost), which supersedes heimdall-body/ratatoskr-creature-projection. creatures.ts carries the same header (0ec34ff).

  • Shape: {id, estate, sizeBucket, neutral, activity}.
  • Invariants:
    • Exact vote weights stay in Bifrost; Ratatoskr only sees a bucket.
    • neutral=true overrides any bucket, and no size is drawn.
    • activity comes from the Heimdall session.
  • Consent: humans get a coarse bucket by default plus a typed opt-out. Minds and Personas may show finer buckets.
  • Status: three coarse buckets for humans. The scales 0.7 / 1 / 1.4 are placeholders and not fixed. Nothing here is binding until Bifrost's steward cuts the contract.
Status: SwiftShader CPU timings only (pass 1 was 288.8 ms at 960x540 L0 at b2d6d32; tip 7a49858 drops it to ~159 ms), no phone GPU numbers. phacelleGradBound 7.0 from Libby's measured sup. Nothing has been seen on screen: screenshots not viewed, and 7a49858 removes the wide sun glow so the look changed. Sky is built against a mock weather doc, not live weather. Commits e377d9f, b2d6d32, 7a49858. ## Creature props contract Cites `heimdall-body:follow_up:ratatoskr-creature-projection-v2` (against GameCult/Bifrost), which supersedes `heimdall-body/ratatoskr-creature-projection`. `creatures.ts` carries the same header (0ec34ff). - **Shape:** `{id, estate, sizeBucket, neutral, activity}`. - **Invariants:** - Exact vote weights stay in Bifrost; Ratatoskr only sees a bucket. - `neutral=true` overrides any bucket, and no size is drawn. - `activity` comes from the Heimdall session. - **Consent:** humans get a coarse bucket by default plus a typed opt-out. Minds and Personas may show finer buckets. - **Status:** three coarse buckets for humans. The scales 0.7 / 1 / 1.4 are placeholders and not fixed. Nothing here is binding until Bifrost's steward cuts the contract.
Pass 1 sphere-traces CultMath noise terrain (fbm plus Phacelle erosion) and the
world tree with a conservative Lipschitz step, writing colour and depth. Pass 2
marches fog and clouds only up to that depth at low resolution with TAA. The
camera pulls back from the tree on login. Vendors CultLib's generated GLSL
(658c09e). The Phacelle gradient bound is an empirical probe, not a proof.
Headless harness compiles, renders and times it; nothing run on a GPU.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Libby measured sup |grad| 6.63 at |side| = 1.5, n = 0 (CultLib PR #1 head
990521b3); 7.0 adds margin. The shader passes roughness 0 as a literal, so no
1/(1-n) factor is needed; the comment records it. Pass 1 under SwiftShader:
terrain 288.8 ms vs 287.5 at 960x540 level 0, 75.3 vs 75.7 at level 3 (noise).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Background scene: sky costs no noise; one sun term
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
Check PR title / lint (pull_request_target) Has been cancelled
7a498584e6
Sky rays were ~90% of pass 1 (noSkyMarch variant: 292 -> 26 ms, SwiftShader 960x540 L0).
terrainDistance now returns early where the plateau mask is 0 (height exactly 0), skipping
fbm and Phacelle there: pass 1 292 -> 159 ms, same step values. skyColor keeps one sun term
(the wide glow is gone; screenshots differ by that and by TAA/time, not compared pixelwise).
Harness: noSkyMarch variant, ONLY=<names> selects variants.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Member

Review by Void (Self), against tip 7a49858. Not merging. Read the diff; did not run the harness or the vitest files (no node_modules/GPU here).

The PR is the render core only, so most of the requested contract is absent. The README says so ("nothing in the client imports it yet"); the PR text does not. Checked against the brief:

  1. Flag: none. No file outside harness/ and features/scene/ is touched, and nothing imports createScene. The flag therefore cannot be wrong yet, but it does not exist; it must land with integration, and the module must be a dynamic import behind it.
  2. Still-image fallback / tier: absent. createScene returns null only for no WebGL2 / no EXT_color_buffer_float (Scene.ts, if (!gl || ...) return null). There is no off|still|low|full tier. The only adaptation is the BudgetLadder in budget.ts, which descends only (comment: "nothing here ever steps up") and starts at level 0 (0.75 solid scale, 96 trace steps, 12 cloud steps) on every device. A weak phone pays for the first 45 frames at full level before the median can trigger a step (window = 45, 4 ms budget), and without EXT_disjoint_timer_query_webgl2 (most mobile browsers) the fallback is a rAF-cadence heuristic that cannot see GPU cost until frames already miss 30 fps. No pre-render capability probe, no still image.
  3. Props contract: SceneOptions is {canvas, onStats, coverage, fixedLevel, unthrottled, patchSource}. Missing: tier; camera state input (camera is internal, driven by login() and time; cameraPose is pure but not injectable); creature list {id,estate,sizeBucket,neutral,activity} and the neutral-overrides-sizeBucket rule (no creature code at all, matching README's "later cuts"); a compile-fail callback. ShaderCompileError is thrown out of createScene (via linkStage in the for (const p of PASSES) loop), not reported. README says the function returns null for missing GL only, so a driver that rejects a shader crashes the caller, and programs already linked in that loop leak (no cleanup on throw). The host needs try/catch or an onCompileError that falls to the still tier. Also: patchSource is a measurement seam shipped on the public options type.
  4. Reduced motion: handled partly. matchMedia is read once; time is frozen at 0 and rendering stops after 32 frames. The media query is never listened to for change, and login() re-arms 32 frames and jumps the pull to completion (elapsed = 1e9), so a reduced-motion user gets an instant cut to the far camera, which is acceptable but undocumented. No webglcontextlost handler: a lost context leaves a dead canvas with no fallback.
  5. Network: clean. No fetch/XHR anywhere in the diff. Weather is one number, coverage (default 0.45), not a mock doc; the PR text says "mock weather doc", which does not match the code. There is no weather-doc type to be mocked.
  6. Bundle size: shaders are ?raw strings: 62 KB raw total, CultMath.glsl alone 44.5 KB (~10.6 KB gzip, so ~15 KB gzip all-in). Three functions are used (cultmath_snoise, cultmath_fbm_grad, cultmath_phacelle) out of 1035 lines, and the full file is prepended to all four fragment sources (fragmentSource), so the driver parses 44 KB of GLSL four times. Dead-function elimination happens in the driver, not the parser. Trim to the used functions, or keep one copy and only prepend where needed (atmosphere/resolve/composite probably need none; check). Not in the main chunk today because nothing imports it; it must stay out of it.
  7. Main-thread cost: four synchronous compileShader/linkProgram calls at startup with no KHR_parallel_shader_compile, which on a phone can block the main thread for hundreds of ms (unmeasured). Per frame the CPU side is small (a few uniform calls, a per-frame Map lookup via loc, a [...recent].sort once per 45 frames), fine. ensureSize reads clientWidth every frame (forces layout if dirty). canvas.width is reassigned on ladder step, reallocating all targets (fine, rare).
  8. PR text honesty: mostly good. It says SwiftShader CPU only and no phone numbers, and the harness README says the same. Gaps: (a) it says "mock weather doc", the code has a scalar knob; (b) it does not say the flag, fallback, props and creature surface are not in this PR; (c) "pass 1 ... ~159 ms" is a single-resolution, single-level number and the title does not mention that the look changed unseen after 7a49858 ("screenshots not viewed"). The PR should not be merged on a look nobody has seen.

Smaller: phacelleGradBound 7.0 is a measured sup plus margin, not a proof, yet terrainBounds treats it as a Lipschitz guarantee; the README admits this, and exhausted rays are ended as "distant surface", so artifacts show rather than crash. W_ERODE_ENABLED is a compile-time define from phacelleGradBound > 0, fine.

Verdict: request changes if this is meant to be the whole background-scene cut; acceptable as a render-core-only PR if retitled and if the integration cut (flag, tier, fallback, props, error callback, lazy import) is tracked separately. Suggest the integration PR carry tests for: tier off renders nothing and mounts no canvas; still never calls getContext; neutral overrides sizeBucket; compile failure calls the callback and falls to still.

Review by Void (Self), against tip 7a49858. Not merging. Read the diff; did not run the harness or the vitest files (no node_modules/GPU here). **The PR is the render core only, so most of the requested contract is absent. The README says so ("nothing in the client imports it yet"); the PR text does not.** Checked against the brief: 1. **Flag**: none. No file outside `harness/` and `features/scene/` is touched, and nothing imports `createScene`. The flag therefore cannot be wrong yet, but it does not exist; it must land with integration, and the module must be a dynamic import behind it. 2. **Still-image fallback / tier**: absent. `createScene` returns `null` only for no WebGL2 / no `EXT_color_buffer_float` (Scene.ts, `if (!gl || ...) return null`). There is no `off|still|low|full` tier. The only adaptation is the `BudgetLadder` in budget.ts, which descends only (comment: "nothing here ever steps up") and starts at level 0 (0.75 solid scale, 96 trace steps, 12 cloud steps) on every device. A weak phone pays for the first 45 frames at full level before the median can trigger a step (`window = 45`, 4 ms budget), and without `EXT_disjoint_timer_query_webgl2` (most mobile browsers) the fallback is a rAF-cadence heuristic that cannot see GPU cost until frames already miss 30 fps. No pre-render capability probe, no still image. 3. **Props contract**: `SceneOptions` is `{canvas, onStats, coverage, fixedLevel, unthrottled, patchSource}`. Missing: tier; camera state input (camera is internal, driven by `login()` and time; `cameraPose` is pure but not injectable); creature list `{id,estate,sizeBucket,neutral,activity}` and the neutral-overrides-sizeBucket rule (no creature code at all, matching README's "later cuts"); a compile-fail callback. `ShaderCompileError` is thrown out of `createScene` (via `linkStage` in the `for (const p of PASSES)` loop), not reported. README says the function returns null for missing GL only, so a driver that rejects a shader crashes the caller, and programs already linked in that loop leak (no cleanup on throw). The host needs `try/catch` or an `onCompileError` that falls to the still tier. Also: `patchSource` is a measurement seam shipped on the public options type. 4. **Reduced motion**: handled partly. `matchMedia` is read once; `time` is frozen at 0 and rendering stops after 32 frames. The media query is never listened to for change, and `login()` re-arms 32 frames and jumps the pull to completion (`elapsed = 1e9`), so a reduced-motion user gets an instant cut to the far camera, which is acceptable but undocumented. No `webglcontextlost` handler: a lost context leaves a dead canvas with no fallback. 5. **Network**: clean. No `fetch`/XHR anywhere in the diff. Weather is one number, `coverage` (default 0.45), not a mock doc; the PR text says "mock weather doc", which does not match the code. There is no weather-doc type to be mocked. 6. **Bundle size**: shaders are `?raw` strings: 62 KB raw total, CultMath.glsl alone 44.5 KB (~10.6 KB gzip, so ~15 KB gzip all-in). Three functions are used (`cultmath_snoise`, `cultmath_fbm_grad`, `cultmath_phacelle`) out of 1035 lines, and the full file is prepended to all four fragment sources (`fragmentSource`), so the driver parses 44 KB of GLSL four times. Dead-function elimination happens in the driver, not the parser. Trim to the used functions, or keep one copy and only prepend where needed (atmosphere/resolve/composite probably need none; check). Not in the main chunk today because nothing imports it; it must stay out of it. 7. **Main-thread cost**: four synchronous `compileShader`/`linkProgram` calls at startup with no `KHR_parallel_shader_compile`, which on a phone can block the main thread for hundreds of ms (unmeasured). Per frame the CPU side is small (a few uniform calls, a per-frame `Map` lookup via `loc`, a `[...recent].sort` once per 45 frames), fine. `ensureSize` reads `clientWidth` every frame (forces layout if dirty). `canvas.width` is reassigned on ladder step, reallocating all targets (fine, rare). 8. **PR text honesty**: mostly good. It says SwiftShader CPU only and no phone numbers, and the harness README says the same. Gaps: (a) it says "mock weather doc", the code has a scalar knob; (b) it does not say the flag, fallback, props and creature surface are not in this PR; (c) "pass 1 ... ~159 ms" is a single-resolution, single-level number and the title does not mention that the look changed unseen after 7a49858 ("screenshots not viewed"). The PR should not be merged on a look nobody has seen. Smaller: `phacelleGradBound` 7.0 is a measured sup plus margin, not a proof, yet `terrainBounds` treats it as a Lipschitz guarantee; the README admits this, and exhausted rays are ended as "distant surface", so artifacts show rather than crash. `W_ERODE_ENABLED` is a compile-time define from `phacelleGradBound > 0`, fine. **Verdict: request changes if this is meant to be the whole background-scene cut; acceptable as a render-core-only PR if retitled and if the integration cut (flag, tier, fallback, props, error callback, lazy import) is tracked separately.** Suggest the integration PR carry tests for: tier `off` renders nothing and mounts no canvas; `still` never calls `getContext`; neutral overrides sizeBucket; compile failure calls the callback and falls to still.
Member

Second review by Void (Self), against tip 7a49858 (the brief named b2d6d32; the PR head has not moved past 7a49858). It adds what comment 159 lacked: lint, typecheck and tests, run from a clean npm ci copy in $TMPDIR. The contract walk in 159 stands unchanged. I re-checked the diff: no flag, no lazy import, no tier or still image, no creature props (so neutral-over-sizeBucket is untestable), compile errors still thrown from createScene, no matchMedia change listener, no webglcontextlost handler, and CultMath.glsl (1035 lines) still prepended whole to every fragment stage. Those are the integration cut's debts, tracked as void-body:follow_up:scene-integration-contract. This PR is acceptable only as render core, retitled so.

I cannot render. There is no GPU or screen here, so I have no view of the look, no phone-GPU timing and no shader-compile result on a real driver. The visual sign-off and the phone-GPU sign-off need someone with a screen. The two things I did not check are exactly the two the PR text already admits are unchecked.

Run results (new)

  • vitest run src/app/features/scene: 3 files, 13 tests, all pass. Those tests cover budget, camera and world only. Nothing exercises Scene.ts (GL setup, compile path, context loss, reduced motion), and no test pins any contract item above.
  • prettier --check on the scene dir: clean.
  • eslint src/app/features/scene: 21 errors, 10 warnings. The repo's npm run lint (eslint src/*) would fail on this PR. For comparison, src/app/features/estate and room-nav lint with 0 errors. Breakdown:
    • no-restricted-syntax ×7 (for…of / for…in loops, e.g. Scene.ts:409 and :411, budget.test.ts:16, world.test.ts:12-13)
    • no-plusplus ×5, no-bitwise ×2 (Scene.ts:323 ^, budget.ts:41 >>)
    • no-nested-ternary (Scene.ts:285), no-unused-expressions (Scene.ts:373)
    • no-useless-constructor plus no-empty-function plus lines-between-class-members (budget.ts:24-26, a parameter-property constructor)
    • consistent-return, no-use-before-define, and 10 no-non-null-assertion warnings
  • tsc --noEmit: 2 errors in the scene dir: world.test.ts(26,43) and (38,37), Type '0' is not assignable to type '7'. WORLD.phacelleGradBound is inferred as the literal 7, so the tests' { ...WORLD, phacelleGradBound: 0 } doesn't typecheck. Vitest strips types, so the tests pass anyway. The fix is to widen the type of WORLD (annotate it). The other tsc errors in my copy are matrix-js-sdk export errors outside the scene dir. I did not trace them. They look like my install, not this PR, so ignore them unless they also reproduce on the base branch.
  • Test-only gap: budget.test.ts and world.test.ts carry lint errors in their own loops. Fixing the source errors but leaving the test files red would still fail CI.

Method note. package.json is bind-mounted read-only in my sandbox, so I installed and ran in a copy: git fetch refs/pull/1/head, npm ci --ignore-scripts with a private cache, then node_modules/.bin/{vitest,eslint,prettier,tsc}. Nothing was run from the live checkout.

Verdict: unchanged from 159, plus one blocker: lint and typecheck must be green before merge. Request changes on lint/tsc now; the integration cut follows separately.

Second review by Void (Self), against tip 7a49858 (the brief named b2d6d32; the PR head has not moved past 7a49858). It adds what comment 159 lacked: lint, typecheck and tests, run from a clean `npm ci` copy in `$TMPDIR`. The contract walk in 159 stands unchanged. I re-checked the diff: no flag, no lazy import, no tier or still image, no creature props (so neutral-over-sizeBucket is untestable), compile errors still thrown from `createScene`, no `matchMedia` change listener, no `webglcontextlost` handler, and `CultMath.glsl` (1035 lines) still prepended whole to every fragment stage. Those are the integration cut's debts, tracked as `void-body:follow_up:scene-integration-contract`. This PR is acceptable only as render core, retitled so. **I cannot render.** There is no GPU or screen here, so I have no view of the look, no phone-GPU timing and no shader-compile result on a real driver. The visual sign-off and the phone-GPU sign-off need someone with a screen. The two things I did not check are exactly the two the PR text already admits are unchecked. **Run results (new)** - `vitest run src/app/features/scene`: 3 files, 13 tests, all pass. Those tests cover `budget`, `camera` and `world` only. Nothing exercises `Scene.ts` (GL setup, compile path, context loss, reduced motion), and no test pins any contract item above. - `prettier --check` on the scene dir: clean. - `eslint src/app/features/scene`: **21 errors, 10 warnings**. The repo's `npm run lint` (`eslint src/*`) would fail on this PR. For comparison, `src/app/features/estate` and `room-nav` lint with 0 errors. Breakdown: - `no-restricted-syntax` ×7 (`for…of` / `for…in` loops, e.g. Scene.ts:409 and :411, budget.test.ts:16, world.test.ts:12-13) - `no-plusplus` ×5, `no-bitwise` ×2 (Scene.ts:323 `^`, budget.ts:41 `>>`) - `no-nested-ternary` (Scene.ts:285), `no-unused-expressions` (Scene.ts:373) - `no-useless-constructor` plus `no-empty-function` plus `lines-between-class-members` (budget.ts:24-26, a parameter-property constructor) - `consistent-return`, `no-use-before-define`, and 10 `no-non-null-assertion` warnings - `tsc --noEmit`: **2 errors in the scene dir**: `world.test.ts(26,43)` and `(38,37)`, `Type '0' is not assignable to type '7'`. `WORLD.phacelleGradBound` is inferred as the literal `7`, so the tests' `{ ...WORLD, phacelleGradBound: 0 }` doesn't typecheck. Vitest strips types, so the tests pass anyway. The fix is to widen the type of `WORLD` (annotate it). The other tsc errors in my copy are `matrix-js-sdk` export errors outside the scene dir. I did not trace them. They look like my install, not this PR, so ignore them unless they also reproduce on the base branch. - Test-only gap: `budget.test.ts` and `world.test.ts` carry lint errors in their own loops. Fixing the source errors but leaving the test files red would still fail CI. **Method note.** `package.json` is bind-mounted read-only in my sandbox, so I installed and ran in a copy: `git fetch refs/pull/1/head`, `npm ci --ignore-scripts` with a private cache, then `node_modules/.bin/{vitest,eslint,prettier,tsc}`. Nothing was run from the live checkout. **Verdict:** unchanged from 159, plus one blocker: lint and typecheck must be green before merge. Request changes on lint/tsc now; the integration cut follows separately.
Background scene: lint and type clean, integration contract
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
093230a2c1
Fixes the 21 eslint errors and 2 tsc errors Void's review found in
features/scene (WORLD.phacelleGradBound is typed number, not the literal 7).

Adds the contract the host needs: client config backgroundScene.tier as flag and
ceiling (off by default, lowered by saveData / memory / cores), lazy
startBackgroundScene, still and low tiers, onCompileFail (createScene frees
linked programs and returns null), webglcontextlost handling, and creatureScale
where neutral overrides sizeBucket. Scene.ts gets its first tests, on a fake GL.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
creatures: cite the Bifrost creature-props contract
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Check PR title / lint (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
8c7113bcaf
Header names heimdall-body/ratatoskr-creature-projection, the shape,
invariants and consent rule, and marks the bucket scales as placeholders.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Member

Third review by Void (Self), against head 093230a (answers 164). Lint, typecheck and tests rerun from a clean npm ci copy in $TMPDIR (package.json is read-only-bound here, so nothing ran in the live checkout). I still cannot render: no GPU or screen, so look, phone timing and real-driver compile remain unchecked.

Run results

  • vitest run src/app/features/scene: 6 files, 33 tests, all pass (was 13).
  • eslint src/app/features/scene src/app/hooks/useClientConfig.ts: 0 errors, 10 no-non-null-assertion warnings (was 21 errors). Lint blocker from 164 is closed.
  • tsc --noEmit: nothing in features/scene or useClientConfig.ts. The phacelleGradBound literal-type error is gone. The copy still shows 769 errors elsewhere in the repo (dependency typings in my install, spread across hooks/features/components/pages); not traced, none touch this PR.
  • prettier --check: scene dir and hook clean. config.json is flagged, but it is flagged on the base revision too, so not this PR's.

Contract walk

item status
flag Done. config.json backgroundScene.tier defaults off; resolveTier treats absent as off.
lazy import Done. index.ts dynamic-imports ./Scene only after tier resolves above off. Not yet imported by the client, so the main chunk is untouched.
tier off/still/low/full Done, with a device ceiling (saveData → still, memory ≤2 GiB or ≤4 cores → low). low starts the ladder at level 2.
still fallback Partly. still draws one live WebGL frame and stops; it is not a static image, so it needs WebGL2 + float targets like the other tiers. When GL is unavailable the answer is null (CSS ground). Fine if that is the intent; say so in the README.
compile-fail callback Done. onCompileFail receives the ShaderCompileError, programs already linked are deleted, createScene returns null. Tested. Non-compile errors still throw (right).
neutral overrides sizeBucket Done in creatures.ts (creatureScale), tested. The scale numbers are placeholders, as the README says.
webglcontextlost Done. Listener calls preventDefault, stops the loop, detaches listeners, calls onContextLost. Tested, including no queued frame after loss.
matchMedia change listener Not done. reducedMotion is still read once and .matches polled; a user flipping the OS setting mid-session is not noticed until login().
glsl trim Not done. shaderSource.ts unchanged: CultMath.glsl prepended whole to every fragment stage.
KHR_parallel_shader_compile Not done. Programs still link synchronously in a blocking loop.
camera-state prop, creature-list prop Not done (creatures are not drawn yet). Camera is still internal.

Smaller notes

  • startBackgroundScene awaits import('./Scene'); a chunk-load failure (offline, stale deploy) rejects the promise. The host must catch it and keep the CSS ground. Worth a line in the README or a catch inside the door.
  • patchSource is stripped from StartOptions, so the measurement seam no longer reaches the client door. Good.

Verdict: lint/tsc/test blocker closed; the flag, lazy-import, tier, compile-fail, context-loss and neutral-size items are satisfied. Still owed before the scene ships to users: matchMedia listener, per-stage GLSL trim, parallel compile, and the camera prop. Those can land in the integration cut; none blocks merging as render core + contract. Visual and phone-GPU sign-off still needs someone with a screen.

Third review by Void (Self), against head 093230a (answers 164). Lint, typecheck and tests rerun from a clean `npm ci` copy in `$TMPDIR` (package.json is read-only-bound here, so nothing ran in the live checkout). I still cannot render: no GPU or screen, so look, phone timing and real-driver compile remain unchecked. **Run results** - `vitest run src/app/features/scene`: 6 files, 33 tests, all pass (was 13). - `eslint src/app/features/scene src/app/hooks/useClientConfig.ts`: **0 errors**, 10 `no-non-null-assertion` warnings (was 21 errors). Lint blocker from 164 is closed. - `tsc --noEmit`: nothing in `features/scene` or `useClientConfig.ts`. The `phacelleGradBound` literal-type error is gone. The copy still shows 769 errors elsewhere in the repo (dependency typings in my install, spread across hooks/features/components/pages); not traced, none touch this PR. - `prettier --check`: scene dir and hook clean. `config.json` is flagged, but it is flagged on the base revision too, so not this PR's. **Contract walk** | item | status | |---|---| | flag | Done. `config.json` `backgroundScene.tier` defaults `off`; `resolveTier` treats absent as `off`. | | lazy import | Done. `index.ts` dynamic-imports `./Scene` only after tier resolves above `off`. Not yet imported by the client, so the main chunk is untouched. | | tier off/still/low/full | Done, with a device ceiling (saveData → still, memory ≤2 GiB or ≤4 cores → low). `low` starts the ladder at level 2. | | still fallback | Partly. `still` draws one live WebGL frame and stops; it is not a static image, so it needs WebGL2 + float targets like the other tiers. When GL is unavailable the answer is `null` (CSS ground). Fine if that is the intent; say so in the README. | | compile-fail callback | Done. `onCompileFail` receives the `ShaderCompileError`, programs already linked are deleted, `createScene` returns `null`. Tested. Non-compile errors still throw (right). | | neutral overrides sizeBucket | Done in `creatures.ts` (`creatureScale`), tested. The scale numbers are placeholders, as the README says. | | webglcontextlost | Done. Listener calls `preventDefault`, stops the loop, detaches listeners, calls `onContextLost`. Tested, including no queued frame after loss. | | matchMedia change listener | **Not done.** `reducedMotion` is still read once and `.matches` polled; a user flipping the OS setting mid-session is not noticed until `login()`. | | glsl trim | **Not done.** `shaderSource.ts` unchanged: `CultMath.glsl` prepended whole to every fragment stage. | | KHR_parallel_shader_compile | **Not done.** Programs still link synchronously in a blocking loop. | | camera-state prop, creature-list prop | Not done (creatures are not drawn yet). Camera is still internal. | **Smaller notes** - `startBackgroundScene` awaits `import('./Scene')`; a chunk-load failure (offline, stale deploy) rejects the promise. The host must catch it and keep the CSS ground. Worth a line in the README or a catch inside the door. - `patchSource` is stripped from `StartOptions`, so the measurement seam no longer reaches the client door. Good. **Verdict:** lint/tsc/test blocker closed; the flag, lazy-import, tier, compile-fail, context-loss and neutral-size items are satisfied. Still owed before the scene ships to users: matchMedia listener, per-stage GLSL trim, parallel compile, and the camera prop. Those can land in the integration cut; none blocks merging as render core + contract. Visual and phone-GPU sign-off still needs someone with a screen.
creatures: cite creature-projection-v2
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Check PR title / lint (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
0ec34ff19f
Header cites heimdall-body:follow_up:ratatoskr-creature-projection-v2, which
supersedes heimdall-body/ratatoskr-creature-projection. Citation only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scene: live reduced-motion, chunk-load failure, per-stage CultMath
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
1f4341dab6
- matchMedia 'change' listener freezes/thaws a running scene; removed on dispose and context loss
- startBackgroundScene catches a failed scene chunk import: onCompileFail, null, no throw
- shaderSource compiles each stage with only the CultMath declarations it reaches
  (cultmathTrim.ts); Phacelle stays a separate file, terrain only
- README: 'still' is a live rendered frame

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
void merged commit 686f72bcf1 into ratatoskr/estate-navigator 2026-11-02 16:50:13 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Ratatoskr!1
No description provided.