feat: port WebGPU/RendererLike + construction firewall + fixes from next#56
feat: port WebGPU/RendererLike + construction firewall + fixes from next#56bigmistqke wants to merge 22 commits into
Conversation
… DOM-renderer DPR
- Widen Context.gl to Meta<ResolvedRenderer> and CanvasProps.gl to accept
any ResolvedRenderer (instance or factory) plus the WebGL config-object
shorthand when WebGLRenderer extends ResolvedRenderer.
- Rewrite the gl memo with a structural instance check (typeof
render/setSize) so WebGPURenderer and other RendererLike implementations
are recognised regardless of class. Task 4 will replace the inline check
with isRenderer().
- Add an intermediate createRenderEffect that awaits renderer.init() before
the first frame (WebGPU); render() gates on a glInitialized flag. Task 6
refactors this into a createResource.
- Replace the legacy useProps({ outputEncoding, toneMapping }) on gl with
two structural-gated render effects setting outputColorSpace (using
LinearSRGBColorSpace/SRGBColorSpace) and toneMapping only when the
renderer exposes those properties.
- Make setPixelRatio optional at call sites (canvas resize + dpr getter)
for DOM-based renderers; default Context.dpr to 1 when getPixelRatio is
absent.
- Targeted casts for XR/shadowMap consumers retain existing behaviour
pending Task 4's isWebXRManager/isWebGLShadowMap helpers.
- Mirror the fixes-branch cast pattern in renderer.test.tsx assertions
that read WebGL-specific surface (shadowMap, toneMapping, xr).
- Cast three.gl.domElement to HTMLElement in playground orbit-controls,
since RendererLike.domElement is the broader Element type.
…t() init gate
Three gaps surfaced while porting regression tests from `next`:
1. autodispose factory/default gl renderers
Task 8's firewall + Task 9's gl-tuple memo didn't dispose the previous
renderer on swap. Wrap factory and default-branch constructions in
`autodispose` (instance renderers are user-supplied so we leave them).
Also broaden `autodispose`'s generic to accept any `T` and runtime-check
for `dispose` so it accepts the `Renderer` union (RendererLike has no
`dispose` member, so an `extends { dispose?: () => void }` bound rejects
it).
2. `canDriveXR` unifies WebGL and WebGPU XR
Replaces `isWebXRManager` (which only matched WebGL's xr.setAnimationLoop
shape). WebGPURenderer exposes XR session events on `xr` (event-target)
but `setAnimationLoop` only on the renderer itself; WebGL has both.
Always calling `_gl.setAnimationLoop` (not `_gl.xr.setAnimationLoop`)
unifies the two paths.
3. `latest(rendererReady)` instead of `isPending(rendererReady)`
`isPending` in Solid 2 requires being read inside a tracking scope or
a `<Loading>` boundary to surface async-pending state. The render-loop
runs from `requestAnimationFrame` — not tracked — so isPending returned
false even while the init promise was unresolved, and render() fired
on the first frame before WebGPU's `init()` resolved. `latest()` reads
the most-recently-committed value bypassing the pending overlay; before
commit it returns undefined, so the gate `latest(rendererReady) !== true`
correctly blocks render until the signal commits `true`.
Mirrors the existing 'Signal was written' trace patch — when the Solid 2 strict-mode warning fires, log a console.trace so the offending call site is visible in the test output.
…ession tests
Ports the regression-test additions from `next` to `next-solid-2`,
adapting to Solid 2.x idioms:
- `Suspense` -> `Loading` (Solid 2 renamed it)
- `@solidjs/testing-library` -> inline `render` shim over `@solidjs/web`
(the testing-library imports from `solid-js/web` which Solid 2 doesn't
expose; the subpath moved to `@solidjs/web`)
- async `test()` awaited in every call site (Solid 2's signal model
needed `onSettled` ordering inside test())
- `untrack(resource)` in the Resource render-function test — Solid 2's
<Show>{r => ...} runs the callback inside untrack, so calling `r()` in
plain JS triggers STRICT_READ_UNTRACKED
Coverage added:
- renderer.test.tsx: ~26 tests (RendererLike instance/factory, init-await,
hasInitialized-skip, foreign Material attach via duck-typing, xr-stub /
shadowMap-stub WebGPU shapes, getPendingInit unit tests, construction
firewall, gl tuple constructor-args + properties form, color-mgmt split)
- use-loader-suspense.test.tsx: 2 tests (no-fallback regression for the
useLoader + Loading crash, fallback↔content swap)
- hooks.test.tsx: re-enable 3 useLoader integration tests (single URL,
onBeforeLoad); record-of-URLs marked .todo — record path works in
use-loader.test.tsx with a sync MockLoader but times out here with the
GLTFLoader (separate Solid 2 issue, not the Task 14 regression intent)
- api-coverage.test.tsx: 11 tests (createEntity, meta/getMeta/hasMeta,
useProps reactivity, load standalone, Resource Object3D + attach +
render-fn, CursorRaycaster + CenterRaycaster)
- Node 20 -> 22: pnpm 11 (which corepack pulls automatically) requires Node 22.13+ and crashes on Node 20 trying to load `node:sqlite`. - Add Playwright Chromium install step before tests (tests run in real browser since the jsdom drop on the next branch). - pnpm-workspace.yaml with `allowBuilds: esbuild: true` so esbuild's postinstall is allowed under pnpm 11's default install-script lockdown. Ports CI changes from the `fixes` branch (b02bf03, aa7eded, ci: install Playwright Chromium before running tests).
Both `scene` and `raycaster` memos return `meta(instance, { get props })`
already; `camera` was returning a raw THREE.Camera. Surfaced by the
fixes-vs-fixes-solid2 parity audit. Even if nothing currently reads
camera meta, the asymmetry would trip up future code or tests that
inspect `getMeta(context.camera)`.
Ports the real-browser test stack from `next`: - Replace jsdom + `@solidjs/testing-library` (the latter imports from `solid-js/web`, a subpath Solid 2 doesn't expose) with vitest browser mode driven by Playwright Chromium. Real WebGL contexts, real DOM layout, real ResizeObserver — drop the WebGL2RenderingContext shim and the ResizeObserver polyfill in tests/setup.ts. - `vitest.config.ts`: enable browser mode, pin SwiftShader (`--use-gl`) so the ~16 concurrent WebGL contexts cap doesn't bite the renderer-recreate tests, disable file parallelism for the same reason, set Loading boundary defaults via @solidjs/web JSX condition. - `src/testing/index.tsx`: track active unmounts so `cleanup()` (afterEach in tests/setup.ts) can explicitly dispose every test context, force-lose its WebGL backing, and unmount its canvas. Eagerly inline-size the canvas (style.width/height) so getBoundingClientRect returns the intended dimensions for raycasting before ResizeObserver fires. - `src/utils/use-measure.ts`: queueMicrotask(forceRefresh) on element attach so bounds are populated synchronously (well, next microtask) rather than waiting for the first async ResizeObserver entry — fixes raycaster tests that fire events immediately after canvas mount. - `tests/core/events.test.tsx` and `tests/core/canvas-events.test.tsx`: port the fixes-branch versions verbatim (real MouseEvent / PointerEvent objects with clientX/clientY, no offsetX defineProperty hacks). Adapt to next-solid-2's async `test()` and `libs/testing-library.ts` shim. Preserve the next-solid-2-only stopPropagation-undefined assertion on the enter/leave non-stoppable test, and keep the `await settled()` between reactive setOnClick and fireEvent so the event listener registration propagates. - `tests/core/use-loader-suspense.test.tsx`: switch from inline render-shim to `libs/testing-library.ts`'s exported render. - `tests/web/__snapshots__/canvas.test.tsx.snap`: regenerate — the canvas now carries inline style for explicit dimensions. - `.github/workflows/pkg-pr-new.yml`: add `playwright install --with-deps chromium` step before the test job. - `package.json` / `pnpm-lock.yaml`: drop jsdom, add @vitest/browser + @vitest/browser-playwright + playwright, bump vitest to ^4.1.7. - `.gitignore`: ignore vitest-attachments and per-test screenshots.
commit: |
Switch from queueMicrotask-deferred forceRefresh to a synchronous call, matching fixes-branch timing. The bounds signal now uses ownedWrite: true so the write from inside the observer-attach effect callback is permitted (avoids SIGNAL_WRITE_IN_OWNED_SCOPE). The forceRefresh call itself is wrapped in untrack() to suppress STRICT_READ_UNTRACKED on the internal element() read — the effect already destructured `el` from its source so the read is safe. Net effect: bounds is correct from the very first synchronous read after setElement, not 1 microtask later. Camera aspect ratio, raycaster NDC math, dpr, and renderer.setSize all see real dimensions on frame one. Matches the synchronous behavior of fixes' when(element, ...) pattern.
…warn Ports the fixes-branch refactor (4f61a89) to fixes-solid2. Drops the `gl={[ctorArgs, props]}` tuple form in favor of a flat `gl={Partial<Props & WebGLRendererParameters>}` (r3f's shape). solid-three internally splits constructor-only keys (alpha, antialias, depth, …) from instance-writable ones at construction time. Why: the tuple's tuple[0]-recreates-renderer behavior was a lie under real WebGL — `getContext` is idempotent per canvas, so ctor args can never actually change after first construction. We now bake ctor args once and warn (once) if the user reactively changes a ctor-only key, so the limitation is visible instead of silent. Changes: - `canvas.tsx`: `gl` accepts `Partial<Props<WebGLRenderer> & WebGLRendererParameters>`. - `create-three.tsx`: drop `glConstructorArgs` memo. gl memo's default branch splits keys via a `WEBGL_CTOR_KEYS` whitelist, reads the flat object untracked. New `createEffect((src, eff))` warns once on reactive ctor-arg change (Solid 2's 2-arg form, not the fixes-branch single-arg form which Solid 2 forbids). - `utils.ts`: drop `shallowEqual` (no longer used). - `tests/core/renderer.test.tsx`: drop the 4 tuple-form tests; add flat-form smoke test, reactive instance-prop test, and warn-on- ctor-arg-change test.
Ports fixes-branch 51e85bc. The JSDoc was a one-liner; replace with the fuller description of the flat-object form (ctor args baked, instance props reactive, warn on reactive ctor-key change) so the type's behavior is discoverable from the prop site.
|
Additional commits since the original description, in push order:
Test status
Still out of scope (deferred)
|
Brings
next-solid-2to feature parity withnext: WebGPU/RendererLike support, construction firewall, gl tuple form, duck-typed Material/Object3D attach, the useLoader+Suspense crash fix, Resource attach fix, and the regression tests behind each of these.Plan + analysis:
docs/superpowers/plans/2026-05-26-port-next-to-next-solid-2.mdanddocs/superpowers/specs/2026-05-26-port-next-to-next-solid-2-design.md(committed on thefixesbranch — same content informed every commit here).Sequence of commits (16)
chore(deps): bump three and @types/three to ^0.181feat(types): introduce RendererLike + Renderer union + Register augmentationfeat(canvas): accept RendererLike, await init, structural color mgmt, DOM-renderer DPRfeat(utils): isRenderer + duck-typed manager narrows + getPendingInitfeat(xr): warn when context.xr.connect/disconnect is a no-oprefactor(create-three): use createSignal(async fn) for renderer initfix(utils): meta() preserves getters without invoking at merge-timefeat(canvas): firewall construction memos against reactive prop contentfeat(canvas): accept [ctorArgs, properties] tuple for the gl propfix(props): duck-type Material/Object3D/Fog/BufferGeometry attach checksfix(create-three): drop merge when calling useSceneGraphfix(Resource): tag loaded resource with meta so parent attach worksrefactor(utils): consolidate isWritable into utils.tsfix(create-three): autodispose gl, canDriveXR for WebGL+WebGPU, latest() init gate(surfaced while porting tests)test(setup): stack-trace STRICT_READ_UNTRACKED warningstest: port renderer + use-loader-suspense + hooks + api-coverage regression testsSolid 2.x-specific adaptations vs
nextcreateResourceis gone — replaced withcreateSignal(() => Promise)andlatest()to gate the render loop.mergeProps→merge. Verified getter preservation behaves the same.splitProps→omitinResource.<Suspense>→<Loading>.isWebXRManager(xr.setAnimationLoop probe) generalised tocanDriveXR(gl)— drivingsetAnimationLoopon the renderer covers both WebGL and WebGPU.rendershim intests/core/use-loader-suspense.test.tsxsince@solidjs/testing-library@0.8.10imports fromsolid-js/web, a subpath Solid 2 doesn't expose.Test plan
pnpm exec tsc --noEmitcleanpnpm exec vitest run— 9 files, 152 passed / 2 skipped / 1 todoKnown follow-ups (not in this PR)
.todotest:hooks.test.tsx > useLoader hook with a record of URLstimes out withGLTFLoader(the same record path works inuse-loader.test.tsxwith a syncMockLoader). Record-of-URLs coverage is preserved; the GLTFLoader + record + Solid 2 async-memo interaction needs separate investigation.