Skip to content

Orientation discipline

The entire deformation chain works in texture space: v grows downward, row 0 is the top. Fullscreen-triangle vertex shaders emit st = (u, 1 − v_clip) — the texture-space coordinate of the fragment’s own texel — and every pass (identity init, each deformation, the composite) operates purely in that space. Mouse and touch coordinates, texture rows and screen rows all share it.

The rationale was found the hard way on Metal: sampling at the raw NDC interpolant vertically flips the field on every offscreen pass, so consecutive deformations cancel instead of composing. With st, a passthrough is a true no-op and deformations compose exactly. The regression check is simple to state: forty passes of a z/40 tine must equal one pass of a z tine.

Metal and D3D11 share a top-left row origin; OpenGL and GLES default to bottom-left; WebGPU is top-left. Any vertical flip a backend needs lives exclusively in the final swapchain composite pass and, mirrored, in the print readback path. The deformation chain contains no backend-specific branch — not one #ifdef.

Because the chain is identical everywhere, its output must be too. The regression runs the canonical deformation script (drops, tines, vortices, wake, pinch, ripple, swirl) from an identity field and reads the raw RGBA16F field back:

BackendResult against the Metal fixture
Metal (macOS, iOS)the reference fixture
D3D11 (Windows, real GPU)bitwise identical
OpenGL 4.1 (Linux, real GPU), GLES 3 (Android)bitwise identical
D3D11 on WARP, GL on Mesa llvmpipe (the GitHub runners, no GPU)mean 6.0e-4 from the Apple fixture — and 1.8e-5 from each other: half-float rounding over the seven passes, judged at the second tier (2.5e-2 / 1e-3)
WebGPU (browser, wasm)within tolerance: max Δ under 1e-3, mean under 1e-8 — an order of magnitude inside the documented 1e-2 / 1e-4 web tier

The release workflow runs it on macOS, Windows and Linux runners through each real renderer before any artifact is built, with a negative control — a deliberately perturbed dump — proving the gate can fail. The web tier runs headlessly in Chrome against the same fixture.

A corollary invariant with its own check: the washi fibre and grain are sampled in screen space, never through the deformed field. Run a tine and a 120 BPM piano-roll scroll over textured output and the grain must stay put while the ink moves.

midi-sink 2.0.0-rc.5 · AGPL-3.0 · source