code wiki / (root) / nx_render_pass.nx

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}