WGSL → WESL conversion + shared shader library (in progress)#36
Closed
WGSL → WESL conversion + shared shader library (in progress)#36
Conversation
Captures the 15-task migration: wesl-plugin tooling bootstrap, lib/ extraction (math/, camera, billboard, orientation, colorIndex, cloudFade, masks, astro, tonemap, util), and uniform vertex/fragment/io file split across all 7 renderer shaders. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bite-sized TDD-style plan for executing the WGSL→WESL refactor: tooling bootstrap, lib/ extractions (math/, camera, billboard, orientation, colorIndex, cloudFade, masks, astro, tonemap, util), and uniform vertex/fragment/io split across all 7 shaders. Also corrects the spec to use the actual wesl-plugin `?static` suffix and `::` import syntax (verified against wesl-lang.dev — `?link` would have been wrong). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds wesl@^0.7.26 + wesl-plugin@^0.6.74 (build-time WESL→WGSL linker) wired into Vite via the ?static import suffix. Renames toneMap.wgsl → toneMap.wesl as the smoke-test shader; toneMapPass.ts switches ?raw → ?static and gains a dev-mode getCompilationInfo log so we can map browser shader-compile errors back to the linked WGSL output (wesl-plugin doesn't yet emit sourcemaps that survive into Chrome's WGSL diagnostics). Three real findings from this smoke test, baked into the plan for later tasks: 1. WESL parser rejects backticks in comments (// or /* */). Stripped from toneMap.wesl as a content change; task 2 globally replaces ` → ' across all remaining shaders. Mechanically reversible if upstream fixes the parser. 2. tsconfig types-array entry "wesl-plugin/suffixes" doesn't reliably resolve under moduleResolution=bundler. Required a triple-slash reference in src/@types/wesl.d.ts for TS to pick up the *?static ambient declarations. 3. Vitest doesn't auto-inherit Vite plugins from vite.config.ts. Added wesl-plugin to vitest.config.ts so the SSR transform pipeline can handle .wesl imports during tests. Verification: npm run typecheck (green), npm run build (green), npm test (880/880 green). Visual sanity check on tone-mapped scene is pending the user — toneMap.wesl has zero imports so the linker is a passthrough and output should be byte-identical. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bulk rename. Each renderer's ?raw import becomes ?static so the WESL linker runs on every shader. Output WGSL is byte-identical until imports are introduced in later tasks, save for one mechanical content change: backticks in shader comments are replaced with single quotes project-wide because the WESL parser tokenises ` regardless of comment context. The single-quote replacement preserves the visual intent of the inline-code callouts and is mechanically reversible if the parser later loosens up. Also refreshes pointRenderer's stale module-import docblock — it still described ?raw + WGSL semantics from before Task 1's wesl-plugin wire-up. Verification: typecheck green, build green, npm test 880/880 green. Visual sanity check pending the user — every shader still has zero imports so the linker output is byte-identical. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r, constants
Pulls scalar constants (PI/TAU/LOG10) and five small primitives
(saturate, rot2, sabs, toPolar, toRect) out of points.wesl and
milkyWayImpostor.wesl into a shared module. Removes the inline
duplicates and renames GLSL-style 'rot' → 'rot2' to free the bare
'rot' name for a future axis-angle 3D rotation helper.
Two important findings about WESL idioms surfaced while building this
out — both folded back into the spec, plan, and an auto-memory note:
1. The literal package prefix is 'package::', not 'skymap::'. The npm
package.json name doesn't resolve through wesl-plugin in this
setup; only the literal token 'package' does. Verified empirically
by debug-instrumenting findSource in node_modules/wesl/dist/.
2. WESL imports a function FROM a module, treating the last segment
of the path as the function name. One-function-one-file (the
pre-execution plan) forces a verbose duplicated leaf in the
import path ('lib::math::saturate::saturate'). The idiomatic
WESL shape is one cohesive multi-function module per file, so
the six original lib/math/<fn>.wesl files were collapsed into a
single lib/math.wesl with section-divider comments. Each fn
keeps its own docblock; reading top-to-bottom mirrors the
previous per-file sequence.
Verification: typecheck green, build green, 880/880 tests green.
Visual sanity check pending the user — the module body is
byte-equivalent to the previously-inline definitions plus the rot →
rot2 rename, so output should be identical.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…+ helpers
Adds a shared module exposing the universal camera-uniform prefix
(viewProj + viewportPx + 8 B alignment pad = 80 B) plus three
helpers: worldToClip, worldToNdc, worldEyeDepth.
The struct is intentionally minimal. The plan's draft included
view, proj, kPerZ, dpr, timeSec, and cameraPos, but inventorying
the existing renderer Uniforms structs showed:
- No renderer separates view/proj — all use combined viewProj.
- kPerZ, dpr, timeSec are not in any uniform today.
- cameraPos placement varies wildly across renderers (points,
quads, disks, proceduralDisks, milkyWayImpostor each put
different fields between viewport and the cameraPos slot).
- filaments has no cameraPos at all.
So CameraUniforms holds only the truly-universal prefix; the
worldEyeDepth helper takes cameraPos as an explicit parameter
rather than reading it off the struct, letting renderers keep
their existing layout for that field.
No consumers yet — this commit is just the module.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Embed the shared 'CameraUniforms' prefix from 'lib/camera.wesl' as the
first 80 bytes of the filaments Uniforms struct, then keep the two
renderer-specific scalars ('halfWidthPx', 'intensityScale') at offsets
80/84 with an 8-byte tail pad. Total uniform size grows from 80 to 96
bytes; the CPU-side uploader writes the scalars at f32-indices 20/21
(was 18/19) and leaves the new reserved pad slots zero.
Replaces the inline 'u.viewProj * vec4<f32>(p, 1.0)' projection with
the shared 'worldToClip' helper. NDC math still uses the local
endpoint clips because the perspective divide's 'w' is reused below
to restore clip space — calling 'worldToNdc' would project twice.
First adopter of the shared camera library; remaining renderers
(quads, disks, proceduralDisks, milkyWayImpostor, points) follow in
subsequent commits per the WESL conversion plan.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The quads uniform layout already matched 'CameraUniforms' byte-for-byte
in its first 80 bytes ('viewProj + viewport + _pad0 + _pad1'), so this
adoption is a pure renaming: 'u.viewProj' becomes 'worldToClip(u.cam, ...)'
and 'u.viewport' becomes 'u.cam.viewportPx'. The renderer-specific
'camPosWorld + pxPerRad' pair stays at offset 80, and the CPU-side
uniform writes at f32-indices 20..23 are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The disks uniform layout already matched 'CameraUniforms' byte-for-byte
in its first 80 bytes ('viewProj + viewport + _pad0 + _pad1'), so this
adoption is a pure renaming: 'u.viewProj' becomes 'worldToClip(u.cam, ...)'.
The disks vertex stage doesn't consult 'viewport' or 'camPos' — disk
orientation is an intrinsic, camera-independent galaxy property — so
only 'worldToClip' is imported, matching the restraint shown in the
filaments + quads adoptions. The renderer-specific 'camPos + _pad2'
pair stays at offset 80, and the CPU-side uniform writes at f32-indices
20..23 are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the inline viewProj/viewport/_pad0/_pad1 prefix with the shared CameraUniforms struct from lib/camera.wesl, and route clip-space projection through the worldToClip helper. The procedural-disk vertex stage is intentionally camera-independent (disk orientation is an intrinsic galaxy property derived from Earth -> galaxy line of sight), so we import only worldToClip; camPosWorld and pxPerRad remain in the trailing renderer-specific tail at the same byte offsets, preserving ABI with proceduralDiskRenderer.ts (no TS-side changes required). Same prefix-rename pattern as the earlier disks.wesl adoption. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Embed the shared 'CameraUniforms' prefix from 'lib/camera.wesl' as the
first 80 bytes of the milkyWayImpostor Uniforms struct. The previous
layout placed 'fadeAlpha' + 'iTime' at offsets 72/76, which collide
with the '_pad0/_pad1' slots that 'CameraUniforms' reserves — so we
can't drop the shared prefix in without relocating those scalars.
Resolution: put 'cam: CameraUniforms' first (occupies 0..79), then
pack the renderer-specific fields after the cam block. 'cameraPosWorld'
(vec3, 16-byte alignment) lands naturally at offset 80, and the two
f32 scalars 'fadeAlpha' + 'iTime' fall in at 92 / 96. The struct grows
from 96 to 112 bytes (round-up to the next 16-byte multiple).
CPU-side offset changes:
- fadeAlpha: f32 index 18 → 23 (offset 72 → 92)
- iTime: f32 index 19 → 24 (offset 76 → 96)
- cameraPosWorld: f32 indices 20..22 unchanged (offset 80, vec3
repacks against the f32 immediately after rather
than against a dedicated _pad slot)
Replaces the inline 'u.viewProj * vec4<f32>(p, 1.0)' projection in the
vertex stage with the shared 'worldToClip(u.cam, p)' helper. The
fragment-stage references to 'u.cameraPosWorld' / 'u.fadeAlpha' stay as
top-level fields — those are renderer-specific and live outside the
cam block.
Also bumps the UNIFORM_BUFFER_SIZE constant test from 96 to 112 to
match the new layout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Embed the shared 'CameraUniforms' prefix from 'lib/camera.wesl' as the first 80 bytes of the points 'Uniforms' struct. The pre-refactor layout placed 'pointSizePx' + 'brightness' at offsets 72/76, which collide with the '_pad0/_pad1' slots that 'CameraUniforms' reserves — so we can't drop the shared prefix in without relocating those scalars. Resolution: put 'cam: CameraUniforms' first (occupies 0..79), then swap 'pointSizePx' + 'brightness' into the 8 bytes of slack that the old layout already had at offsets 88..95 (the '_pad0/_pad1' u32s required for vec3-alignment before 'camPosWorld'). Same total size (176 bytes), same alignment, every field from offset 96 onward unchanged. Replaces the inline 'u.viewProj * vec4(p, 1.0)' projection with the shared 'worldToClip(u.cam, p)' helper and 'u.viewport' references with 'u.cam.viewportPx'. CPU-side offset changes: - pointSizePx: f32 index 18 -> 22 (offset 72 -> 88) - brightness: f32 index 19 -> 23 (offset 76 -> 92) - selectedIndex: UNCHANGED at offset 80 - camPosWorld + pxPerRad + everything after: UNCHANGED pickRenderer.ts DID need updating: its mid-frame 'POINT_SIZE_OFFSET' write moves from 72 to 88. The 'SELECTED_INDEX_OFFSET = 80' write stays put, because 'CameraUniforms' is exactly 80 bytes and 'selectedIndex' is the first renderer-specific field — the same byte address it occupied before the refactor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Vitest sets `import.meta.env.DEV = true` by default, so the dev-mode
WGSL logger added in the wesl-plugin bootstrap commit fires inside
postProcess.test.ts. The previous shader-module mock returned a bare
`{}`, so calling `module.getCompilationInfo()` blew up with a
TypeError. Add a no-op mock that resolves with an empty messages
array — exactly the shape the production logger pattern-matches.
This is the only mock site that needed updating; other GPU module
tests (pickRenderer, pointRenderer, ...) don't take this code path
because their shader-module call sites don't yet wire the logger.
That changes when the rest of the renderers gain `?static` imports
(later WESL conversion tasks); we'll bring their mocks up to the
same shape at that point.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
skymap | ad086b2 | Commit Preview URL Branch Preview URL |
May 07 2026, 10:02 PM |
Regression: at any zoom where fadeAlpha rose above zero, the milkyWay pipeline was invalidated and the whole frame went black. Root cause: wesl-plugin@0.6.74's linker only resolves `import` statements that appear at the top of the shader. The four `lib/math` imports were sitting next to the call sites near line 392 (idiomatic in TypeScript / Rust, but wrong here). The linker emitted the source verbatim, the `import` keyword reached Chrome's WGSL parser, and the shader module compile failed silently — surfacing as "Invalid RenderPipeline (unlabeled)" only on the first frame the renderer actually used the impostor. Fix is mechanical: move the four `import package::lib::math::*` lines up alongside the existing `lib::camera` import at the top of the file. Production bundle now contains inlined `fn rot2`, `fn sabs`, `fn toPolar`, `fn toRect` (verified by grep). This is the same constraint that broke brace-list imports earlier in this PR — wesl-plugin's parser is stricter than the WESL spec suggests. Revisit when wesl-plugin gets bumped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds `label` properties to every createShaderModule, createRenderPipeline, createBindGroup, createBindGroupLayout, createPipelineLayout, createBuffer, createTexture, and createSampler call across the renderer (41 new labels across 11 files). Routes all shader-module creation through the new src/services/gpu/shaderCompileLogger.ts helper so compile errors auto-dump the linked WGSL alongside the labelled module name. Pure metadata — zero behavioral change. Browser-console errors that previously said "(unlabeled)" now name the offending resource, which materially shortened the round-trip on the milkyWay zoom-regression debug earlier in this branch (the upstream "Invalid ShaderModule" error chain was much harder to trace without a label naming the failing module). Test mocks (pointRenderer.test.ts, pickRenderer.test.ts, postProcess.test.ts) gain a getCompilationInfo no-op shim so the helper's dev-mode logger path runs cleanly under Vitest's default import.meta.env.DEV=true. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… helpers Standalone lib file with no consumers yet. Exports three helpers extracted from the duplicated billboard-expansion patterns across points/quads/disks/ proceduralDisks: - quadCorner(vid: u32) -> vec2<f32>: 6-vertex triangle-list corner offset in [-1,+1]², replacing the per-shader CORNERS constant array. - quadUv(vid: u32) -> vec2<f32>: same as (quadCorner+1)*0.5, returning the unit-square UV in [0,1]². - expandBillboardScreen(cam, centerClip, sizePx, corner) -> vec2<f32>: clip-XY offset for a screen-pixel-sized, screen-aligned billboard. Used by points. Notable scope decisions captured in the file's docblock: - The plan's draft 'expandBillboardWorld' helper is omitted. None of the four candidate renderers actually wants a generic view-aligned world basis — quads uses a celestial-north basis, disks/proceduralDisks use orientation- driven (PA + inclination) bases that belong in lib/orientation (Task 6), and points is screen-aligned. Adding an unused helper would also force a 'view' matrix into CameraUniforms that no renderer currently needs. - The 6-vertex corner ordering follows the (BL, BR, TR, BL, TR, TL) pattern used by 3 of 4 callers; points will be migrated to match in a follow-up commit. Both orderings produce identical fragment coverage and identical interpolated UVs at every pixel inside the square (CCW triangulations of the same convex region), so the migration is safe. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the inline 6-vec CORNERS const + '(corner + 1) * 0.5' UV remap with calls to 'lib/billboard::quadCorner' and 'quadUv'. The view-aligned celestial-north basis math (NORTH_WORLD / upClip / upPx) stays renderer-specific. Also split the previously brace-listed camera import into one-per-line per the wesl-plugin gotcha. No TS-side changes; corner ordering matches the lib (BL, BR, TR, BL, TR, TL). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the inline 6-vec CORNERS const + '(corner + 1) * 0.5' UV remap with calls to 'lib/billboard::quadCorner' and 'quadUv'. The orientation-aligned disk-plane basis (PA + inclination → 'major' / 'minor_3d' in 3D world space) stays renderer-specific and untouched — that math is camera-independent and belongs to Task 6's 'lib/orientation.wesl' rather than the screen-space billboard lib. Also split the previously brace-listed camera import into one-per- line per the wesl-plugin gotcha. No TS-side changes; corner ordering matches the lib (BL, BR, TR, BL, TR, TL). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror of the disks.wesl sub-commit: replace the inline 6-vec
CORNERS const with a call to 'lib/billboard::quadCorner'. The
orientation-aligned disk-plane basis (PA + inclination →
'majorAxis' / 'minorAxis' in 3D world space) stays renderer-
specific and untouched — that math is camera-independent and
belongs to Task 6's 'lib/orientation.wesl' rather than the
screen-space billboard lib. Also split the previously brace-
listed camera import into one-per-line per the wesl-plugin
gotcha.
Unlike disks.wesl, this pass does NOT import 'quadUv': the
fragment uses the raw [-1, +1]² corner directly as the radial
coordinate ('length(in.uv)' for the bulge + disk profile), so
the [0, 1]² remap that 'quadUv' performs would be wrong here.
The 'out.uv = corner' forwarding is unchanged.
No TS-side changes; corner ordering matches the lib (BL, BR,
TR, BL, TR, TL).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the inline QUAD const-array lookup and screen-space pixel-to- clip expansion with the shared 'quadCorner' and 'expandBillboardScreen' helpers from lib/billboard.wesl. Migrates from the points-specific (BL, BR, TL, TL, BR, TR) corner ordering to the (BL, BR, TR, BL, TR, TL) ordering used by quads/disks/proceduralDisks; both are CCW triangulations of the same square and the points pipeline runs with the default cullMode: 'none', so output is byte-identical. Per-instance sizeScale (selection halo) is post-multiplied onto the helper's returned offset. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…is math
Standalone introduction of the disk-plane axis lib — no consumers
adopt it yet, so output WGSL is unchanged for existing renderers.
Both 'disks.wesl' and 'proceduralDisks.wesl' will adopt 'diskAxes' in
follow-up commits.
API: 'diskAxes(posWS, paRad, cosI, sinI) -> DiskAxes { major, minor }'.
Camera-independent by design: orientation is intrinsic to the galaxy
in 3D space (see disks.wesl's long header for why this is load-bearing).
Standardises the pole-degeneracy threshold on disks.wesl's wider ~8°
form ('abs(dot(northPole, losDir)) > 0.99') rather than
proceduralDisks.wesl's narrower ~exact-pole form ('length < 1e-4'). The
wider form wins because it eliminates a basis disagreement that could
otherwise show as an abrupt rotation at the crossfade boundary between
the procedural impostor and the textured thumbnail for any galaxy
within 8° of the celestial pole.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the inline los/north/east/major/minor derivation in disks.wesl's vertex stage with a call to 'diskAxes' from 'lib/orientation.wesl'. Byte-equivalent for disks: the lib standardised on the wider '|dot(north, los)| > 0.99' (~8°) pole-fallback threshold that disks already used, so no near-pole behaviour changes. The axisRatio 0.05 clamp + (cosI, sinI) trig pair stay at the call site by design — the lib intentionally takes pre-clamped trig inputs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switches from the wider disks form ('abs(dot(northPole, losDir)) >
0.99', ~8° from pole, swap seed BEFORE projection) to the tighter
proceduralDisks form ('length(northTangentRaw) < 1e-4', ~exactly at
pole, swap result AFTER projection). Most galaxies within 8° of the
celestial pole still produce a usable in-sky north tangent — float
math near the pole is fine until length(seed - dot(seed, los) * los)
genuinely underflows, which only happens when |dot| is essentially 1.
Net effect: PA fidelity preserved for ~8° of sky around each pole.
Both renderers still agree at the (much narrower) genuine-degeneracy
region where the fallback fires.
This is technically a behavior change for disks (a few catalog
galaxies very close to the pole no longer take the early world-Y
fallback), but the new path produces the SAME result the
proceduralDisks impostor was already producing for those galaxies —
so cross-pass consistency improves.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the inline los/north/east/major/minor derivation in proceduralDisks.wesl's vertex stage with a call to 'diskAxes' from 'lib/orientation.wesl'. Byte-equivalent for proceduralDisks: the lib standardised on the tight 'northLen < 1e-4' post-projection pole fallback that this renderer already used, so no near-pole behaviour changes. The axisRatio 0.05 clamp + (cosI, sinI) trig pair stay at the call site by design — the lib intentionally takes pre-clamped trig inputs. Final sub-commit of task 6's orientation-lib adoption. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapses duplicate fn ramp(t) between points.wesl and proceduralDisks.wesl. Both renderers now share a single color-index → RGB mapping. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds applyCloudFade(color, opacity) helper used by points.wesl and filaments.wesl. The CloudUniforms struct itself stays per-renderer: points carries a sourceCode field for pick-identity packing that filaments has no equivalent for, so a shared struct would be a fictional unification. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both renderers now import the shared struct. Filaments doesn't read sourceCode today, but the CPU-side CloudFade class already produces that exact 16-byte layout for both, so a divergent shader struct was a fictional separation. Also collapses the helper to a scalar applyCloudFade(alpha, opacity) to undo the vec4 ceremony at the call sites — both sites multiply opacity into a scalar alpha alongside other modulators. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapses three recurring smoothstep mask shapes (circular cutoff, luminance gate, axis edge-band) into named helpers. Naming the shape makes intent visible at the call site and removes per-renderer copies of the same three patterns. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner
Author
|
Renamed branch |
4 tasks
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.
Status
Draft PR. Tasks 1–4 of the 15-task migration plan are complete; tasks 5–15 are pending. Open early so the foundational tooling + uniform-layout work can be reviewed before the rest builds on it.
Summary
wesl-plugin@^0.6.74(build-time WESL→WGSL linker for Vite) wired in viavite.config.tsandvitest.config.ts. All seven shaders renamed.wgsl→.wesl; renderer imports switched from?rawto?static.lib/of shared shader modules undersrc/services/gpu/shaders/lib/:lib/math.wesl— saturate, rot2, sabs, toPolar, toRect, PI/TAU/LOG10lib/camera.wesl—CameraUniforms(80-byte universal prefix: viewProj + viewportPx + 8 B pad),worldToClip,worldToNdc,worldEyeDepthfilaments,quads,disks,proceduralDisks,milkyWayImpostor,points+pick) now embedcam: CameraUniformsas the prefix of their uniform struct, with renderer-specific fields after the shared block.What's pending (tasks 5–15)
lib/billboard.wesl,lib/orientation.wesl,lib/colorIndex.wesl,lib/cloudFade.wesl,lib/masks.wesl,lib/astro.wesl,lib/tonemap.wesl,lib/util.wesl, then the uniform vertex/fragment/io file split for every renderer (pointsgets a special two-fragment-file split for color+pick).Spec & plan
docs/superpowers/specs/2026-05-07-wesl-conversion-design.mddocs/superpowers/plans/2026-05-07-wesl-conversion.mdFindings baked into the codebase + plan
Three concrete WESL-tooling gotchas surfaced and are documented inline:
`in any comment (//or/* */). Project-wide substitution`→'applied to every shader; doesn't affect.ts/.md. Reversible if upstream fixes the parser.tsconfigtypesarray entry alone doesn't reliably resolve subpath types.src/@types/wesl.d.tscarries a/// <reference types="wesl-plugin/suffixes" />triple-slash to make*?staticresolve tostringfrom any compiler entry.vitest.config.tsregisterswesl-plugindirectly; without it the SSR transform pipeline tries to parse.weslfiles as JavaScript and rolldown rejects them.Test plan
npm run typecheckgreen (both src and tools tsconfigs)npm run buildgreennpm testgreen — 895/895 across 117 test fileslocalhost:5173(main worktree) vslocalhost:5174(this branch worktree) before un-draftingNotes
mainafter the post-process aggregate (refactor(gpu): postProcess aggregate replaces hdrTarget + toneMapPass #33) +(source, localIdx)packing (refactor(gpu): pack (source, localIdx) directly; delete priorCount machinery #35) + pick-coupling (refactor(gpu): bind PickRenderer to PointRenderer at construction #32) PRs landed. The merge-resolution preserves main'sselectedPackedsemantics (bytes 80–83) while threading ourCameraUniforms80-byte prefix into bytes 0–79 and migratingpointSizePx/brightnessinto bytes 88–95 (formerly_pad0/_pad1).my-feature-pre-rebaseleft locally as a safety net during execution; safe to ignore.🤖 Generated with Claude Code