WGSL → WESL conversion + shared shader library#39
Merged
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>
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>
Names the distance-modulus formula that was inlined in points.wesl (appMag + Mpc distance -> absolute magnitude) as 'distanceModulus' in a new astronomy-flavoured lib module. Future shaders that need to convert between apparent and absolute magnitudes get a domain-named import rather than re-deriving log10 via the natural-log + LOG10 dance, and the inline 'let LOG10 = ...' shadowing in points.wesl goes away. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5 tasks
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
skymap | cd7c630 | Commit Preview URL Branch Preview URL |
May 07 2026, 10:57 PM |
Moves the five tone-map curves (linear / reinhard / asinh / gamma2 / aces) out of toneMap.wesl into a shared lib so future renderers that want to do their own tone-mapping (e.g. an HDR thumbnail pass) can reuse them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pull five orphan helpers out of milkyWayImpostor.wesl into a shared 'util' lib: hash21 (ex 'rand'), valueNoise2 (ex 'noise1'), raySphere, worldToGalactic, galacticToShader. Renames are the real win — 'rand' was a deterministic hash misnamed as a PRNG, and 'noise1' was a numbered slot with no companion. Skipped the plan's linearToSRGB / srgbToLinear / encodePickId candidates: zero current callers (sRGB) or one-line literal at one site (pick id), so adding them would be speculative dead code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ment,pickFragment}.wesl Replaces the single ~1500-line points.wesl with four files under points/: - points/io.wesl — shared structs (Uniforms, PerVertex, VSOut) - points/vertex.wesl — @vertex fn vs (used by both renderers) - points/colorFragment.wesl — @Fragment fn fs (pointRenderer) - points/pickFragment.wesl — @Fragment fn fsPick (pickRenderer) Each renderer pipeline now builds a separate vertex + fragment shader module from disjoint sources. This eliminates a class of selection- on-wrong-galaxy bugs that came from sharing one module between two pipelines with diverging fragment paths. WESL has no global state, so '@group/@binding' declarations cannot be exported across modules. The bindings ('u', 'cloud') are re-declared in vertex.wesl and colorFragment.wesl using the structs imported from io.wesl, so the layouts are guaranteed to match; pickFragment.wesl declares no bindings since it only reads VSOut. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ex,fragment}.wesl Mirrors the points/* split (task 13). The procedural-galaxy helpers (stars, height, galaxyNormal, shadeGalaxyDisk, renderGalaxy) stay in the fragment file — they're fragment-only and not reused. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t}.wesl Mirrors the points/ and milkyWay/ splits (tasks 13-14). Each pipeline now builds a separate vertex + fragment shader module from disjoint sources. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gment}.wesl Mirrors the points/ and milkyWay/ splits (tasks 13-14). Each pipeline now builds a separate vertex + fragment shader module from disjoint sources. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the points/ and milkyWay/ splits (tasks 13-14). Each pipeline now builds a separate vertex + fragment shader module from disjoint sources. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the points/ and milkyWay/ splits (tasks 13-14). Each pipeline now builds a separate vertex + fragment shader module from disjoint sources. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…o,vertex,fragment}.wesl Mirrors the points/ and milkyWay/ splits (tasks 13-14). Each pipeline now builds a separate vertex + fragment shader module from disjoint sources. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolves package-lock.json conflict by taking main's lockfile and regenerating with npm install to merge in wesl + wesl-plugin deps alongside main's MSDF label deps (msdf-bmfont-xml). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
….wesl Brings the MSDF labels foundation (added on main in #38) under the WESL convention: directory layout, three-file split, lib/camera adoption. The Uniforms struct now embeds 'cam: CameraUniforms' (80-byte universal prefix) instead of declaring its own viewProj+viewport pair — both layouts are byte-equivalent at offsets 0..79, so no CPU-side change is required when the labels renderer eventually wires up a uniform writer. No consumer existed yet (foundation-only PR), so no renderer.ts to update. Co-Authored-By: Claude Opus 4.7 <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.
Summary
Full migration from raw WGSL to WESL (WebGPU Shading Extended Language) with build-time linking via
wesl-plugin, plus a complete reorganisation of the shader tree.wesl-plugin@^0.6.74wired intovite.config.tsandvitest.config.ts. All seven shaders renamed.wgsl→.wesl; renderer imports switched from?rawto?static(build-time linked).lib/of 10 modules undersrc/services/gpu/shaders/lib/:math.wesl— saturate, rot2, sabs, toPolar, toRect, PI/TAU/LOG10camera.wesl—CameraUniforms(80-byte universal prefix),worldToClip,worldToNdc,worldEyeDepthbillboard.wesl— quadCorner, quadUv, expandBillboardScreenorientation.wesl—DiskAxes+diskAxes(posWS, paRad, cosI, sinI)for galaxy disk-plane mathcolorIndex.wesl—ramp(t)color-index → RGBcloudFade.wesl— unifiedCloudUniforms+ scalarapplyCloudFade(alpha, opacity)masks.wesl—circularMask,lumAlpha,edgeBandMaskastro.wesl—distanceModulus(appMag, distMpc)tonemap.wesl— five tone-map curves (linear / reinhard / asinh / gamma2 / aces)util.wesl—hash21,valueNoise2,raySphere,worldToGalactic,galacticToShader(orphan utilities)<name>/{io,vertex,fragment}.wesl.points/is special-cased with FOUR files —io,vertex,colorFragment(pointRenderer),pickFragment(pickRenderer). This eliminates the class of selection-on-wrong-galaxy bugs that came from sharing one shader module between two pipelines with diverging fragment paths.@if(PICK)conditional-compilation path with a clean two-fragment-file split.Spec:
docs/superpowers/specs/2026-05-07-wesl-conversion-design.mdPlan:
docs/superpowers/plans/2026-05-07-wesl-conversion.mdSide benefits
createShaderModule,createBuffer,createTexture,createBindGroup,createRenderPipeline,createPipelineLayout,createBindGroupLayoutnow has a meaningfullabelfor debuggability. Browser-console errors that previously said(unlabeled)now name the offending resource.createShaderModuleWithDevLoghelper (src/services/gpu/shaderCompileLogger.ts): every renderer routes shader-module creation through this wrapper, which logs the linked WGSL alongside any compile-time error in dev mode. Maps "WGSL line 142 error" back to the source.weslfile.Findings baked into the codebase + plan
Seven concrete WESL-tooling gotchas surfaced and are documented in a personal skill at
~/.claude/skills/wesl-shaders/:`in any comment (//or/* */). Project-wide substitution`→'applied to every shader.package, not the npm name.import package::lib::math::saturate(verified againstnode_modules/wesl/dist/index.js).import path::{ a, b };doesn't link in this plugin version. One identifier per line.lib/math.weslexportssaturate, notlib/math/saturate.wesl.vitest.config.tsregisterswesl-plugindirectly.src/@types/wesl.d.tscarries/// <reference types="wesl-plugin/suffixes" />for*?staticto resolve tostring.Test plan
npm run typecheckgreen (both src and tools tsconfigs)npm run buildgreennpm testgreen — 895/895 across 117 test fileslocalhost:5174(this branch's worktree) — every renderer output identical to pre-migration: pan / zoom / rotate, click a galaxy (pick), tier-swap (cloudFade), tone-map dropdown cycles all five curves, milkyWay impostor renders procedural disk + stars, filaments + disks + quads + proceduralDisks all green.Notes
my-featuretofeat/wesl-conversion. Original draft PR WGSL → WESL conversion + shared shader library (in progress) #36 was closed and superseded by this one (WGSL → WESL conversion + shared shader library #39).points/has TWO fragment files (colorFragment.weslfor pointRenderer,pickFragment.weslfor pickRenderer). Both renderers build their own vertex module frompoints/vertex.wesl?static— separatecreateShaderModulecalls, no cross-renderer module sharing (would re-introduce the WebGPU bind-group-layout-auto-derived trap).@group/@bindingnumbers. WESL has no global state, so binding declarations can't be imported across modules — but WGSL accepts redeclared bindings as long as the layout matches.🤖 Generated with Claude Code