Monorepo for Aesthetic.Computer aesthetic.computer
README.md

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:

  1. Split screen into 64×64 tiles
  2. Transform all vertices on the main thread
  3. Bucket each triangle into the tiles it covers
  4. Dispatch tiles to a worker pool (Web Workers on web, pthreads on native)
  5. Each worker rasterizes its tiles using ac_rast_triangle with a scissor rect set to the tile bounds
  6. 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.