nx_framebuffer.nx source
↩ module page · 213 lines · 8453 B
1// nx_framebuffer.nx -- u32 pixel buffer + z-buffer foundation.
2//
3// Per gap roadmap Phase 1: substrate needs a framebuffer for the
4// software rasterizer to write into. This primitive provides:
5// - u32 packed-RGBA framebuffer (w x h i64 cells)
6// - Q14 depth buffer (parallel array)
7// - Clear / set-pixel / set-depth primitives
8// - Tile-clear for chunked updates
9//
10// PIXEL FORMAT: 0xRRGGBBAA packed in low 32 bits of i64.
11// DEPTH FORMAT: Q14 metres signed. Initial depth = MAX (far plane).
12//
13// Caller allocates the buffer; primitive just provides accessors.
14// To compose with the canvas blit, the JS shim reads the buffer's
15// low 32 bits and writes RGBA8.
16//
17// PUBLIC APIs:
18// nx_fb_alloc(w, h) -> *i64 framebuffer (w * h cells)
19// nx_fb_depth_alloc(w, h) -> *i64 depth buffer
20// nx_fb_clear(fb, w, h, color) fill with solid color
21// nx_fb_clear_depth(zb, w, h) reset depth to far plane
22// nx_fb_set(fb, w, h, x, y, color)
23// nx_fb_get(fb, w, h, x, y) -> i64
24// nx_fb_depth_test_set(fb, zb, w, h, x, y, z, color) -> 0/1
25// Returns 1 if pixel written (z < existing); 0 otherwise.
26// nx_fb_pack_rgba(r, g, b, a) -> i64 packed color helper
27//
28// genealogy_id: software_rasterizer_canon + frame_buffer_canon
29// lineage_id: nx_framebuffer_q14_v1
30
31// nx_safety_envelope:
32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
33// sil_target: SIL1
34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
35// verdict: NOT_YET_EVALUATED
36
37import "nx_syscalls.nx"
38import "nx_tier.nx"
39const NX_MAGIC_65536: i64 = 65536
40const NX_MAGIC_16777216: i64 = 16777216
41
42const NX_FB_Q: nx_int = 16384
43
44// ===== Far-plane depth ==============================================
45// 1e6 metres in Q14 = ~1.6e10; fits comfortably in i64.
46const NX_FB_FAR_DEPTH_Q14: nx_int = 16384000000
47
48// ===== Allocate framebuffer ========================================
49// Returns pointer to w * h i64 cells. Zero-initialised.
50func nx_fb_alloc(w: nx_int, h: nx_int) -> *i64 {
51 if w <= 0 { return 0 as *i64 }
52 if h <= 0 { return 0 as *i64 }
53 let bytes: nx_int = w * h * NX_SIZEOF_NX_INT
54 return (sys_mmap(bytes)) as *i64
55}
56
57func nx_fb_depth_alloc(w: nx_int, h: nx_int) -> *i64 {
58 if w <= 0 { return 0 as *i64 }
59 if h <= 0 { return 0 as *i64 }
60 let bytes: nx_int = w * h * NX_SIZEOF_NX_INT
61 return (sys_mmap(bytes)) as *i64
62}
63
64// ===== Clear ========================================================
65func nx_fb_clear(fb: *i64, w: nx_int, h: nx_int, color: nx_int) {
66 let n: nx_int = w * h
67 var i: nx_int = 0
68 while i < n { fb[i] = color; i = i + 1 }
69}
70
71func nx_fb_clear_depth(zb: *i64, w: nx_int, h: nx_int) {
72 let n: nx_int = w * h
73 var i: nx_int = 0
74 while i < n { zb[i] = NX_FB_FAR_DEPTH_Q14; i = i + 1 }
75}
76
77// Sky-gradient overpaint -- the per-cell variant -- lives in
78// nishi-engine/nx_voxel_runtime.nx as `_runtime_overpaint_sky`.
79// It composes nx_fb_pack_rgba + NX_FB_FAR_DEPTH_Q14 with depth-test
80// gating so the gradient shows through only on rays that miss every
81// voxel face. No framebuffer-side helper needed.
82
83
84// ===== Bounds-safe pixel set =======================================
85func nx_fb_set(
86 fb: *i64, w: nx_int, h: nx_int, x: nx_int, y: nx_int, color: nx_int
87) {
88 if x < 0 { return }
89 if x >= w { return }
90 if y < 0 { return }
91 if y >= h { return }
92 fb[y * w + x] = color
93}
94
95func nx_fb_get(
96 fb: *i64, w: nx_int, h: nx_int, x: nx_int, y: nx_int
97) -> nx_int {
98 if x < 0 { return 0 }
99 if x >= w { return 0 }
100 if y < 0 { return 0 }
101 if y >= h { return 0 }
102 return fb[y * w + x]
103}
104
105// ===== Depth-tested set ============================================
106// Writes pixel + depth iff z_q14 < zb[idx]. Returns 1 if written.
107func nx_fb_depth_test_set(
108 fb: *i64, zb: *i64,
109 w: nx_int, h: nx_int,
110 x: nx_int, y: nx_int,
111 z_q14: nx_int, color: nx_int
112) -> nx_int {
113 if x < 0 { return 0 }
114 if x >= w { return 0 }
115 if y < 0 { return 0 }
116 if y >= h { return 0 }
117 let idx: nx_int = y * w + x
118 if z_q14 < zb[idx] {
119 fb[idx] = color
120 zb[idx] = z_q14
121 return 1
122 }
123 return 0
124}
125
126// ===== Color helpers ===============================================
127// Pack (R, G, B, A) in [0, 255] into a single i64. RGBA order; the
128// JS shim reads as little-endian RGBA8 from the same 32 bits.
129func nx_fb_pack_rgba(r: nx_int, g: nx_int, b: nx_int, a: nx_int) -> nx_int {
130 var rr: nx_int = r
131 if rr < 0 { rr = 0 }
132 if rr > 255 { rr = 255 }
133 var gg: nx_int = g
134 if gg < 0 { gg = 0 }
135 if gg > 255 { gg = 255 }
136 var bb: nx_int = b
137 if bb < 0 { bb = 0 }
138 if bb > 255 { bb = 255 }
139 var aa: nx_int = a
140 if aa < 0 { aa = 0 }
141 if aa > 255 { aa = 255 }
142 return rr + gg * 256 + bb * NX_MAGIC_65536 + aa * NX_MAGIC_16777216
143}
144
145// Unpack (mostly for tests).
146func nx_fb_unpack_r(color: nx_int) -> nx_int { return color % 256 }
147func nx_fb_unpack_g(color: nx_int) -> nx_int { return (color / 256) % 256 }
148func nx_fb_unpack_b(color: nx_int) -> nx_int { return (color / NX_MAGIC_65536) % 256 }
149func nx_fb_unpack_a(color: nx_int) -> nx_int { return (color / NX_MAGIC_16777216) % 256 }
150
151// ===== Self-test ====================================================
152func main() -> i64 {
153 let q: nx_int = NX_FB_Q
154
155 // T1: Allocate small framebuffer + depth buffer.
156 let w: nx_int = 8
157 let h: nx_int = 8
158 let fb: *i64 = nx_fb_alloc(w, h)
159 let zb: *i64 = nx_fb_depth_alloc(w, h)
160
161 // T2: Clear + read back.
162 let red: nx_int = nx_fb_pack_rgba(255, 0, 0, 255)
163 nx_fb_clear(fb, w, h, red)
164 if nx_fb_get(fb, w, h, 0, 0) != red { return __syscall(93, 1, 0, 0, 0, 0, 0) }
165 if nx_fb_get(fb, w, h, 7, 7) != red { return __syscall(93, 2, 0, 0, 0, 0, 0) }
166 // Out-of-bounds reads return 0.
167 if nx_fb_get(fb, w, h, 0 - 1, 0) != 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
168 if nx_fb_get(fb, w, h, 100, 0) != 0 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
169
170 // T3: Set + get a single pixel.
171 let blue: nx_int = nx_fb_pack_rgba(0, 0, 255, 255)
172 nx_fb_set(fb, w, h, 3, 4, blue)
173 if nx_fb_get(fb, w, h, 3, 4) != blue { return __syscall(93, 10, 0, 0, 0, 0, 0) }
174 // Neighbour pixels still red.
175 if nx_fb_get(fb, w, h, 2, 4) != red { return __syscall(93, 11, 0, 0, 0, 0, 0) }
176
177 // T4: Pack / unpack RGBA roundtrip.
178 let color: nx_int = nx_fb_pack_rgba(128, 64, 32, 200)
179 if nx_fb_unpack_r(color) != 128 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
180 if nx_fb_unpack_g(color) != 64 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
181 if nx_fb_unpack_b(color) != 32 { return __syscall(93, 22, 0, 0, 0, 0, 0) }
182 if nx_fb_unpack_a(color) != 200 { return __syscall(93, 23, 0, 0, 0, 0, 0) }
183 // Out-of-range clamp.
184 let clamped: nx_int = nx_fb_pack_rgba(0 - 50, 300, 1000, 50)
185 if nx_fb_unpack_r(clamped) != 0 { return __syscall(93, 24, 0, 0, 0, 0, 0) }
186 if nx_fb_unpack_g(clamped) != 255 { return __syscall(93, 25, 0, 0, 0, 0, 0) }
187 if nx_fb_unpack_b(clamped) != 255 { return __syscall(93, 26, 0, 0, 0, 0, 0) }
188
189 // T5: Depth-test write.
190 nx_fb_clear_depth(zb, w, h)
191 // Initial: NEAR (z=5) writes; subsequent FAR (z=10) blocks.
192 let green: nx_int = nx_fb_pack_rgba(0, 255, 0, 255)
193 let wrote1: nx_int = nx_fb_depth_test_set(fb, zb, w, h, 1, 1, 5 * q, green)
194 if wrote1 != 1 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
195 // Same pixel, FARTHER z -> rejected.
196 let wrote2: nx_int = nx_fb_depth_test_set(fb, zb, w, h, 1, 1, 10 * q, red)
197 if wrote2 != 0 { return __syscall(93, 31, 0, 0, 0, 0, 0) }
198 if nx_fb_get(fb, w, h, 1, 1) != green { return __syscall(93, 32, 0, 0, 0, 0, 0) }
199 // Same pixel, NEARER z -> accepted.
200 let wrote3: nx_int = nx_fb_depth_test_set(fb, zb, w, h, 1, 1, 2 * q, blue)
201 if wrote3 != 1 { return __syscall(93, 33, 0, 0, 0, 0, 0) }
202 if nx_fb_get(fb, w, h, 1, 1) != blue { return __syscall(93, 34, 0, 0, 0, 0, 0) }
203
204 // T6: Depth-clear resets all cells to far plane.
205 nx_fb_clear_depth(zb, w, h)
206 if zb[0] != NX_FB_FAR_DEPTH_Q14 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
207 if zb[w * h - 1] != NX_FB_FAR_DEPTH_Q14 { return __syscall(93, 41, 0, 0, 0, 0, 0) }
208 // After clear, any z writes successfully.
209 let wrote4: nx_int = nx_fb_depth_test_set(fb, zb, w, h, 5, 5, 100 * q, green)
210 if wrote4 != 1 { return __syscall(93, 42, 0, 0, 0, 0, 0) }
211
212 return 0
213}