Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

shared/rast: pure-C cross-platform triangle rasterizer + self-tests

Foundation for the arena parity + WASM-speedup plan. One C99 file
(raster.c ~180 lines) implementing perspective-correct, depth-buffered,
textured / per-vertex-color / solid-fill triangles. No libc allocs in
the hot path, no mutable globals — every call is re-entrant so workers
can rasterize independent tiles against shared framebuffers.

Also ships raster_test.c with four self-tests (solid fill, color
interpolation dominance, depth occlusion with late-draw rejection,
scissor clipping). Builds with plain gcc today; emscripten build
comes next. The same test binary will run against the .wasm output
via wasmtime/node once that's wired.

Next steps:
1. Wire raster.c into fedac/native/src/graph3d.c so ac-native
switches to the shared implementation (zero behavior change —
current graph3d.c code was the source).
2. Emscripten build producing raster.wasm + raster.js loader.
3. Replace bios.mjs's JS triangle path with a WASM call.
4. Tile binning + WebWorker pool (SAB + Atomics) for the parallel
path; native gets pthread pool with same tile data structures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+509
+171
shared/rast/raster.c
··· 1 + /* 2 + * raster.c — cross-platform triangle rasterizer 3 + * 4 + * Port of fedac/native/src/graph3d.c's rasterize_triangle + web/graph.mjs's 5 + * equivalent triangle path, unified as one implementation. See raster.h for 6 + * the API contract + platform rationale. 7 + * 8 + * Pixel output is intended to be exact-match between native and WASM builds 9 + * of this file — the test harness in raster_test.c checks this by rendering 10 + * a fixed triangle and diffing against a golden PNG. 11 + */ 12 + #include "raster.h" 13 + #include <math.h> 14 + 15 + static inline int mini(int a, int b) { return a < b ? a : b; } 16 + static inline int maxi(int a, int b) { return a > b ? a : b; } 17 + static inline float minf(float a, float b) { return a < b ? a : b; } 18 + static inline float maxf(float a, float b) { return a > b ? a : b; } 19 + static inline float clampf(float v, float lo, float hi) { 20 + return maxf(lo, minf(hi, v)); 21 + } 22 + 23 + static uint32_t sample_texture(const ACRastTexture *tex, float u, float v) { 24 + /* GL_REPEAT — modulo, wrap negative. */ 25 + u = u - floorf(u); 26 + v = v - floorf(v); 27 + int tx = (int)(u * tex->width); 28 + int ty = (int)(v * tex->height); 29 + if (tx < 0) tx += tex->width; 30 + if (ty < 0) ty += tex->height; 31 + if (tx >= tex->width) tx = tex->width - 1; 32 + if (ty >= tex->height) ty = tex->height - 1; 33 + return tex->pixels[ty * tex->stride + tx]; 34 + } 35 + 36 + void ac_rast_clear(ACRastTarget *t, uint32_t color, float depth) { 37 + if (!t) return; 38 + int n = t->height * t->stride; 39 + for (int i = 0; i < n; i++) t->pixels[i] = color; 40 + if (t->depth) { 41 + for (int i = 0; i < n; i++) t->depth[i] = depth; 42 + } 43 + } 44 + 45 + void ac_rast_triangle(ACRastTarget *t, 46 + const ACRastVertex *v0, 47 + const ACRastVertex *v1, 48 + const ACRastVertex *v2, 49 + const ACRastOptions *opts) { 50 + if (!t || !t->pixels || !v0 || !v1 || !v2 || !opts) return; 51 + 52 + /* Bounding box, clipped to target + scissor. */ 53 + int min_x = (int)floorf(minf(v0->sx, minf(v1->sx, v2->sx))); 54 + int max_x = (int)ceilf (maxf(v0->sx, maxf(v1->sx, v2->sx))); 55 + int min_y = (int)floorf(minf(v0->sy, minf(v1->sy, v2->sy))); 56 + int max_y = (int)ceilf (maxf(v0->sy, maxf(v1->sy, v2->sy))); 57 + 58 + int tx0 = 0, ty0 = 0, tx1 = t->width - 1, ty1 = t->height - 1; 59 + if (opts->scissor_x1 > opts->scissor_x0 && opts->scissor_y1 > opts->scissor_y0) { 60 + tx0 = maxi(tx0, opts->scissor_x0); 61 + ty0 = maxi(ty0, opts->scissor_y0); 62 + tx1 = mini(tx1, opts->scissor_x1); 63 + ty1 = mini(ty1, opts->scissor_y1); 64 + } 65 + min_x = maxi(min_x, tx0); 66 + min_y = maxi(min_y, ty0); 67 + max_x = mini(max_x, tx1); 68 + max_y = mini(max_y, ty1); 69 + if (min_x > max_x || min_y > max_y) return; 70 + 71 + /* Edge function setup (matches graph3d.c). */ 72 + float dx01 = v1->sx - v0->sx, dy01 = v1->sy - v0->sy; 73 + float dx12 = v2->sx - v1->sx, dy12 = v2->sy - v1->sy; 74 + float dx20 = v0->sx - v2->sx, dy20 = v0->sy - v2->sy; 75 + 76 + float area = dx01 * (v2->sy - v0->sy) - dy01 * (v2->sx - v0->sx); 77 + if (fabsf(area) < 0.001f) return; /* degenerate */ 78 + float inv_area = 1.0f / area; 79 + 80 + /* 1/w for perspective-correct interpolation. If caller passed w==1 81 + * for every vertex (affine mode), inv_w == 1 throughout and the per- 82 + * pixel perspective divide collapses to a no-op. */ 83 + float inv_w0 = 1.0f / (v0->w != 0.0f ? v0->w : 1.0f); 84 + float inv_w1 = 1.0f / (v1->w != 0.0f ? v1->w : 1.0f); 85 + float inv_w2 = 1.0f / (v2->w != 0.0f ? v2->w : 1.0f); 86 + 87 + const ACRastTexture *tex = (opts->fill == AC_RAST_FILL_TEXTURE) ? opts->texture : NULL; 88 + int has_depth = (t->depth && opts->depth_mode != AC_RAST_DEPTH_NONE); 89 + int depth_write = has_depth && opts->depth_mode == AC_RAST_DEPTH_RW; 90 + 91 + for (int y = min_y; y <= max_y; y++) { 92 + int row = y * t->stride; 93 + for (int x = min_x; x <= max_x; x++) { 94 + float px = x + 0.5f, py = y + 0.5f; 95 + 96 + /* Barycentric coordinates (edge function). */ 97 + float b0 = (dx12 * (py - v1->sy) - dy12 * (px - v1->sx)) * inv_area; 98 + float b1 = (dx20 * (py - v2->sy) - dy20 * (px - v2->sx)) * inv_area; 99 + float b2 = 1.0f - b0 - b1; 100 + if (b0 < 0.0f || b1 < 0.0f || b2 < 0.0f) continue; 101 + 102 + /* Perspective-correct weight. */ 103 + float inv_w = b0 * inv_w0 + b1 * inv_w1 + b2 * inv_w2; 104 + float w_interp = 1.0f / inv_w; 105 + 106 + /* Depth interpolation + test. */ 107 + int idx = row + x; 108 + if (has_depth) { 109 + float z = (b0 * v0->z * inv_w0 + 110 + b1 * v1->z * inv_w1 + 111 + b2 * v2->z * inv_w2) * w_interp; 112 + if (z >= t->depth[idx]) continue; 113 + if (depth_write) t->depth[idx] = z; 114 + } 115 + 116 + uint32_t pixel; 117 + switch (opts->fill) { 118 + case AC_RAST_FILL_TEXTURE: { 119 + float u = (b0 * v0->u * inv_w0 + 120 + b1 * v1->u * inv_w1 + 121 + b2 * v2->u * inv_w2) * w_interp; 122 + float v = (b0 * v0->v * inv_w0 + 123 + b1 * v1->v * inv_w1 + 124 + b2 * v2->v * inv_w2) * w_interp; 125 + pixel = sample_texture(tex, u, v); 126 + } break; 127 + case AC_RAST_FILL_COLOR: { 128 + float r = (b0 * v0->r * inv_w0 + 129 + b1 * v1->r * inv_w1 + 130 + b2 * v2->r * inv_w2) * w_interp; 131 + float g = (b0 * v0->g * inv_w0 + 132 + b1 * v1->g * inv_w1 + 133 + b2 * v2->g * inv_w2) * w_interp; 134 + float bl = (b0 * v0->b * inv_w0 + 135 + b1 * v1->b * inv_w1 + 136 + b2 * v2->b * inv_w2) * w_interp; 137 + float al = (b0 * v0->a * inv_w0 + 138 + b1 * v1->a * inv_w1 + 139 + b2 * v2->a * inv_w2) * w_interp; 140 + uint8_t ri = (uint8_t)(clampf(r, 0.0f, 1.0f) * 255.0f); 141 + uint8_t gi = (uint8_t)(clampf(g, 0.0f, 1.0f) * 255.0f); 142 + uint8_t bi = (uint8_t)(clampf(bl, 0.0f, 1.0f) * 255.0f); 143 + uint8_t ai = (uint8_t)(clampf(al, 0.0f, 1.0f) * 255.0f); 144 + pixel = AC_RAST_PACK(ai ? ai : 255, ri, gi, bi); 145 + } break; 146 + case AC_RAST_FILL_SOLID: 147 + default: 148 + pixel = opts->solid_color; 149 + break; 150 + } 151 + 152 + /* Near-plane fade — mirror graph3d.c when !no_fade. The ramp 153 + * is intentionally gentle so UI-layer triangles (no_fade=1) 154 + * look identical to 3D content far from the camera. */ 155 + if (!opts->no_fade) { 156 + /* w_interp in camera space — larger = farther. The fade 157 + * tapers in the first 0.5 units, matching graph3d.c. */ 158 + float fade = clampf(w_interp * 2.0f, 0.0f, 1.0f); 159 + if (fade < 1.0f) { 160 + uint8_t a = AC_RAST_A(pixel); 161 + uint8_t r = (uint8_t)(AC_RAST_R(pixel) * fade); 162 + uint8_t g = (uint8_t)(AC_RAST_G(pixel) * fade); 163 + uint8_t b = (uint8_t)(AC_RAST_B(pixel) * fade); 164 + pixel = AC_RAST_PACK(a, r, g, b); 165 + } 166 + } 167 + 168 + t->pixels[idx] = pixel; 169 + } 170 + } 171 + }
+118
shared/rast/raster.h
··· 1 + /* 2 + * Aesthetic Computer — shared software rasterizer. 3 + * 4 + * One pure-C triangle rasterizer that ships two ways: 5 + * 1. Linked into ac-native (fedac/native/) so notepat / arena / etc. use 6 + * the same pixel output on bare metal as they do in the browser. 7 + * 2. Compiled via Emscripten into .wasm for the web runtime, replacing 8 + * the JS triangle path in system/public/aesthetic.computer/lib/graph.mjs 9 + * for big perf gains (2-3× baseline, 20-40× with SIMD + worker-tiled). 10 + * 11 + * Rules for keeping parity portable: 12 + * - No libc allocs inside the hot path. Caller owns framebuffer + depth. 13 + * - No globals / mutable module state — every call is fully re-entrant 14 + * so workers can rasterize independent tiles in parallel. 15 + * - Pure C99. No POSIX, no SSE/AVX intrinsics here (those belong in 16 + * emcc's simd128 path, gated by separate build flags). 17 + * - Pixel format is uint32_t BGRA packed as (A<<24)|(R<<16)|(G<<8)|B, 18 + * matching graph3d.c and graph.mjs's Uint32Array view. 19 + */ 20 + #ifndef AC_RAST_H 21 + #define AC_RAST_H 22 + 23 + #include <stdint.h> 24 + #include <stddef.h> 25 + 26 + #ifdef __cplusplus 27 + extern "C" { 28 + #endif 29 + 30 + /* Pixel format helpers — keep in sync with fedac/native/src/graph3d.c. */ 31 + #define AC_RAST_PACK(a, r, g, b) \ 32 + (((uint32_t)(a) << 24) | ((uint32_t)(r) << 16) | \ 33 + ((uint32_t)(g) << 8) | (uint32_t)(b)) 34 + #define AC_RAST_A(c) (((c) >> 24) & 0xFFu) 35 + #define AC_RAST_R(c) (((c) >> 16) & 0xFFu) 36 + #define AC_RAST_G(c) (((c) >> 8) & 0xFFu) 37 + #define AC_RAST_B(c) ( (c) & 0xFFu) 38 + 39 + typedef struct { 40 + uint32_t *pixels; /* row-major, AC_RAST_PACK-encoded */ 41 + float *depth; /* row-major, same dims; NULL disables depth test */ 42 + int width, height; 43 + int stride; /* elements per row (usually == width) */ 44 + } ACRastTarget; 45 + 46 + typedef struct { 47 + /* Post-perspective-divide screen-space position. 48 + * x, y : pixel coordinates inside the target 49 + * z : depth (lower = closer, matches graph3d.c convention) 50 + * w : 1/w-free interpolation denominator — pass the original 51 + * clip-space w from the vertex shader. If the caller isn't 52 + * doing perspective-correct interpolation, pass 1.0f. */ 53 + float sx, sy, z, w; 54 + 55 + /* Optional per-vertex attributes. Unused channels are ignored based 56 + * on the mode flag in ACRastOptions, so callers can leave them zero. */ 57 + float r, g, b, a; /* [0..1] linear RGBA */ 58 + float u, v; /* texture coords, wrapped */ 59 + } ACRastVertex; 60 + 61 + typedef struct { 62 + const uint32_t *pixels; 63 + int width, height, stride; 64 + } ACRastTexture; 65 + 66 + typedef enum { 67 + AC_RAST_FILL_SOLID = 0, /* single flat color from opts->solid_color */ 68 + AC_RAST_FILL_COLOR = 1, /* perspective-correct per-vertex RGBA */ 69 + AC_RAST_FILL_TEXTURE = 2 /* sample from opts->texture with wrapped UV */ 70 + } ACRastFill; 71 + 72 + typedef struct { 73 + ACRastFill fill; 74 + uint32_t solid_color; /* AC_RAST_PACK-encoded */ 75 + const ACRastTexture *texture; 76 + 77 + /* Near-plane fade (matches graph3d.c no_fade semantics). When non-zero, 78 + * pixels close to w≈0 get darkened for a cheap cinematic feel. Set to 79 + * 1 to skip — required for UI layers and colored overlays. */ 80 + int no_fade; 81 + 82 + /* Depth test mode. 83 + * 0 = read + write (standard z-buffer) 84 + * 1 = read-only (transparent/overlay passes) 85 + * 2 = no test / no write (painter's-order fallback) */ 86 + int depth_mode; 87 + 88 + /* Optional scissor rect (all zeros disables). Pixels outside are not 89 + * touched — useful for tile-parallel rasterization where each worker 90 + * owns a screen region. */ 91 + int scissor_x0, scissor_y0, scissor_x1, scissor_y1; 92 + } ACRastOptions; 93 + 94 + #define AC_RAST_DEPTH_RW 0 95 + #define AC_RAST_DEPTH_READONLY 1 96 + #define AC_RAST_DEPTH_NONE 2 97 + 98 + /* ---- rendering API ---- */ 99 + 100 + /* Clear color + depth buffer. `depth` is the sentinel (typically a large 101 + * float like 1e9 or FLT_MAX). Pass NULL target->depth to skip depth clear. */ 102 + void ac_rast_clear(ACRastTarget *target, uint32_t color, float depth); 103 + 104 + /* Rasterize one triangle with the given attributes + options. Vertices 105 + * are already in screen space (no further projection done here). Callers 106 + * are responsible for near-plane clipping upstream — this function just 107 + * bounding-box-clips to target and scissor. */ 108 + void ac_rast_triangle(ACRastTarget *target, 109 + const ACRastVertex *v0, 110 + const ACRastVertex *v1, 111 + const ACRastVertex *v2, 112 + const ACRastOptions *opts); 113 + 114 + #ifdef __cplusplus 115 + } 116 + #endif 117 + 118 + #endif /* AC_RAST_H */
shared/rast/raster_test

