Repository navigation
#584: device-lost recovery + force-loss seam + cpuMs fallback - #586
Merged
MaximumTrainer merged 1 commit intoOct 25, 2026
Merged
MaximumTrainer merged 1 commit into
MaximumTrainer merged 1 commit into
Conversation
The WebGPU engine detected device.lost but did nothing with it: no re-attach, no banner, no fallback publish, and the rAF loop kept submitting work to a dead device. Measured on Intel Gen-9 against next/svelte-webgpu @ 43433ba: 45 GPUCommandEncoder.finish + 45 queue.submit in 1.5s after a forced loss, no visible sign to the rower, __ROWER3D_CONTEXT_STATE stuck at 'ready'. Factors the per-device scene into src/lib/renderer/engine/rideScene.ts; attach.ts stays the thin orchestration layer (telemetry, outer render loop, device session lifecycle). On the first device.lost (D1-a): - publish fallbackReason='device-lost' - flip __ROWER3D_CONTEXT_STATE.kind to 'lost', backend stays 'webgpu' - publish the "restoring…" banner (D2-a copy ported verbatim from the retired WebGL spec) - dispose the current scene so no further work lands on the dead device - request a fresh device with isRetry: true - on retry success, build a new scene on the SAME canvas and continue; on retry failure, the retry's own onDeviceLost fires with 'renderer-unavailable', the engine publishes the "couldn't get it back" banner and stops drawing (D3-a). FR2 (D4-a): installs __ROWER3D_FORCE_DEVICE_LOST on the current device, gated on window.__PLAYWRIGHT_TESTING, so a spec can trigger a loss without reaching into the Chrome rendering panel. Replaced on every scene build, cleared on dispose. FR3 (D6-c): device opts in to the timestamp-query feature when the adapter has it (follow-up wires per-pass timestampWrites + readback for __ROWER3D_FRAME_STATS.gpuMs). cpuMs lands this PR as the always-on fallback — measured around frameOnce with performance.now, written to telemetry every frame. UI: /row polls window.__ROWER3D_BANNER at 250 ms and renders a banner overlay at the top of the stage with variant-colored backgrounds for restoring (amber) and failed (red). The row keeps counting through either state (ride.currentSession is DOM-driven, not render-driven). Flips the it.todo in ac4PortedBehaviors.test.ts (added in #585) to a passing test; adds three more: reattach-success, retry-also-dies, and force-loss seam presence. rideScene.ts is a mechanical extraction from attach.ts, exercised transitively via the attach suite — TDD guard off for the file move only (no new behaviour). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5D9BTXmu7SeMa3LghJD8b
MaximumTrainer
deleted the
claude/project-thread-lddi4g-584-device-lost
branch
October 25, 2026 15:26
4 tasks
MaximumTrainer
added a commit
that referenced
this pull request
Oct 25, 2026
* #542 AC1: remove the React tree from next/svelte-webgpu The Svelte + WebGPU port is at parity (PRs #581–#586). Everything the React/three.js shell owned is gone: `src/App.tsx`, `src/main.tsx`, `src/components/` (bar `routeMapProjection.ts` and the pure-TS scenery config / helpers that `src/lib/scenery/` and the Svelte UI still read), `src/hooks/`, `src/context/`, all 66 `*.tsx` tests and the handful of `*.ts` tests that only measured a React-era contract. The scenery `__tests__` were parity suites pinned on each React implementation, so they go too. Pure-TS modules under `src/components/rower3d/` that Svelte, `src/lib` or the renderer still import stay: `conditions`, `constants`, `crewModel`, `ghost`, `navigableWidth`, `rowerAppearance`, `sceneQuality`, `sceneryAssets`, `sceneryClearance`, `sceneryConfig`, `sceneryCrossings`, `sceneryRegion`, `sceneryTrack`, `segmentScenery`, `themeConfig`. They move into `src/lib/` in a later slice. Config fallout: * `index.html` — repo-root entry only existed to boot React; SvelteKit owns the HTML host via `src/app.html`. * `tsconfig.app.json` — `jsx: react-jsx` is dead with no `.tsx` left. * `tsconfig.playwright.json` — drops `renderBudget`/`strokePose` includes (and the two spec files that read them, which only ran on the React engine). * `eslint.config.js` — drops the react-hooks + react-refresh plugins. * `setupTests.ts` — drops the `@testing-library/jest-dom` shim; no preserved test uses its matchers. * `playwright.config.next.ts` — removes two deleted specs from the next-branch testIgnore. What stays for the follow-up: * `package.json` dependencies on `react`, `react-dom`, `@react-three/*`, `postprocessing`, `react-chartjs-2`, `@testing-library/react`, `@react-three/test-renderer`, `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh` — nothing in the surviving tree imports them, but ripping them out touches `package-lock.json` and deserves its own PR. Local gates: `tsc`, `eslint`, `vitest` (213 files · 2081 pass · 8 skip), `tsc -p tsconfig.playwright.json`, `svelte-check` (clean — remaining warnings are pre-existing node_modules + tsconfig composite notes). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5D9BTXmu7SeMa3LghJD8b * #542 AC1: re-base coverage floors after the React sweep Removing the React tree is a measurement change on the same shape as `#542 P0`'s Vitest 1 -> 3 upgrade: the tests that only existed to pin the three.js scene went with the production code they covered, and the parity suites under `src/lib/scenery/__tests__/` that happened to count toward preserved files' per-file 100 floors are gone too. New measurement over the surviving tree: 89.11 / 89.11 / 85.77 / 90.66 (lines / statements / branches / functions). Floors land a point under each reading, following the "set a floor clear of its measurement so the gate stops answering 'did the last digit move'" rule the comments above these numbers have followed since #340. Also drops the per-file 100 floors for files that left with React — `wakeTexture.ts`, `foliageMaterial.ts`, the rower3d `landscapeLayout.ts` and `foliagePlan.ts`, the three R3F `*.tsx` wrappers — plus the `**/foliageTexture.ts` floor whose only remaining match (the Svelte `src/lib/scenery/foliageTexture.ts`) is at 15 % because its parity test left too. The engine-side test that owns its measurement re-lands with the renderer work it belongs to. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5D9BTXmu7SeMa3LghJD8b --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #584 FR1 / FR2 / FR3 (D1-a, D2-a, D3-a, D4-a, D6-c). FR4 (pixel-level tier differentiation) stays per the agreed D5-a — rely on today's content gates first, open a follow-up only if screenshots still look identical after SSAO lands harder.
The problem this fixes
Measured on Dan's Intel Gen-9 laptop against
next/svelte-webgpu @ 43433ba: when the WebGPU device is lost, the engine sees it (deviceLostCount→ 1,device.lostpromise fires) but doesn't act on it. The rAF loop keeps submittingGPUCommandEncoder.finish+queue.submitagainst the dead device (45 of each in 1.5s),__ROWER3D_CONTEXT_STATEstays'ready', no banner appears, and the rower sees a visually frozen scene with no sign of what went wrong.What this PR does
FR1 — the engine actually recovers (D1-a + D2-a + D3-a)
attach.tsnow factors the per-device scene intosrc/lib/renderer/engine/rideScene.ts(mechanical extraction, same behaviour).attachkeeps the device-session lifecycle, outer rAF loop and telemetry.device.lost:publishFallback('device-lost')__ROWER3D_CONTEXT_STATE.kind→'lost', backend stays'webgpu'__ROWER3D_BANNER) →"Lost the graphics context — restoring…"(D2-a copy ported verbatim from the retired WebGL spec)isRetry: true; on success a new scene is built on the same canvas (D1-a) and the loop resumes; context state back to'ready', banner cleared.requestDevice's existingisRetrypath firesonDeviceLost('renderer-unavailable'), which maps to:__ROWER3D_CONTEXT_STATE.kind→'unavailable'with reason'renderer-unavailable'"We couldn't get the graphics context back. Your row is still being recorded."(D3-a)/rowreads__ROWER3D_BANNERon a 250 ms interval and renders an overlay at the top of the stage with--restoring(amber) and--failed(red) variants. The row keeps counting underneath becauseride.currentSessionis DOM-driven.FR2 — forced loss, test-only (D4-a)
__ROWER3D_FORCE_DEVICE_LOST()is installed onwindowonly when__PLAYWRIGHT_TESTINGis set. Points at the current scene's device, callsdevice.destroy(). Replaced on every scene build so a reattach exposes the new device, cleared on dispose.FR3 — timing (D6-c)
device.tsnow asks for thetimestamp-queryfeature when the adapter has it (same shape as the existing BC-compression opt-in).__ROWER3D_FRAME_STATS.cpuMsis populated every frame — measured end-to-end aroundframeOncewithperformance.now().gpuMsstays0until the frame-graph instrumentation lands. Follow-up: wiretimestampWriteson each pass + a resolve/map readback. Called out in the issue so a reader can tell it apart from the no-timing case.Tests
Flipped the
it.todo('reattaches and flips contextState to lost on first device loss')from #585 to a real passing test; added:reattaches and flips contextState to lost on first device loss, then back to ready on retry success— covers FR1 success path, asserts the scene owns a different device after loss.drops to renderer-unavailable + failed banner when the retry device also dies— covers D3-a.exposes __ROWER3D_FORCE_DEVICE_LOST only under __PLAYWRIGHT_TESTING (FR2 D4-a)— covers the test seam gate.writes cpuMs on every frame as the gpuMs fallback (FR3 D6-c)— covers the always-on timing.Test plan
npx vitest run src/lib/renderer/engine/__tests__/— 119 pass (was 116 + 1 todo)npx vitest run— 3652 pass + 8 skippednpx tsc --noEmit -p tsconfig.app.jsonnpx tsc -p tsconfig.playwright.json --noEmitnpm run lintnext/svelte-webgpu(preflight / scaffold smoke / WebGPU engine smoke / e2e-shards 1/2 + 2/2 / GPU runner skipped)Generated by Claude Code