diff --git a/shared/rast/raster.c b/shared/rast/raster.c new file mode 100644 index 000000000..6393e5944 --- /dev/null +++ b/shared/rast/raster.c @@ -0,0 +1,171 @@ +/* + * raster.c — cross-platform triangle rasterizer + * + * Port of fedac/native/src/graph3d.c's rasterize_triangle + web/graph.mjs's + * equivalent triangle path, unified as one implementation. See raster.h for + * the API contract + platform rationale. + * + * Pixel output is intended to be exact-match between native and WASM builds + * of this file — the test harness in raster_test.c checks this by rendering + * a fixed triangle and diffing against a golden PNG. + */ +#include "raster.h" +#include + +static inline int mini(int a, int b) { return a < b ? a : b; } +static inline int maxi(int a, int b) { return a > b ? a : b; } +static inline float minf(float a, float b) { return a < b ? a : b; } +static inline float maxf(float a, float b) { return a > b ? a : b; } +static inline float clampf(float v, float lo, float hi) { + return maxf(lo, minf(hi, v)); +} + +static uint32_t sample_texture(const ACRastTexture *tex, float u, float v) { + /* GL_REPEAT — modulo, wrap negative. */ + u = u - floorf(u); + v = v - floorf(v); + int tx = (int)(u * tex->width); + int ty = (int)(v * tex->height); + if (tx < 0) tx += tex->width; + if (ty < 0) ty += tex->height; + if (tx >= tex->width) tx = tex->width - 1; + if (ty >= tex->height) ty = tex->height - 1; + return tex->pixels[ty * tex->stride + tx]; +} + +void ac_rast_clear(ACRastTarget *t, uint32_t color, float depth) { + if (!t) return; + int n = t->height * t->stride; + for (int i = 0; i < n; i++) t->pixels[i] = color; + if (t->depth) { + for (int i = 0; i < n; i++) t->depth[i] = depth; + } +} + +void ac_rast_triangle(ACRastTarget *t, + const ACRastVertex *v0, + const ACRastVertex *v1, + const ACRastVertex *v2, + const ACRastOptions *opts) { + if (!t || !t->pixels || !v0 || !v1 || !v2 || !opts) return; + + /* Bounding box, clipped to target + scissor. */ + int min_x = (int)floorf(minf(v0->sx, minf(v1->sx, v2->sx))); + int max_x = (int)ceilf (maxf(v0->sx, maxf(v1->sx, v2->sx))); + int min_y = (int)floorf(minf(v0->sy, minf(v1->sy, v2->sy))); + int max_y = (int)ceilf (maxf(v0->sy, maxf(v1->sy, v2->sy))); + + int tx0 = 0, ty0 = 0, tx1 = t->width - 1, ty1 = t->height - 1; + if (opts->scissor_x1 > opts->scissor_x0 && opts->scissor_y1 > opts->scissor_y0) { + tx0 = maxi(tx0, opts->scissor_x0); + ty0 = maxi(ty0, opts->scissor_y0); + tx1 = mini(tx1, opts->scissor_x1); + ty1 = mini(ty1, opts->scissor_y1); + } + min_x = maxi(min_x, tx0); + min_y = maxi(min_y, ty0); + max_x = mini(max_x, tx1); + max_y = mini(max_y, ty1); + if (min_x > max_x || min_y > max_y) return; + + /* Edge function setup (matches graph3d.c). */ + float dx01 = v1->sx - v0->sx, dy01 = v1->sy - v0->sy; + float dx12 = v2->sx - v1->sx, dy12 = v2->sy - v1->sy; + float dx20 = v0->sx - v2->sx, dy20 = v0->sy - v2->sy; + + float area = dx01 * (v2->sy - v0->sy) - dy01 * (v2->sx - v0->sx); + if (fabsf(area) < 0.001f) return; /* degenerate */ + float inv_area = 1.0f / area; + + /* 1/w for perspective-correct interpolation. If caller passed w==1 + * for every vertex (affine mode), inv_w == 1 throughout and the per- + * pixel perspective divide collapses to a no-op. */ + float inv_w0 = 1.0f / (v0->w != 0.0f ? v0->w : 1.0f); + float inv_w1 = 1.0f / (v1->w != 0.0f ? v1->w : 1.0f); + float inv_w2 = 1.0f / (v2->w != 0.0f ? v2->w : 1.0f); + + const ACRastTexture *tex = (opts->fill == AC_RAST_FILL_TEXTURE) ? opts->texture : NULL; + int has_depth = (t->depth && opts->depth_mode != AC_RAST_DEPTH_NONE); + int depth_write = has_depth && opts->depth_mode == AC_RAST_DEPTH_RW; + + for (int y = min_y; y <= max_y; y++) { + int row = y * t->stride; + for (int x = min_x; x <= max_x; x++) { + float px = x + 0.5f, py = y + 0.5f; + + /* Barycentric coordinates (edge function). */ + float b0 = (dx12 * (py - v1->sy) - dy12 * (px - v1->sx)) * inv_area; + float b1 = (dx20 * (py - v2->sy) - dy20 * (px - v2->sx)) * inv_area; + float b2 = 1.0f - b0 - b1; + if (b0 < 0.0f || b1 < 0.0f || b2 < 0.0f) continue; + + /* Perspective-correct weight. */ + float inv_w = b0 * inv_w0 + b1 * inv_w1 + b2 * inv_w2; + float w_interp = 1.0f / inv_w; + + /* Depth interpolation + test. */ + int idx = row + x; + if (has_depth) { + float z = (b0 * v0->z * inv_w0 + + b1 * v1->z * inv_w1 + + b2 * v2->z * inv_w2) * w_interp; + if (z >= t->depth[idx]) continue; + if (depth_write) t->depth[idx] = z; + } + + uint32_t pixel; + switch (opts->fill) { + case AC_RAST_FILL_TEXTURE: { + float u = (b0 * v0->u * inv_w0 + + b1 * v1->u * inv_w1 + + b2 * v2->u * inv_w2) * w_interp; + float v = (b0 * v0->v * inv_w0 + + b1 * v1->v * inv_w1 + + b2 * v2->v * inv_w2) * w_interp; + pixel = sample_texture(tex, u, v); + } break; + case AC_RAST_FILL_COLOR: { + float r = (b0 * v0->r * inv_w0 + + b1 * v1->r * inv_w1 + + b2 * v2->r * inv_w2) * w_interp; + float g = (b0 * v0->g * inv_w0 + + b1 * v1->g * inv_w1 + + b2 * v2->g * inv_w2) * w_interp; + float bl = (b0 * v0->b * inv_w0 + + b1 * v1->b * inv_w1 + + b2 * v2->b * inv_w2) * w_interp; + float al = (b0 * v0->a * inv_w0 + + b1 * v1->a * inv_w1 + + b2 * v2->a * inv_w2) * w_interp; + uint8_t ri = (uint8_t)(clampf(r, 0.0f, 1.0f) * 255.0f); + uint8_t gi = (uint8_t)(clampf(g, 0.0f, 1.0f) * 255.0f); + uint8_t bi = (uint8_t)(clampf(bl, 0.0f, 1.0f) * 255.0f); + uint8_t ai = (uint8_t)(clampf(al, 0.0f, 1.0f) * 255.0f); + pixel = AC_RAST_PACK(ai ? ai : 255, ri, gi, bi); + } break; + case AC_RAST_FILL_SOLID: + default: + pixel = opts->solid_color; + break; + } + + /* Near-plane fade — mirror graph3d.c when !no_fade. The ramp + * is intentionally gentle so UI-layer triangles (no_fade=1) + * look identical to 3D content far from the camera. */ + if (!opts->no_fade) { + /* w_interp in camera space — larger = farther. The fade + * tapers in the first 0.5 units, matching graph3d.c. */ + float fade = clampf(w_interp * 2.0f, 0.0f, 1.0f); + if (fade < 1.0f) { + uint8_t a = AC_RAST_A(pixel); + uint8_t r = (uint8_t)(AC_RAST_R(pixel) * fade); + uint8_t g = (uint8_t)(AC_RAST_G(pixel) * fade); + uint8_t b = (uint8_t)(AC_RAST_B(pixel) * fade); + pixel = AC_RAST_PACK(a, r, g, b); + } + } + + t->pixels[idx] = pixel; + } + } +} diff --git a/shared/rast/raster.h b/shared/rast/raster.h new file mode 100644 index 000000000..7d1b8fbc3 --- /dev/null +++ b/shared/rast/raster.h @@ -0,0 +1,118 @@ +/* + * Aesthetic Computer — shared software rasterizer. + * + * One pure-C triangle rasterizer that ships two ways: + * 1. Linked into ac-native (fedac/native/) so notepat / arena / etc. use + * the same pixel output on bare metal as they do in the browser. + * 2. Compiled via Emscripten into .wasm for the web runtime, replacing + * the JS triangle path in system/public/aesthetic.computer/lib/graph.mjs + * for big perf gains (2-3× baseline, 20-40× with SIMD + worker-tiled). + * + * Rules for keeping parity portable: + * - No libc allocs inside the hot path. Caller owns framebuffer + depth. + * - No globals / mutable module state — every call is fully re-entrant + * so workers can rasterize independent tiles in parallel. + * - Pure C99. No POSIX, no SSE/AVX intrinsics here (those belong in + * emcc's simd128 path, gated by separate build flags). + * - Pixel format is uint32_t BGRA packed as (A<<24)|(R<<16)|(G<<8)|B, + * matching graph3d.c and graph.mjs's Uint32Array view. + */ +#ifndef AC_RAST_H +#define AC_RAST_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Pixel format helpers — keep in sync with fedac/native/src/graph3d.c. */ +#define AC_RAST_PACK(a, r, g, b) \ + (((uint32_t)(a) << 24) | ((uint32_t)(r) << 16) | \ + ((uint32_t)(g) << 8) | (uint32_t)(b)) +#define AC_RAST_A(c) (((c) >> 24) & 0xFFu) +#define AC_RAST_R(c) (((c) >> 16) & 0xFFu) +#define AC_RAST_G(c) (((c) >> 8) & 0xFFu) +#define AC_RAST_B(c) ( (c) & 0xFFu) + +typedef struct { + uint32_t *pixels; /* row-major, AC_RAST_PACK-encoded */ + float *depth; /* row-major, same dims; NULL disables depth test */ + int width, height; + int stride; /* elements per row (usually == width) */ +} ACRastTarget; + +typedef struct { + /* Post-perspective-divide screen-space position. + * x, y : pixel coordinates inside the target + * z : depth (lower = closer, matches graph3d.c convention) + * w : 1/w-free interpolation denominator — pass the original + * clip-space w from the vertex shader. If the caller isn't + * doing perspective-correct interpolation, pass 1.0f. */ + float sx, sy, z, w; + + /* Optional per-vertex attributes. Unused channels are ignored based + * on the mode flag in ACRastOptions, so callers can leave them zero. */ + float r, g, b, a; /* [0..1] linear RGBA */ + float u, v; /* texture coords, wrapped */ +} ACRastVertex; + +typedef struct { + const uint32_t *pixels; + int width, height, stride; +} ACRastTexture; + +typedef enum { + AC_RAST_FILL_SOLID = 0, /* single flat color from opts->solid_color */ + AC_RAST_FILL_COLOR = 1, /* perspective-correct per-vertex RGBA */ + AC_RAST_FILL_TEXTURE = 2 /* sample from opts->texture with wrapped UV */ +} ACRastFill; + +typedef struct { + ACRastFill fill; + uint32_t solid_color; /* AC_RAST_PACK-encoded */ + const ACRastTexture *texture; + + /* Near-plane fade (matches graph3d.c no_fade semantics). When non-zero, + * pixels close to w≈0 get darkened for a cheap cinematic feel. Set to + * 1 to skip — required for UI layers and colored overlays. */ + int no_fade; + + /* Depth test mode. + * 0 = read + write (standard z-buffer) + * 1 = read-only (transparent/overlay passes) + * 2 = no test / no write (painter's-order fallback) */ + int depth_mode; + + /* Optional scissor rect (all zeros disables). Pixels outside are not + * touched — useful for tile-parallel rasterization where each worker + * owns a screen region. */ + int scissor_x0, scissor_y0, scissor_x1, scissor_y1; +} ACRastOptions; + +#define AC_RAST_DEPTH_RW 0 +#define AC_RAST_DEPTH_READONLY 1 +#define AC_RAST_DEPTH_NONE 2 + +/* ---- rendering API ---- */ + +/* Clear color + depth buffer. `depth` is the sentinel (typically a large + * float like 1e9 or FLT_MAX). Pass NULL target->depth to skip depth clear. */ +void ac_rast_clear(ACRastTarget *target, uint32_t color, float depth); + +/* Rasterize one triangle with the given attributes + options. Vertices + * are already in screen space (no further projection done here). Callers + * are responsible for near-plane clipping upstream — this function just + * bounding-box-clips to target and scissor. */ +void ac_rast_triangle(ACRastTarget *target, + const ACRastVertex *v0, + const ACRastVertex *v1, + const ACRastVertex *v2, + const ACRastOptions *opts); + +#ifdef __cplusplus +} +#endif + +#endif /* AC_RAST_H */ diff --git a/shared/rast/raster_test b/shared/rast/raster_test new file mode 100755 index 0000000000000000000000000000000000000000..acc95b159acde0f60c32458314658e419b226ad0 GIT binary patch literal 17216 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

