Skip to content

[for reference] feat: TypeGPU core shaders - #46

Open
iwoplaza wants to merge 5 commits into
pmndrs:mainfrom
iwoplaza:feat/typegpu-core-shaders
Open

[for reference] feat: TypeGPU core shaders#46
iwoplaza wants to merge 5 commits into
pmndrs:mainfrom
iwoplaza:feat/typegpu-core-shaders

Conversation

@iwoplaza

@iwoplaza iwoplaza commented Aug 8, 2026

Copy link
Copy Markdown

Changes:

  • Since the code of the library needs to be transformed by unplugin-typegpu, I adopted tsdown. However, if preferred, babel can be used on the files produced by the TypeScript compiler.
  • Core shader logic has been translated from TSL to TypeGPU shader functions. There are some workarounds accounting for limitations around @typegpu/gl and @typegpu/three, but I am resolving those promptly 🫡

iwoplaza and others added 5 commits August 7, 2026 11:02
Move the analytic Slug fill out of TSL into `slug-shaders/core`, whose
modules import TypeGPU only. The q-form solver, root-eligibility table,
per-curve coverage and weight, band header and reference bit layout,
fragment scale, thickening, weighted blend, and vertex dilation are now
expressed once and are callable from vanilla WebGPU, vanilla WebGL,
TypeGPU, or Three.js.

Both axes reach one curve evaluator and one quadratic solver, because a
vertical band is the transposed horizontal band with the opposite winding
sense. The modules beside the core stay the Three.js host and keep the
band loop, its sorted-reference terminator, the 2D grid addressing, and
every texture read: `@typegpu/three` has no texture bridge, and grid
addressing exists only because these resources are textures.

Every crossing goes through `coreValue`, which assigns the result to a
named TSL property. That is load-bearing rather than cosmetic. The bridge
resolves a core call by traversing the TSL nodes it reads, and TypeGPU's
resolution context is a single stack, so a core node reachable through
another core call's arguments resolves re-entrantly and fails a
resolution-stack assertion. `.toVar()` is not sufficient, because a
variable node's traversal still descends into its initializer.

Two correctness fixes come along: the horizontal solve's `b` coefficient
is `p0.y - p1.y`, matching `reference.ts` and the vertical solver, and the
core clamps to the unit interval instead of calling `std.saturate`, which
WGSL defines and GLSL does not, so the same functions now link on WebGL2.

Add a device-free test that compiles the staged Slug material through both
Three.js node builders and holds one declaration per core function, one
statement per boundary, no boundary variable shadowing a core function,
and no WGSL-only builtin in the GLSL output. Weakening `coreValue` makes
it fail with the same assertion the browser reports.

Exclude emitted declarations from the shader build plugin, which its
default include would otherwise select and then fail to parse as
non-ambient TypeScript.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`typegpu`, `@typegpu/three`, and `@typegpu/gl` are peers for the same
reason Three.js and React are: each keys its identity to a single
instance, so a duplicate copy breaks interop rather than merely wasting
bytes. `typegpu` warns on a duplicate version and derives every internal
symbol from its version string, and `@typegpu/three` already declares
`three`, `typegpu`, and `@typegpu/gl` as its own peers.

They are optional because only the Slug subpath needs them, exactly as
`react` and `@react-three/fiber` are optional for the React subpath, so a
Bitmap-only or MTSDF-only consumer installs nothing extra. The declared
ranges mirror the upstream contract while the exact pkg.pr.new builds stay
in devDependencies, which is how the `three` peer is already handled.

The package-size boundary follows the dependency boundary: peer runtimes
are outside the reviewed ceilings because the consumer installs and
dedupes them. This moves the Slug graph from 944,637 to 445,687 raw bytes
and from 160,911 to 89,110 gzip, leaving only this package's own emitted
shader code inside the measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regenerate the report, which the tsdown/Rolldown emit commit left stale.
Two causes are separated in the recorded numbers.

Slug's own shipped shader code grows because the build plugin emits a
transpiled syntax tree per shader function for runtime resolution: roughly
45,000 raw and 41,000 minified bytes. That metadata compresses well, so
Slug's reviewed gzip and Brotli ceilings are unchanged and still shared
with the other two rasters, and only its uncompressed ceilings rise. Its
retained-capacity baseline is rebased rather than given a five-fold
allowance, which would have stopped the assertion bounding anything.

Everything else is drift from the emit migration, between 85 and 211
bytes across `browser-core`, both shared-graph rasters, and the baker
worker; those graphs' own code did not change. One Wasm brotli growth
allowance was already exceeded against the committed report before any of
this work and is corrected to the recorded figure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add the host-agnostic core and its TypeGPU-to-TSL boundary as cited
sources, describe what is portable and what stays with the Three.js host,
state the single-resolution-stack invariant the boundary enforces, and
record the peer-dependency reasoning and the size consequence. Refresh
both package digests, which the preceding commit left stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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