nx_raster_sw.nx source
↩ module page · 183 lines · 7242 B
1// nx_raster_sw.nx -- bits-up integer software triangle rasterizer.
2//
3// LINEAGE (per [[feedback-no-tool-proliferation-consolidate-or-justify]]
4// cardinal landed 2026-05-20):
5//
6// Distinct from [[nx_raster.nx]] because: nx_raster.nx ships Bresenham
7// lines + Pineda triangles + barycentric-shaded triangles on the
8// *Image abstraction; this file ships Pineda strict-interior coverage
9// on the *i64 framebuffer directly, AND exposes the edge function +
10// signed-area helpers as public APIs for downstream composition.
11//
12// Distinct from [[nx_raster_triangle.nx]] because: nx_raster_triangle.nx
13// adds a Z-buffer for 3D occlusion; this file is the 2D strict-interior
14// coverage path used by the perceptual-render demos that don't need
15// depth. The two compose: nx_raster_triangle.nx CALLS the edge
16// function exported here in its Pineda inner loop.
17//
18// HONEST PROLIFERATION NOTE: the substrate has three rasterizer
19// primitives by 2026-05-20. Future raster work MUST extend one of
20// these three, not ship a fourth. When the substrate hits 4
21// rasterizers, consolidate per the cardinal.
22//
23// G1 first stone of NISHI_GRAPHICS_ROADMAP.md. Pineda 1988 edge-
24// function method, absorbed clean-room from the ACM SIGGRAPH paper.
25// Pure integer i64 math; no libm; no float; no SIMD; no GPU.
26//
27// Composes:
28// [[NISHI_GRAPHICS_ROADMAP]] G1
29// existing nx_framebuffer.nx (this rasterizer uses nx_fb_set as
30// its pixel write primitive; preserves additively per Cardinal 13)
31// [[feedback-bits-up-exceed-never-match]] (no Cairo, no Skia, no
32// Mesa, no AGG; integer-only Pineda)
33// [[feedback-no-false-ok-substrate-honesty-audit]] (degenerate +
34// CW triangles return 0 pixels deterministically; KAT verifies
35// known triangle -> known pixel count)
36//
37// API:
38// nx_raster_signed_area_2x(x0,y0,x1,y1,x2,y2) -> i64
39// Returns twice the signed area. Positive => front-facing (drawable).
40// Zero => degenerate (collinear). Negative => back-facing (culled).
41//
42// nx_raster_edge(ax,ay,bx,by,px,py) -> i64
43// The Pineda edge function: (bx-ax)*(py-ay) - (by-ay)*(px-ax).
44// Positive when (px,py) is in the half-plane to the left of the
45// directed edge (ax,ay)->(bx,by) under CCW (positive-area) winding.
46//
47// nx_raster_triangle(fb, w, h, x0,y0,x1,y1,x2,y2, color) -> i64
48// Rasterizes the triangle with strict-interior coverage (all
49// three edge functions > 0 at the integer pixel coordinate).
50// Writes color via nx_fb_set for each covered pixel.
51// Returns the number of pixels written (0 if degenerate or
52// back-facing or fully outside the framebuffer).
53//
54// Pixel-center convention: G1 evaluates the edge function at the
55// integer pixel coordinate (the top-left corner of the pixel),
56// strict-greater-than-zero for inclusion. G2 will introduce Q4
57// sub-pixel evaluation at pixel center (x+0.5, y+0.5) with the
58// top-left fill rule from Pineda 1988 + Heckbert 1990.
59//
60// Reference (absorbed clean-room, no code copied):
61// Pineda 1988 "A Parallel Algorithm for Polygon Rasterization"
62// (ACM SIGGRAPH; edge function + half-plane test method)
63// Heckbert 1990 "What Are the Coordinates of a Pixel?" (queued
64// for G2 sub-pixel + top-left fill rule)
65// Akenine-Möller + Haines + Hoffman "Real-Time Rendering" 4ed
66// (chapter 23 reference textbook; concepts, not code)
67//
68// genealogy_id: nishi-graphics-g1-raster + pineda_1988_edge_function +
69// heckbert_1990_pixel_coordinates
70// lineage_id: substrate_raster_sw_v1_bits_up_integer
71
72// nx_safety_envelope:
73// intended_use: "bits-up integer software triangle
74// rasterizer; Pineda 1988 edge-function
75// method; G1 substrate stone for the
76// DirectX-replacement arc"
77// sil_target: SIL2
78// evidence: [kat_known_triangle_pixel_count_36,
79// kat_degenerate_returns_zero,
80// kat_back_facing_returns_zero,
81// no_libm_no_float_no_simd]
82// hazard_register: [bug-tape-edge-tie-breaking,
83// bug-tape-bounding-box-clip,
84// bug-tape-coordinate-convention-mismatch]
85// verdict: NOT_YET_EVALUATED
86
87import "nx_syscalls.nx"
88import "nx_framebuffer.nx"
89
90// ===== Edge + area primitives =====================================
91
92func nx_raster_edge(
93 ax: i64, ay: i64,
94 bx: i64, by: i64,
95 px: i64, py: i64
96) -> i64 {
97 return (bx - ax) * (py - ay) - (by - ay) * (px - ax)
98}
99
100// Twice the signed area of the triangle (v0, v1, v2). Avoids the
101// /2 to stay in exact integer math. Sign convention: positive =>
102// front-facing (drawable); zero => degenerate; negative => back-facing.
103func nx_raster_signed_area_2x(
104 x0: i64, y0: i64,
105 x1: i64, y1: i64,
106 x2: i64, y2: i64
107) -> i64 {
108 return (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
109}
110
111// ===== Bounding-box helpers ======================================
112
113func nx_raster_min3(a: i64, b: i64, c: i64) -> i64 {
114 var m: i64 = a
115 if b < m { m = b }
116 if c < m { m = c }
117 return m
118}
119
120func nx_raster_max3(a: i64, b: i64, c: i64) -> i64 {
121 var m: i64 = a
122 if b > m { m = b }
123 if c > m { m = c }
124 return m
125}
126
127func nx_raster_clamp(v: i64, lo: i64, hi: i64) -> i64 {
128 if v < lo { return lo }
129 if v > hi { return hi }
130 return v
131}
132
133// ===== Triangle rasterizer =======================================
134//
135// Rasterizes triangle (v0, v1, v2) into framebuffer fb (w x h).
136// Returns the number of pixels written. Degenerate (area == 0) or
137// back-facing (area < 0) returns 0 with no writes.
138func nx_raster_triangle(
139 fb: *i64,
140 w: nx_int,
141 h: nx_int,
142 x0: i64, y0: i64,
143 x1: i64, y1: i64,
144 x2: i64, y2: i64,
145 color: i64
146) -> i64 {
147 let area2x: i64 = nx_raster_signed_area_2x(x0, y0, x1, y1, x2, y2)
148 if area2x <= 0 { return 0 }
149
150 // Bounding box clipped to framebuffer bounds.
151 let bb_min_x_raw: i64 = nx_raster_min3(x0, x1, x2)
152 let bb_max_x_raw: i64 = nx_raster_max3(x0, x1, x2)
153 let bb_min_y_raw: i64 = nx_raster_min3(y0, y1, y2)
154 let bb_max_y_raw: i64 = nx_raster_max3(y0, y1, y2)
155 let bb_min_x: i64 = nx_raster_clamp(bb_min_x_raw, 0, w - 1)
156 let bb_max_x: i64 = nx_raster_clamp(bb_max_x_raw, 0, w - 1)
157 let bb_min_y: i64 = nx_raster_clamp(bb_min_y_raw, 0, h - 1)
158 let bb_max_y: i64 = nx_raster_clamp(bb_max_y_raw, 0, h - 1)
159
160 // Walk bounding box; for each pixel, test all three edge
161 // functions. Strict-interior coverage: all three > 0.
162 var written: i64 = 0
163 var py: i64 = bb_min_y
164 while py <= bb_max_y {
165 var px: i64 = bb_min_x
166 while px <= bb_max_x {
167 let e0: i64 = nx_raster_edge(x0, y0, x1, y1, px, py)
168 let e1: i64 = nx_raster_edge(x1, y1, x2, y2, px, py)
169 let e2: i64 = nx_raster_edge(x2, y2, x0, y0, px, py)
170 if e0 > 0 {
171 if e1 > 0 {
172 if e2 > 0 {
173 nx_fb_set(fb, w, h, px, py, color)
174 written = written + 1
175 }
176 }
177 }
178 px = px + 1
179 }
180 py = py + 1
181 }
182 return written
183}