SnzT7I@CD!F!YsTC{*7AC??E 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 +#include +#include + +#define FB_W 64 +#define FB_H 64 + +static int fail_count = 0; + +static void check_pixel(ACRastTarget *t, int x, int y, uint32_t expected, const char *label) { + uint32_t got = t->pixels[y * t->stride + x]; + if (got != expected) { + fprintf(stderr, " FAIL %s @ (%d,%d): got 0x%08x, expected 0x%08x\n", + label, x, y, got, expected); + fail_count++; + } +} + +static void check_nonzero(ACRastTarget *t, int x, int y, const char *label) { + uint32_t got = t->pixels[y * t->stride + x]; + if (got == 0) { + fprintf(stderr, " FAIL %s @ (%d,%d): got 0x%08x, expected non-zero\n", + label, x, y, got); + fail_count++; + } +} + +static void check_zero(ACRastTarget *t, int x, int y, const char *label) { + uint32_t got = t->pixels[y * t->stride + x]; + if (got != 0) { + fprintf(stderr, " FAIL %s @ (%d,%d): got 0x%08x, expected 0x00000000\n", + label, x, y, got); + fail_count++; + } +} + +/* ---- test 1: solid color triangle, no depth ---- */ +static void test_solid_fill(void) { + fprintf(stderr, "test_solid_fill\n"); + + uint32_t pixels[FB_W * FB_H]; + ACRastTarget t = { .pixels = pixels, .depth = NULL, + .width = FB_W, .height = FB_H, .stride = FB_W }; + ac_rast_clear(&t, 0x00000000u, 0.0f); + + ACRastVertex v0 = { .sx = 10, .sy = 10, .z = 0.5f, .w = 1.0f }; + ACRastVertex v1 = { .sx = 50, .sy = 10, .z = 0.5f, .w = 1.0f }; + ACRastVertex v2 = { .sx = 30, .sy = 50, .z = 0.5f, .w = 1.0f }; + ACRastOptions opts = { + .fill = AC_RAST_FILL_SOLID, + .solid_color = AC_RAST_PACK(255, 255, 0, 0), /* opaque red */ + .no_fade = 1, + .depth_mode = AC_RAST_DEPTH_NONE, + }; + ac_rast_triangle(&t, &v0, &v1, &v2, &opts); + + /* Corner pixel (0,0) untouched. */ + check_zero(&t, 0, 0, "outside tri top-left"); + + /* Centroid-ish point (30, 23) is inside. */ + check_pixel(&t, 30, 23, AC_RAST_PACK(255, 255, 0, 0), "centroid"); + + /* Far-outside point (60, 60) untouched. */ + check_zero(&t, 60, 60, "outside tri bottom-right"); +} + +/* ---- test 2: per-vertex color interpolation ---- */ +static void test_color_interp(void) { + fprintf(stderr, "test_color_interp\n"); + + uint32_t pixels[FB_W * FB_H]; + ACRastTarget t = { .pixels = pixels, .depth = NULL, + .width = FB_W, .height = FB_H, .stride = FB_W }; + ac_rast_clear(&t, 0x00000000u, 0.0f); + + ACRastVertex v0 = { .sx = 0, .sy = 0, .z = 0, .w = 1, + .r = 1, .g = 0, .b = 0, .a = 1 }; /* red */ + ACRastVertex v1 = { .sx = 63, .sy = 0, .z = 0, .w = 1, + .r = 0, .g = 1, .b = 0, .a = 1 }; /* green */ + ACRastVertex v2 = { .sx = 32, .sy = 63, .z = 0, .w = 1, + .r = 0, .g = 0, .b = 1, .a = 1 }; /* blue */ + ACRastOptions opts = { + .fill = AC_RAST_FILL_COLOR, + .no_fade = 1, + .depth_mode = AC_RAST_DEPTH_NONE, + }; + ac_rast_triangle(&t, &v0, &v1, &v2, &opts); + + /* Near each vertex, the dominant channel should lead. We test inside + * the triangle but close to each corner. */ + uint32_t near_red = t.pixels[3 * FB_W + 3]; + uint32_t near_green = t.pixels[3 * FB_W + 60]; + uint32_t near_blue = t.pixels[60 * FB_W + 32]; + + if (!(AC_RAST_R(near_red) > AC_RAST_G(near_red) && + AC_RAST_R(near_red) > AC_RAST_B(near_red))) { + fprintf(stderr, " FAIL near_red is not reddest: 0x%08x\n", near_red); + fail_count++; + } + if (!(AC_RAST_G(near_green) > AC_RAST_R(near_green) && + AC_RAST_G(near_green) > AC_RAST_B(near_green))) { + fprintf(stderr, " FAIL near_green is not greenest: 0x%08x\n", near_green); + fail_count++; + } + if (!(AC_RAST_B(near_blue) > AC_RAST_R(near_blue) && + AC_RAST_B(near_blue) > AC_RAST_G(near_blue))) { + fprintf(stderr, " FAIL near_blue is not bluest: 0x%08x\n", near_blue); + fail_count++; + } +} + +/* ---- test 3: depth test hides occluded triangle ---- */ +static void test_depth_occlusion(void) { + fprintf(stderr, "test_depth_occlusion\n"); + + uint32_t pixels[FB_W * FB_H]; + float depth [FB_W * FB_H]; + ACRastTarget t = { .pixels = pixels, .depth = depth, + .width = FB_W, .height = FB_H, .stride = FB_W }; + ac_rast_clear(&t, 0x00000000u, 1e9f); + + /* Blue triangle at z=0.8 (far), covers whole frame. */ + ACRastVertex bg0 = { .sx = 0, .sy = 0, .z = 0.8f, .w = 1.0f }; + ACRastVertex bg1 = { .sx = 63, .sy = 0, .z = 0.8f, .w = 1.0f }; + ACRastVertex bg2 = { .sx = 32, .sy = 63, .z = 0.8f, .w = 1.0f }; + ACRastOptions bg_opts = { + .fill = AC_RAST_FILL_SOLID, + .solid_color = AC_RAST_PACK(255, 0, 0, 255), /* blue */ + .no_fade = 1, + .depth_mode = AC_RAST_DEPTH_RW, + }; + ac_rast_triangle(&t, &bg0, &bg1, &bg2, &bg_opts); + + /* Red triangle at z=0.2 (near), covers center region. */ + ACRastVertex fg0 = { .sx = 20, .sy = 20, .z = 0.2f, .w = 1.0f }; + ACRastVertex fg1 = { .sx = 44, .sy = 20, .z = 0.2f, .w = 1.0f }; + ACRastVertex fg2 = { .sx = 32, .sy = 44, .z = 0.2f, .w = 1.0f }; + ACRastOptions fg_opts = bg_opts; + fg_opts.solid_color = AC_RAST_PACK(255, 255, 0, 0); /* red */ + ac_rast_triangle(&t, &fg0, &fg1, &fg2, &fg_opts); + + /* Center is red (foreground wins). */ + check_pixel(&t, 32, 28, AC_RAST_PACK(255, 255, 0, 0), "fg wins at center"); + + /* Corner of bg is blue (fg didn't cover it). */ + check_pixel(&t, 5, 5, AC_RAST_PACK(255, 0, 0, 255), "bg at corner"); + + /* Now draw blue AGAIN on top at z=0.9 (even farther) — should be hidden + * everywhere by the z-buffer and leave pixels unchanged. */ + ACRastVertex late0 = { .sx = 0, .sy = 0, .z = 0.9f, .w = 1.0f }; + ACRastVertex late1 = { .sx = 63, .sy = 0, .z = 0.9f, .w = 1.0f }; + ACRastVertex late2 = { .sx = 32, .sy = 63, .z = 0.9f, .w = 1.0f }; + ACRastOptions late_opts = bg_opts; + late_opts.solid_color = AC_RAST_PACK(255, 200, 200, 200); /* gray */ + ac_rast_triangle(&t, &late0, &late1, &late2, &late_opts); + + /* Center still red, corner still blue — late_opts shouldn't overwrite. */ + check_pixel(&t, 32, 28, AC_RAST_PACK(255, 255, 0, 0), "depth blocks late-draw center"); + check_pixel(&t, 5, 5, AC_RAST_PACK(255, 0, 0, 255), "depth blocks late-draw corner"); +} + +/* ---- test 4: scissor rect clips output ---- */ +static void test_scissor(void) { + fprintf(stderr, "test_scissor\n"); + + uint32_t pixels[FB_W * FB_H]; + ACRastTarget t = { .pixels = pixels, .depth = NULL, + .width = FB_W, .height = FB_H, .stride = FB_W }; + ac_rast_clear(&t, 0x00000000u, 0.0f); + + ACRastVertex v0 = { .sx = 0, .sy = 0, .z = 0, .w = 1 }; + ACRastVertex v1 = { .sx = 63, .sy = 0, .z = 0, .w = 1 }; + ACRastVertex v2 = { .sx = 32, .sy = 63, .z = 0, .w = 1 }; + ACRastOptions opts = { + .fill = AC_RAST_FILL_SOLID, + .solid_color = AC_RAST_PACK(255, 255, 255, 255), + .no_fade = 1, + .depth_mode = AC_RAST_DEPTH_NONE, + .scissor_x0 = 20, .scissor_y0 = 20, + .scissor_x1 = 44, .scissor_y1 = 40, + }; + ac_rast_triangle(&t, &v0, &v1, &v2, &opts); + + /* Inside scissor → written. */ + check_nonzero(&t, 30, 28, "inside scissor"); + /* Outside scissor but inside triangle → untouched. */ + check_zero(&t, 10, 10, "outside scissor top-left"); + check_zero(&t, 30, 50, "outside scissor bottom"); +} + +int main(void) { + test_solid_fill(); + test_color_interp(); + test_depth_occlusion(); + test_scissor(); + + if (fail_count) { + fprintf(stderr, "FAILED: %d assertion(s)\n", fail_count); + return 1; + } + fprintf(stderr, "OK: all tests passed\n"); + return 0; +}