Skip to content

feat: port WebGPU/RendererLike + construction firewall + fixes from next#56

Open
bigmistqke wants to merge 22 commits into
solidjs-community:next-solid-2from
bigmistqke:fixes-solid2
Open

feat: port WebGPU/RendererLike + construction firewall + fixes from next#56
bigmistqke wants to merge 22 commits into
solidjs-community:next-solid-2from
bigmistqke:fixes-solid2

Conversation

@bigmistqke
Copy link
Copy Markdown
Contributor

@bigmistqke bigmistqke commented May 26, 2026

Brings next-solid-2 to feature parity with next: 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.md and docs/superpowers/specs/2026-05-26-port-next-to-next-solid-2-design.md (committed on the fixes branch — same content informed every commit here).

Sequence of commits (16)

  1. chore(deps): bump three and @types/three to ^0.181
  2. feat(types): introduce RendererLike + Renderer union + Register augmentation
  3. feat(canvas): accept RendererLike, await init, structural color mgmt, DOM-renderer DPR
  4. feat(utils): isRenderer + duck-typed manager narrows + getPendingInit
  5. feat(xr): warn when context.xr.connect/disconnect is a no-op
  6. refactor(create-three): use createSignal(async fn) for renderer init
  7. fix(utils): meta() preserves getters without invoking at merge-time
  8. feat(canvas): firewall construction memos against reactive prop content
  9. feat(canvas): accept [ctorArgs, properties] tuple for the gl prop
  10. fix(props): duck-type Material/Object3D/Fog/BufferGeometry attach checks
  11. fix(create-three): drop merge when calling useSceneGraph
  12. fix(Resource): tag loaded resource with meta so parent attach works
  13. refactor(utils): consolidate isWritable into utils.ts
  14. fix(create-three): autodispose gl, canDriveXR for WebGL+WebGPU, latest() init gate (surfaced while porting tests)
  15. test(setup): stack-trace STRICT_READ_UNTRACKED warnings
  16. test: port renderer + use-loader-suspense + hooks + api-coverage regression tests

Solid 2.x-specific adaptations vs next

  • createResource is gone — replaced with createSignal(() => Promise) and latest() to gate the render loop.
  • mergePropsmerge. Verified getter preservation behaves the same.
  • splitPropsomit in Resource.
  • <Suspense><Loading>.
  • isWebXRManager (xr.setAnimationLoop probe) generalised to canDriveXR(gl) — driving setAnimationLoop on the renderer covers both WebGL and WebGPU.
  • Inline render shim in tests/core/use-loader-suspense.test.tsx since @solidjs/testing-library@0.8.10 imports from solid-js/web, a subpath Solid 2 doesn't expose.

Test plan

  • pnpm exec tsc --noEmit clean
  • pnpm exec vitest run — 9 files, 152 passed / 2 skipped / 1 todo
  • Manual smoke test in the playground (recommended given the createSignal-based init refactor — jsdom can't drive the actual async path).

Known follow-ups (not in this PR)

  • The 1 .todo test: hooks.test.tsx > useLoader hook with a record of URLs times out with GLTFLoader (the same record path works in use-loader.test.tsx with a sync MockLoader). Record-of-URLs coverage is preserved; the GLTFLoader + record + Solid 2 async-memo interaction needs separate investigation.
  • Documentation port (README sections on Renderer union / Register augmentation / WebGPU / CSS3D / SVG playground examples / CONTRIBUTING.md rename + trim) is deferred to a follow-up PR.

bigmistqke added 19 commits May 26, 2026 20:48
… 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.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 26, 2026

commit: 90000cc

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.
@bigmistqke
Copy link
Copy Markdown
Contributor Author

bigmistqke commented May 26, 2026

Additional commits since the original description, in push order:

  • c43efe9 ci: bump Node 20 → 22, pnpm v11 allowBuilds (so esbuild's postinstall is permitted). Required to unstick the original CI failure — corepack pulls pnpm 11 which needs Node 22.13+.

  • fd5843b fix(create-three): wrap camera memo result with meta(..., { get props }). scene and raycaster already did this; the asymmetry was surfaced by a parity audit of fixes vs fixes-solid2 and would have tripped up future code reading getMeta(context.camera).

  • 08f044e test: switch fully to vitest browser mode + Playwright (Chromium with SwiftShader), drop jsdom and @solidjs/testing-library. The latter imports from solid-js/web, a subpath Solid 2 doesn't expose (moved to @solidjs/web). Drops the WebGL2RenderingContext shim and the ResizeObserver polyfill in tests/setup.ts. Test files use real MouseEvent / PointerEvent instead of new Event(...) + defineProperty(offsetX) hacks. tests/core/use-loader-suspense.test.tsx now uses the project's libs/testing-library.ts shim. Workflow adds playwright install --with-deps chromium before tests.

  • 5b84e3f fix(useMeasure): populate bounds synchronously on element attach. Matches fixes-branch timing (when(element, ...) reads synchronously) so the first render frame sees real canvas dimensions instead of {width: 0, height: 0}. The bounds signal is created with ownedWrite: true to allow the write from inside the observer-attach effect callback; the forceRefresh call is wrapped in untrack(...) to suppress the read warning on element() (the effect already has el from its source).

  • 8a53eb0 refactor(canvas): replace the gl={[ctorArgs, props]} tuple form with a flat gl={Partial<Props & WebGLRendererParameters>} (r3f's shape). Ports fixes-branch 4f61a89. The tuple's tuple[0]-recreates-renderer behavior was a lie under real WebGL — getContext is idempotent per canvas. solid-three now splits the flat object via a WEBGL_CTOR_KEYS whitelist at construction; instance-writable keys stay reactive via useProps. A createEffect (Solid 2's 2-arg form) warns once if the user reactively changes a ctor-only key. The 4 tuple-specific tests are replaced by 3 new flat-form tests (acceptance, reactive instance prop, warn-on-ctor-arg-change). Also drops the unused shallowEqual util.

  • 90000cc docs(canvas): update the gl prop JSDoc to describe the flat-form API (ctor args baked, instance props reactive, warn on reactive ctor-key change).

Test status

pnpm exec tsc --noEmit clean. pnpm exec vitest run — 9 files, 153 passed / 3 todo. One of the todos (useLoader + record-of-URLs + GLTFLoader) was noted in the original body and is unaffected by these commits. CI is green on the latest push.

Still out of scope (deferred)

  • README + playground demo updates that mirror the new flat-gl shape (matches the fixes-branch ece7abb and the WebGPU/CSS3D/SVG example commits).
  • CONTRIBUTING.md rename + trim.

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