diff --git a/shared/rast/Makefile b/shared/rast/Makefile new file mode 100644 index 0000000000..c57fa2fdf2 --- /dev/null +++ b/shared/rast/Makefile @@ -0,0 +1,92 @@ +## shared/rast — build the cross-platform rasterizer for every target. +## +## Targets: +## test → native test binary (gcc), runs the self-tests +## native → static archive (libac_rast.a) for linking into ac-native +## wasm → wasm modules for the web runtime (needs emscripten in PATH): +## raster-baseline.{wasm,js} — no SIMD, no threads +## raster-simd.{wasm,js} — SIMD, no threads +## raster-threads.{wasm,js} — threads + SAB +## raster-full.{wasm,js} — SIMD + threads +## clean +## +## Pixel output must stay identical across test / native / every wasm +## variant. raster_test.c exercises the correctness path; a follow-up +## snapshot harness (golden PNG diff) covers the richer drift catch. + +CC ?= gcc +CFLAGS ?= -O3 -Wall -Wextra -std=c99 -fPIC +AR ?= ar +EMCC ?= emcc + +RAST_SRC := raster.c +RAST_OBJ := raster.o + +.PHONY: all test native wasm clean + +all: test + +# ── self-test (gcc native) ─────────────────────────────────────────── +raster_test: $(RAST_SRC) raster_test.c raster.h + $(CC) $(CFLAGS) -o $@ $(RAST_SRC) raster_test.c -lm + +test: raster_test + ./raster_test + +# ── static archive for ac-native link ──────────────────────────────── +$(RAST_OBJ): $(RAST_SRC) raster.h + $(CC) $(CFLAGS) -c -o $@ $< + +libac_rast.a: $(RAST_OBJ) + $(AR) rcs $@ $^ + +native: libac_rast.a + +# ── WASM variants (emscripten) ─────────────────────────────────────── +# Each variant exports the same C API, so the JS loader probes features +# and picks the best one the browser can actually run. See raster.h for +# the exported entry points. +# +# Build flags (same across variants for everything NOT related to the +# capability being toggled, so variants stay pixel-identical): +# -O3 — maximum inlining and loop opts +# -flto — link-time optimization +# -s STANDALONE_WASM=1 — emit a standalone module (no Emscripten JS shim) +# -s EXPORTED_FUNCTIONS=['_ac_rast_triangle','_ac_rast_clear'] +# -s ENVIRONMENT='web,worker' — only ship web code paths +# +# Variant-specific flags: +# baseline → (none) +# simd → -msimd128 +# threads → -pthread -s PTHREAD_POOL_SIZE='navigator.hardwareConcurrency' +# full → -msimd128 -pthread … + +EMCC_COMMON := -O3 -flto \ + -s STANDALONE_WASM=1 \ + -s EXPORTED_FUNCTIONS='["_ac_rast_triangle","_ac_rast_clear","_malloc","_free"]' \ + -s EXPORTED_RUNTIME_METHODS='["HEAPU8","HEAPU32","HEAPF32"]' \ + -s ENVIRONMENT='web,worker' \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s MALLOC=emmalloc + +raster-baseline.wasm: $(RAST_SRC) raster.h + $(EMCC) $(EMCC_COMMON) -o $@ $(RAST_SRC) + +raster-simd.wasm: $(RAST_SRC) raster.h + $(EMCC) $(EMCC_COMMON) -msimd128 -o $@ $(RAST_SRC) + +raster-threads.wasm: $(RAST_SRC) raster.h + $(EMCC) $(EMCC_COMMON) -pthread \ + -s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=8 \ + -o $@ $(RAST_SRC) + +raster-full.wasm: $(RAST_SRC) raster.h + $(EMCC) $(EMCC_COMMON) -msimd128 -pthread \ + -s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=8 \ + -o $@ $(RAST_SRC) + +wasm: raster-baseline.wasm raster-simd.wasm raster-threads.wasm raster-full.wasm + +clean: + rm -f raster_test $(RAST_OBJ) libac_rast.a \ + raster-baseline.* raster-simd.* raster-threads.* raster-full.* diff --git a/shared/rast/README.md b/shared/rast/README.md new file mode 100644 index 0000000000..4a6a887a0e --- /dev/null +++ b/shared/rast/README.md @@ -0,0 +1,121 @@ +# 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](#cross-browser-support) below. + +## Integration status + +- [x] Pure-C rasterizer + self-tests +- [x] Makefile for native + wasm build targets +- [ ] Wire into `fedac/native/src/graph3d.c` (replace `rasterize_triangle`) +- [ ] emcc build variants produced + smoke-tested via wasmtime or node +- [ ] JS loader in `system/public/aesthetic.computer/lib/graph.mjs` that + probes caps and instantiates the best module +- [ ] Replace `graph.mjs`'s JS triangle inner loop with a WASM call +- [ ] Golden PNG snapshot diff in CI (catches any drift between variants) +- [ ] Tile-binning + multi-worker pipeline (unlocks `threads`/`full` variants) + +## 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.