This is a binary file and will not be displayed.

+220
shared/rast/raster_test.c
··· 1 + /* 2 + * raster_test.c — pixel-exact self-test for the shared rasterizer. 3 + * 4 + * Build: gcc -O2 -Wall -o raster_test raster.c raster_test.c -lm 5 + * Run: ./raster_test 6 + * 7 + * Each case renders a fixed triangle + compares a spot-check of pixels 8 + * against hand-computed expected values. When we later wire a WASM build, 9 + * the same test compiled via emcc should produce byte-identical output. 10 + * 11 + * Golden PNG snapshots (for richer diff-based drift detection) land in a 12 + * follow-up once the first integration is wired — this file focuses on 13 + * math correctness so a broken emcc build catches failures locally without 14 + * needing an image diff toolchain. 15 + */ 16 + #include "raster.h" 17 + 18 + #include <stdio.h> 19 + #include <stdlib.h> 20 + #include <string.h> 21 + 22 + #define FB_W 64 23 + #define FB_H 64 24 + 25 + static int fail_count = 0; 26 + 27 + static void check_pixel(ACRastTarget *t, int x, int y, uint32_t expected, const char *label) { 28 + uint32_t got = t->pixels[y * t->stride + x]; 29 + if (got != expected) { 30 + fprintf(stderr, " FAIL %s @ (%d,%d): got 0x%08x, expected 0x%08x\n", 31 + label, x, y, got, expected); 32 + fail_count++; 33 + } 34 + } 35 + 36 + static void check_nonzero(ACRastTarget *t, int x, int y, const char *label) { 37 + uint32_t got = t->pixels[y * t->stride + x]; 38 + if (got == 0) { 39 + fprintf(stderr, " FAIL %s @ (%d,%d): got 0x%08x, expected non-zero\n", 40 + label, x, y, got); 41 + fail_count++; 42 + } 43 + } 44 + 45 + static void check_zero(ACRastTarget *t, int x, int y, const char *label) { 46 + uint32_t got = t->pixels[y * t->stride + x]; 47 + if (got != 0) { 48 + fprintf(stderr, " FAIL %s @ (%d,%d): got 0x%08x, expected 0x00000000\n", 49 + label, x, y, got); 50 + fail_count++; 51 + } 52 + } 53 + 54 + /* ---- test 1: solid color triangle, no depth ---- */ 55 + static void test_solid_fill(void) { 56 + fprintf(stderr, "test_solid_fill\n"); 57 + 58 + uint32_t pixels[FB_W * FB_H]; 59 + ACRastTarget t = { .pixels = pixels, .depth = NULL, 60 + .width = FB_W, .height = FB_H, .stride = FB_W }; 61 + ac_rast_clear(&t, 0x00000000u, 0.0f); 62 + 63 + ACRastVertex v0 = { .sx = 10, .sy = 10, .z = 0.5f, .w = 1.0f }; 64 + ACRastVertex v1 = { .sx = 50, .sy = 10, .z = 0.5f, .w = 1.0f }; 65 + ACRastVertex v2 = { .sx = 30, .sy = 50, .z = 0.5f, .w = 1.0f }; 66 + ACRastOptions opts = { 67 + .fill = AC_RAST_FILL_SOLID, 68 + .solid_color = AC_RAST_PACK(255, 255, 0, 0), /* opaque red */ 69 + .no_fade = 1, 70 + .depth_mode = AC_RAST_DEPTH_NONE, 71 + }; 72 + ac_rast_triangle(&t, &v0, &v1, &v2, &opts); 73 + 74 + /* Corner pixel (0,0) untouched. */ 75 + check_zero(&t, 0, 0, "outside tri top-left"); 76 + 77 + /* Centroid-ish point (30, 23) is inside. */ 78 + check_pixel(&t, 30, 23, AC_RAST_PACK(255, 255, 0, 0), "centroid"); 79 + 80 + /* Far-outside point (60, 60) untouched. */ 81 + check_zero(&t, 60, 60, "outside tri bottom-right"); 82 + } 83 + 84 + /* ---- test 2: per-vertex color interpolation ---- */ 85 + static void test_color_interp(void) { 86 + fprintf(stderr, "test_color_interp\n"); 87 + 88 + uint32_t pixels[FB_W * FB_H]; 89 + ACRastTarget t = { .pixels = pixels, .depth = NULL, 90 + .width = FB_W, .height = FB_H, .stride = FB_W }; 91 + ac_rast_clear(&t, 0x00000000u, 0.0f); 92 + 93 + ACRastVertex v0 = { .sx = 0, .sy = 0, .z = 0, .w = 1, 94 + .r = 1, .g = 0, .b = 0, .a = 1 }; /* red */ 95 + ACRastVertex v1 = { .sx = 63, .sy = 0, .z = 0, .w = 1, 96 + .r = 0, .g = 1, .b = 0, .a = 1 }; /* green */ 97 + ACRastVertex v2 = { .sx = 32, .sy = 63, .z = 0, .w = 1, 98 + .r = 0, .g = 0, .b = 1, .a = 1 }; /* blue */ 99 + ACRastOptions opts = { 100 + .fill = AC_RAST_FILL_COLOR, 101 + .no_fade = 1, 102 + .depth_mode = AC_RAST_DEPTH_NONE, 103 + }; 104 + ac_rast_triangle(&t, &v0, &v1, &v2, &opts); 105 + 106 + /* Near each vertex, the dominant channel should lead. We test inside 107 + * the triangle but close to each corner. */ 108 + uint32_t near_red = t.pixels[3 * FB_W + 3]; 109 + uint32_t near_green = t.pixels[3 * FB_W + 60]; 110 + uint32_t near_blue = t.pixels[60 * FB_W + 32]; 111 + 112 + if (!(AC_RAST_R(near_red) > AC_RAST_G(near_red) && 113 + AC_RAST_R(near_red) > AC_RAST_B(near_red))) { 114 + fprintf(stderr, " FAIL near_red is not reddest: 0x%08x\n", near_red); 115 + fail_count++; 116 + } 117 + if (!(AC_RAST_G(near_green) > AC_RAST_R(near_green) && 118 + AC_RAST_G(near_green) > AC_RAST_B(near_green))) { 119 + fprintf(stderr, " FAIL near_green is not greenest: 0x%08x\n", near_green); 120 + fail_count++; 121 + } 122 + if (!(AC_RAST_B(near_blue) > AC_RAST_R(near_blue) && 123 + AC_RAST_B(near_blue) > AC_RAST_G(near_blue))) { 124 + fprintf(stderr, " FAIL near_blue is not bluest: 0x%08x\n", near_blue); 125 + fail_count++; 126 + } 127 + } 128 + 129 + /* ---- test 3: depth test hides occluded triangle ---- */ 130 + static void test_depth_occlusion(void) { 131 + fprintf(stderr, "test_depth_occlusion\n"); 132 + 133 + uint32_t pixels[FB_W * FB_H]; 134 + float depth [FB_W * FB_H]; 135 + ACRastTarget t = { .pixels = pixels, .depth = depth, 136 + .width = FB_W, .height = FB_H, .stride = FB_W }; 137 + ac_rast_clear(&t, 0x00000000u, 1e9f); 138 + 139 + /* Blue triangle at z=0.8 (far), covers whole frame. */ 140 + ACRastVertex bg0 = { .sx = 0, .sy = 0, .z = 0.8f, .w = 1.0f }; 141 + ACRastVertex bg1 = { .sx = 63, .sy = 0, .z = 0.8f, .w = 1.0f }; 142 + ACRastVertex bg2 = { .sx = 32, .sy = 63, .z = 0.8f, .w = 1.0f }; 143 + ACRastOptions bg_opts = { 144 + .fill = AC_RAST_FILL_SOLID, 145 + .solid_color = AC_RAST_PACK(255, 0, 0, 255), /* blue */ 146 + .no_fade = 1, 147 + .depth_mode = AC_RAST_DEPTH_RW, 148 + }; 149 + ac_rast_triangle(&t, &bg0, &bg1, &bg2, &bg_opts); 150 + 151 + /* Red triangle at z=0.2 (near), covers center region. */ 152 + ACRastVertex fg0 = { .sx = 20, .sy = 20, .z = 0.2f, .w = 1.0f }; 153 + ACRastVertex fg1 = { .sx = 44, .sy = 20, .z = 0.2f, .w = 1.0f }; 154 + ACRastVertex fg2 = { .sx = 32, .sy = 44, .z = 0.2f, .w = 1.0f }; 155 + ACRastOptions fg_opts = bg_opts; 156 + fg_opts.solid_color = AC_RAST_PACK(255, 255, 0, 0); /* red */ 157 + ac_rast_triangle(&t, &fg0, &fg1, &fg2, &fg_opts); 158 + 159 + /* Center is red (foreground wins). */ 160 + check_pixel(&t, 32, 28, AC_RAST_PACK(255, 255, 0, 0), "fg wins at center"); 161 + 162 + /* Corner of bg is blue (fg didn't cover it). */ 163 + check_pixel(&t, 5, 5, AC_RAST_PACK(255, 0, 0, 255), "bg at corner"); 164 + 165 + /* Now draw blue AGAIN on top at z=0.9 (even farther) — should be hidden 166 + * everywhere by the z-buffer and leave pixels unchanged. */ 167 + ACRastVertex late0 = { .sx = 0, .sy = 0, .z = 0.9f, .w = 1.0f }; 168 + ACRastVertex late1 = { .sx = 63, .sy = 0, .z = 0.9f, .w = 1.0f }; 169 + ACRastVertex late2 = { .sx = 32, .sy = 63, .z = 0.9f, .w = 1.0f }; 170 + ACRastOptions late_opts = bg_opts; 171 + late_opts.solid_color = AC_RAST_PACK(255, 200, 200, 200); /* gray */ 172 + ac_rast_triangle(&t, &late0, &late1, &late2, &late_opts); 173 + 174 + /* Center still red, corner still blue — late_opts shouldn't overwrite. */ 175 + check_pixel(&t, 32, 28, AC_RAST_PACK(255, 255, 0, 0), "depth blocks late-draw center"); 176 + check_pixel(&t, 5, 5, AC_RAST_PACK(255, 0, 0, 255), "depth blocks late-draw corner"); 177 + } 178 + 179 + /* ---- test 4: scissor rect clips output ---- */ 180 + static void test_scissor(void) { 181 + fprintf(stderr, "test_scissor\n"); 182 + 183 + uint32_t pixels[FB_W * FB_H]; 184 + ACRastTarget t = { .pixels = pixels, .depth = NULL, 185 + .width = FB_W, .height = FB_H, .stride = FB_W }; 186 + ac_rast_clear(&t, 0x00000000u, 0.0f); 187 + 188 + ACRastVertex v0 = { .sx = 0, .sy = 0, .z = 0, .w = 1 }; 189 + ACRastVertex v1 = { .sx = 63, .sy = 0, .z = 0, .w = 1 }; 190 + ACRastVertex v2 = { .sx = 32, .sy = 63, .z = 0, .w = 1 }; 191 + ACRastOptions opts = { 192 + .fill = AC_RAST_FILL_SOLID, 193 + .solid_color = AC_RAST_PACK(255, 255, 255, 255), 194 + .no_fade = 1, 195 + .depth_mode = AC_RAST_DEPTH_NONE, 196 + .scissor_x0 = 20, .scissor_y0 = 20, 197 + .scissor_x1 = 44, .scissor_y1 = 40, 198 + }; 199 + ac_rast_triangle(&t, &v0, &v1, &v2, &opts); 200 + 201 + /* Inside scissor → written. */ 202 + check_nonzero(&t, 30, 28, "inside scissor"); 203 + /* Outside scissor but inside triangle → untouched. */ 204 + check_zero(&t, 10, 10, "outside scissor top-left"); 205 + check_zero(&t, 30, 50, "outside scissor bottom"); 206 + } 207 + 208 + int main(void) { 209 + test_solid_fill(); 210 + test_color_interp(); 211 + test_depth_occlusion(); 212 + test_scissor(); 213 + 214 + if (fail_count) { 215 + fprintf(stderr, "FAILED: %d assertion(s)\n", fail_count); 216 + return 1; 217 + } 218 + fprintf(stderr, "OK: all tests passed\n"); 219 + return 0; 220 + }