nx_render_pass.nx source
↩ module page · 309 lines · 11842 B
1// nx_render_pass.nx -- L4 mesh-render composer (vertex + zbuf + texture).
2//
3// Closes the GRAPHICS COMPOSITION gap. Today's substrate had:
4//
5// L1 containers : nx_image.Image + nx_image.ImageS64
6// L2 transforms : nx_raster (Bresenham + Pineda) +
7// nx_zbuf (depth-tested triangle) +
8// nx_tex_sample (UV bilinear + nearest) +
9// nx_vertex_pipeline (world -> screen)
10// L3 algorithms : the building blocks above
11// L4 composer : MISSING
12//
13// This brick is the L4 composer. Caller passes a mesh (indices +
14// vertices), a texture, an MVP matrix, and a framebuffer + depth
15// buffer. We:
16//
17// 1. Run vertex_pipeline_transform to project world vertices to
18// screen space.
19// 2. For each triangle (i, j, k) in indices: pull the three
20// transformed vertices, sample texture at their UVs, call
21// nx_zbuf_triangle with the depth-tested color write.
22//
23// Pure i64 substrate. No FPU, no GPU, no driver. Runs on every
24// backend including Cortex-M3 with a framebuffer.
25//
26// ===== Mesh format ===============================================
27//
28// verts: 4 i64 per vertex: (x_q14, y_q14, z_q14, packed_color)
29// uvs: 2 i64 per vertex: (u_q10, v_q10) -- texture coords
30// indices: 3 nx_int per triangle: (i0, i1, i2) -- mesh-flat layout
31//
32// All Q-format choices match nx_vertex_pipeline + nx_tex_sample
33// conventions so the buffers compose directly.
34//
35// ===== Vertex coloring (v1) ======================================
36//
37// v1 samples the texture at each VERTEX'S UV and lets nx_zbuf_triangle
38// barycentric-interpolate the per-vertex sampled colors across
39// pixels. This is the "Gouraud shading" approach -- fast, correct
40// for diffuse Lambertian surfaces, but lacks per-pixel texture
41// detail.
42//
43// v2 (queued): per-fragment texture sampling. Requires a fragment-
44// shader callback or a custom inner-loop variant of nx_zbuf_triangle
45// that samples per pixel.
46//
47// genealogy_id: catmull_1974_z_buffer + heckbert_1990_texture_mapping +
48// gouraud_1971_continuous_shading +
49// opengl_es_2_pipeline_2007
50// lineage_id: substrate_render_pass_v1_gouraud_textured
51
52// nx_safety_envelope:
53// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
54// sil_target: SIL1
55// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
56// verdict: NOT_YET_EVALUATED
57
58import "nx_syscalls.nx"
59import "nx_tier.nx"
60import "nx_loop.nx"
61import "nx_image.nx"
62import "nx_zbuf.nx"
63import "nx_tex_sample.nx"
64import "nx_vertex_pipeline.nx"
65
66// ===== Constants ==================================================
67
68const NX_RP_VTX_STRIDE: nx_int = 4 // 4 i64 per transformed vertex
69const NX_RP_UV_STRIDE: nx_int = 2 // 2 i64 per UV pair
70
71// ===== Sealed-enum: RenderPassVerdict =============================
72
73const NX_RP_OK: nx_int = 0
74const NX_RP_ERR_BAD_DIMS: nx_int = 1
75const NX_RP_ERR_BAD_INDEX: nx_int = 2
76const NX_RP_N_VERDICTS: nx_int = 3
77
78func nx_rp_verdict_is_valid(v: nx_int) -> nx_int {
79 if v < 0 { return 0 }
80 if v >= NX_RP_N_VERDICTS { return 0 }
81 return 1
82}
83
84// ===== Render a single mesh ======================================
85//
86// verts: [n_verts * 4] i64 (x_q14, y_q14, z_q14, color)
87// uvs: [n_verts * 2] i64 (u_q10, v_q10) -- nullable
88// indices: [n_tris * 3] i64 triangle indices
89// n_tris: number of triangles
90// mvp: [16] i64 row-major 4x4 matrix Q14
91// texture: *Image -- if non-null + uvs non-null, use it
92// wrap, filter: NX_TXS_* constants
93// img: color framebuffer
94// zbuf: depth buffer (same dims as img)
95//
96// Returns NX_RP_OK or a sealed error verdict.
97
98func nx_render_pass(verts: *i64, uvs: *i64, indices: *i64, n_verts: nx_int,
99 n_tris: nx_int, mvp: *i64,
100 texture: *Image, wrap: nx_int, filter: nx_int,
101 img: *Image, zbuf: *ImageS64) -> nx_int {
102 if img.width != zbuf.width { return NX_RP_ERR_BAD_DIMS }
103 if img.height != zbuf.height { return NX_RP_ERR_BAD_DIMS }
104 if n_verts <= 0 { return NX_RP_ERR_BAD_DIMS }
105 if n_tris <= 0 { return NX_RP_ERR_BAD_DIMS }
106
107 // Allocate scratch screen-space vertex buffer.
108 let screen: *i64 = sys_mmap(n_verts * NX_RP_VTX_STRIDE * 8) as *i64
109
110 // Step 1: vertex transform world -> screen.
111 nx_vertex_pipeline_transform(
112 verts, n_verts, mvp, img.width, img.height, screen)
113
114 // Step 2: rasterize each triangle.
115 var t: nx_int = 0
116 var iter: nx_int = 0
117 var verdict: nx_int = NX_LOOP_RUNNING
118 let BUDGET: nx_int = n_tris
119 while verdict == NX_LOOP_RUNNING && iter < BUDGET {
120 let i0: nx_int = indices[t * 3 + 0]
121 let i1: nx_int = indices[t * 3 + 1]
122 let i2: nx_int = indices[t * 3 + 2]
123
124 if i0 < 0 { verdict = NX_LOOP_ABORTED }
125 if i1 < 0 { verdict = NX_LOOP_ABORTED }
126 if i2 < 0 { verdict = NX_LOOP_ABORTED }
127 if i0 >= n_verts { verdict = NX_LOOP_ABORTED }
128 if i1 >= n_verts { verdict = NX_LOOP_ABORTED }
129 if i2 >= n_verts { verdict = NX_LOOP_ABORTED }
130
131 if verdict == NX_LOOP_RUNNING {
132 // Screen-space coords.
133 let x0: nx_int = screen[i0 * NX_RP_VTX_STRIDE + 0]
134 let y0: nx_int = screen[i0 * NX_RP_VTX_STRIDE + 1]
135 let z0: i64 = screen[i0 * NX_RP_VTX_STRIDE + 2]
136 let x1: nx_int = screen[i1 * NX_RP_VTX_STRIDE + 0]
137 let y1: nx_int = screen[i1 * NX_RP_VTX_STRIDE + 1]
138 let z1: i64 = screen[i1 * NX_RP_VTX_STRIDE + 2]
139 let x2: nx_int = screen[i2 * NX_RP_VTX_STRIDE + 0]
140 let y2: nx_int = screen[i2 * NX_RP_VTX_STRIDE + 1]
141 let z2: i64 = screen[i2 * NX_RP_VTX_STRIDE + 2]
142
143 // Per-vertex color: texture-sample at the vertex's UV
144 // if texture + uvs are non-null; otherwise use the
145 // vertex's packed_color field.
146 var c0: nx_int = screen[i0 * NX_RP_VTX_STRIDE + 3]
147 var c1: nx_int = screen[i1 * NX_RP_VTX_STRIDE + 3]
148 var c2: nx_int = screen[i2 * NX_RP_VTX_STRIDE + 3]
149
150 if (texture as i64) != 0 {
151 if (uvs as i64) != 0 {
152 let u0: nx_int = uvs[i0 * NX_RP_UV_STRIDE + 0]
153 let v0: nx_int = uvs[i0 * NX_RP_UV_STRIDE + 1]
154 let u1: nx_int = uvs[i1 * NX_RP_UV_STRIDE + 0]
155 let v1: nx_int = uvs[i1 * NX_RP_UV_STRIDE + 1]
156 let u2: nx_int = uvs[i2 * NX_RP_UV_STRIDE + 0]
157 let v2: nx_int = uvs[i2 * NX_RP_UV_STRIDE + 1]
158 c0 = nx_txs_sample(texture, u0, v0, wrap, filter)
159 c1 = nx_txs_sample(texture, u1, v1, wrap, filter)
160 c2 = nx_txs_sample(texture, u2, v2, wrap, filter)
161 }
162 }
163
164 nx_zbuf_triangle(img, zbuf,
165 x0, y0, z0, c0,
166 x1, y1, z1, c1,
167 x2, y2, z2, c2)
168 }
169 t = t + 1
170 iter = iter + 1
171 }
172 if verdict == NX_LOOP_ABORTED { return NX_RP_ERR_BAD_INDEX }
173 return NX_RP_OK
174}
175
176// ===== Convenience: clear + render =================================
177//
178// One-call shape: clears depth buffer, clears color buffer to a
179// background color, runs nx_render_pass.
180
181func nx_render_pass_clear_and_draw(
182 verts: *i64, uvs: *i64, indices: *i64,
183 n_verts: nx_int, n_tris: nx_int, mvp: *i64,
184 texture: *Image, wrap: nx_int, filter: nx_int,
185 img: *Image, zbuf: *ImageS64, bg_color: nx_int) -> nx_int {
186 // Clear depth.
187 nx_zbuf_clear(zbuf)
188
189 // Clear color.
190 let n_pix: nx_int = img.width * img.height
191 var i: nx_int = 0
192 var iter: nx_int = 0
193 var verdict: nx_int = NX_LOOP_RUNNING
194 let BUDGET: nx_int = img.height
195 var py: nx_int = 0
196 while verdict == NX_LOOP_RUNNING && iter < BUDGET {
197 var px: nx_int = 0
198 while px < img.width {
199 nx_image_set(img, px, py, 0, bg_color)
200 px = px + 1
201 }
202 py = py + 1
203 iter = iter + 1
204 }
205
206 return nx_render_pass(verts, uvs, indices, n_verts, n_tris, mvp,
207 texture, wrap, filter, img, zbuf)
208}
209
210// ===== Self-test ==================================================
211//
212// Single textured triangle test:
213//
214// Identity MVP (so input coords are already in screen-pixel-Q14).
215// 3 vertices forming a triangle in the upper-left of a 32x32
216// framebuffer. UVs (0,0), (1,0), (0,1) sample a 4x4 texture.
217//
218// We verify:
219// (a) Renders without error
220// (b) Triangle interior pixels are non-zero (something rendered)
221// (c) Pixel outside the triangle is bg color
222// (d) clear_and_draw zeroes the color buf where no triangle covers
223// (e) Verdict gate
224
225func main() -> i64 {
226 let W: nx_int = 32
227 let H: nx_int = 32
228 let img: *Image = nx_image_alloc(W, H, 1)
229 let zbuf: *ImageS64 = nx_image_s64_alloc(W, H)
230
231 // Texture: 4x4 with value = 10 + r * 40 + c * 10.
232 let tex: *Image = nx_image_alloc(4, 4, 1)
233 var r: nx_int = 0
234 while r < 4 {
235 var c: nx_int = 0
236 while c < 4 {
237 nx_image_set(tex, c, r, 0, 10 + r * 40 + c * 10)
238 c = c + 1
239 }
240 r = r + 1
241 }
242
243 // Identity MVP: 4x4 with Q14=16384 on the diagonal.
244 let MVP_Q: i64 = 16384
245 let mvp: *i64 = sys_mmap(16 * 8) as *i64
246 var m: nx_int = 0
247 while m < 16 { mvp[m] = 0; m = m + 1 }
248 mvp[0] = MVP_Q // [0,0]
249 mvp[5] = MVP_Q // [1,1]
250 mvp[10] = MVP_Q // [2,2]
251 mvp[15] = MVP_Q // [3,3]
252
253 // 3 vertices in normalized device coords (centered at origin).
254 // vertex_pipeline maps NDC [-Q, +Q] to pixel [0, viewport].
255 // Pick coords that produce a triangle fully within the 32x32 buf.
256 let verts: *i64 = sys_mmap(3 * 4 * 8) as *i64
257 // Triangle: (-0.5, -0.5) -> pixel (8, 24); (+0.5, -0.5) -> (24, 24);
258 // (-0.5, +0.5) -> (8, 8).
259 let HALF_Q: i64 = MVP_Q / 2
260 verts[0]=0 - HALF_Q; verts[1]=0 - HALF_Q; verts[2]=0; verts[3]=100
261 verts[4]=HALF_Q; verts[5]=0 - HALF_Q; verts[6]=0; verts[7]=200
262 verts[8]=0 - HALF_Q; verts[9]=HALF_Q; verts[10]=0; verts[11]=50
263
264 // UVs for the 3 vertices.
265 let uvs: *i64 = sys_mmap(3 * 2 * 8) as *i64
266 uvs[0]=0; uvs[1]=0 // (0, 0)
267 uvs[2]=1024; uvs[3]=0 // (1, 0)
268 uvs[4]=0; uvs[5]=1024 // (0, 1)
269
270 // One triangle: (0, 1, 2).
271 let indices: *i64 = sys_mmap(3 * 8) as *i64
272 indices[0]=0; indices[1]=1; indices[2]=2
273
274 let BG: nx_int = 5
275 let v: nx_int = nx_render_pass_clear_and_draw(
276 verts, uvs, indices, 3, 1, mvp,
277 tex, NX_TXS_WRAP_CLAMP, NX_TXS_FILTER_NEAREST,
278 img, zbuf, BG)
279 if v != NX_RP_OK { return 10 + v }
280
281 // --- (b) Triangle interior should have a non-bg color. ---
282 // Centroid roughly at (13, 19) (averaging pixels 8/24/8 and 24/24/8).
283 let centroid_x: nx_int = (8 + 24 + 8) / 3
284 let centroid_y: nx_int = (24 + 24 + 8) / 3
285 let interior: nx_int = nx_image_get(img, centroid_x, centroid_y, 0)
286 if interior == BG { return 20 }
287 if interior == 0 { return 21 }
288
289 // --- (c) Pixel far outside the triangle should be BG. ---
290 let outside: nx_int = nx_image_get(img, 30, 2, 0)
291 if outside != BG { return 30 }
292
293 // --- (d) Verdict gate ---
294 var vi: nx_int = 0
295 while vi < NX_RP_N_VERDICTS {
296 if nx_rp_verdict_is_valid(vi) != 1 { return 40 + vi }
297 vi = vi + 1
298 }
299
300 // --- (e) Bad-index detection ---
301 indices[0] = 99 // out of bounds
302 let v_bad: nx_int = nx_render_pass(
303 verts, uvs, indices, 3, 1, mvp,
304 tex, NX_TXS_WRAP_CLAMP, NX_TXS_FILTER_NEAREST,
305 img, zbuf)
306 if v_bad != NX_RP_ERR_BAD_INDEX { return 50 }
307
308 return 0
309}