shared/rast — cross-platform triangle rasterizer #
One C99 triangle rasterizer used by both ac-native (fedac/native) and the web runtime (system/public/aesthetic.computer). Same pixel output on both platforms — no more drift between what a piece looks like in a browser and what it looks like on bare metal.
Why #
Before this, arena.mjs (and other 3D pieces) ran through two completely
independent rasterizers — graph.mjs's JS triangle path on web, and
graph3d.c's software rasterizer on native. Both implementations had to be
maintained in parallel and drifted in subtle ways: different pixel coverage
rules, different near-plane behaviour, different color precision. The fix
is structural: one source of truth, multiple build targets.
Source #
raster.h— public API (pixel format, vertex struct, options, entry points)raster.c— implementation (~180 lines, pure C99, no libc allocs on the hot path, no mutable globals — fully re-entrant so worker threads can rasterize independent tiles against shared framebuffers)raster_test.c— self-tests (solid fill, perspective-correct color interpolation, depth occlusion, scissor clipping)
Build #
make test # run self-tests (gcc)
make native # libac_rast.a for linking into ac-native
make wasm # four WASM variants for the web runtime (needs emcc)
make clean
The wasm target emits four modules with progressive feature sets. The
web loader probes browser capability and picks the best one available:
| Variant | Tradeoff |
|---|---|
raster-baseline.wasm |
ubiquitous; 2-3× faster than current JS |
raster-simd.wasm |
needs WASM SIMD (Chrome 91+, FF 89+, Safari 16.4+); 4-6× |
raster-threads.wasm |
needs SharedArrayBuffer (COOP+COEP on server); 8-15× |
raster-full.wasm |
SIMD + threads; 20-40× on multicore desktops |
See the compatibility matrix below.
Integration status #
Cross-browser support #
Graceful fallback — every current browser gets at minimum the baseline WASM (2-3× faster than today's JS). Better variants unlock automatically:
| Feature | Chrome/Edge | Firefox | Safari |
|---|---|---|---|
| Baseline WASM | 57+ (2017) | 52+ (2017) | 11+ (2017) |
| SharedArrayBuffer | 68+ (2018, isolated 91+) | 79+ (2020) | 15.2+ (Dec 2021) |
| Atomics | same as SAB | same | same |
| WASM SIMD (128-bit) | 91+ (2021) | 89+ (2021) | 16.4+ (Mar 2023) |
| WASM threads | same as SAB | same | same |
Hosting prerequisites for the parallel path #
The server must send these headers to enable crossOriginIsolated:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
The docs route already does this (see system/netlify/functions/docs.js).
Extending to the piece-hosting routes is a separate commit. If third-party
iframes (YouTube, Spotify, arbitrary user URLs) need to still work, use
COEP: credentialless instead of require-corp — same isolation, fewer
embedding breakages.
Pixel format #
All framebuffers use uint32_t per pixel packed as:
(A << 24) | (R << 16) | (G << 8) | B
Matches graph3d.c's existing convention and web's Uint32Array view over
a Uint8ClampedArray. The AC_RAST_PACK(a,r,g,b) macro constructs one.
Depth buffer #
32-bit float per pixel, parallel to the color buffer. Lower values =
closer to camera (matching graph3d.c but inverted from WebGL's default
— callers that pass post-perspective-divide Z in [-1,+1] should negate
before calling if they want standard z-buffer semantics).
Modes:
AC_RAST_DEPTH_RW— read + write (standard opaque pass)AC_RAST_DEPTH_READONLY— read only (transparent / overlay pass)AC_RAST_DEPTH_NONE— no test / no write (painter's-order fallback, also what pieces without a depth buffer at all use)
Parallelism plan #
The rasterizer is designed so each triangle call is independent. A tile binner — not yet implemented here — will:
- Split screen into 64×64 tiles
- Transform all vertices on the main thread
- Bucket each triangle into the tiles it covers
- Dispatch tiles to a worker pool (Web Workers on web, pthreads on native)
- Each worker rasterizes its tiles using
ac_rast_trianglewith a scissor rect set to the tile bounds - Main thread waits on a completion counter, flips buffers, presents
Workers never touch each other's tiles, so no locking is needed beyond the submit/complete atomics. SharedArrayBuffer holds the color + depth backing store; workers write with normal pointer ops.