nx_tex_sample.nx source
↩ module page · 269 lines · 9492 B
1// nx_tex_sample.nx -- UV texture sampling on Image (graphics).
2//
3// Distinct from nx_texture.nx (Haralick GLCM + LBP texture
4// ANALYSIS for image classification). This file is texture
5// SAMPLING for rendering -- the GLSL texture2D / HLSL Sample /
6// Metal texture-access primitive. Completes the graphics minimum
7// stack (nx_raster + nx_zbuf + this).
8//
9// Pure i64 substrate -- no FPU, no GPU. Substrate can now sample
10// textures with bilinear filtering + wrap modes on any backend
11// including Cortex-M3 with a framebuffer.
12//
13// L3 composition (bits-up):
14// nx_image.Image (canonical pixel container, L1)
15// nx_loop.LoopVerdict (bounded loops)
16//
17// ===== UV convention =============================================
18//
19// (u, v) in Q10 fixed-point representing [0, 1) UV space.
20// u_q10 = 0 -> left edge of texture
21// u_q10 = 1024 -> right edge (exclusive)
22// v_q10 = 0 -> top edge of texture
23// v_q10 = 1024 -> bottom edge (exclusive)
24//
25// Q10 over Q14: bilinear weights need sub-pixel accuracy that 8-bit
26// can't give; Q14 is too tight for clean integer multiplication;
27// Q10 = sweet spot for the substrate's existing arithmetic.
28//
29// ===== Wrap modes (sealed enum) ==================================
30//
31// NX_TEX_WRAP_CLAMP -- saturate to [0, W-1] / [0, H-1]
32// NX_TEX_WRAP_REPEAT -- modulo W / H (tile)
33// NX_TEX_WRAP_MIRROR -- ping-pong: every other tile is flipped
34//
35// ===== Filter modes ==============================================
36//
37// NX_TEX_FILTER_NEAREST -- single texel lookup; fast, blocky
38// NX_TEX_FILTER_BILINEAR -- 4-tap weighted average; smooth
39//
40// genealogy_id: catmull_smith_1980_texture_mapping +
41// heckbert_1990_fundamentals_of_texture_mapping +
42// williams_1983_mipmap_pyramidal_parametrics
43// lineage_id: substrate_tex_sample_v1
44
45// nx_safety_envelope:
46// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
47// sil_target: SIL1
48// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
49// verdict: NOT_YET_EVALUATED
50
51import "nx_syscalls.nx"
52import "nx_tier.nx"
53import "nx_loop.nx"
54import "nx_image.nx"
55
56// ===== Constants ==================================================
57
58const NX_TXS_Q10: nx_int = 1024
59
60// ===== Sealed-enum: WrapMode ======================================
61
62const NX_TXS_WRAP_CLAMP: nx_int = 0
63const NX_TXS_WRAP_REPEAT: nx_int = 1
64const NX_TXS_WRAP_MIRROR: nx_int = 2
65const NX_TXS_WRAP_N: nx_int = 3
66
67func nx_txs_wrap_is_valid(k: nx_int) -> nx_int {
68 if k < 0 { return 0 }
69 if k >= NX_TXS_WRAP_N { return 0 }
70 return 1
71}
72
73// ===== Sealed-enum: FilterMode ====================================
74
75const NX_TXS_FILTER_NEAREST: nx_int = 0
76const NX_TXS_FILTER_BILINEAR: nx_int = 1
77const NX_TXS_FILTER_N: nx_int = 2
78
79func nx_txs_filter_is_valid(k: nx_int) -> nx_int {
80 if k < 0 { return 0 }
81 if k >= NX_TXS_FILTER_N { return 0 }
82 return 1
83}
84
85// ===== Sealed-enum: SampleVerdict =================================
86
87const NX_TXS_OK: nx_int = 0
88const NX_TXS_ERR_BAD_TEXTURE: nx_int = 1
89const NX_TXS_ERR_BAD_KIND: nx_int = 2
90const NX_TXS_N_VERDICTS: nx_int = 3
91
92func nx_txs_verdict_is_valid(v: nx_int) -> nx_int {
93 if v < 0 { return 0 }
94 if v >= NX_TXS_N_VERDICTS { return 0 }
95 return 1
96}
97
98// ===== Wrap helpers ==============================================
99
100func _txs_wrap_coord(x: nx_int, dim: nx_int, mode: nx_int) -> nx_int {
101 if dim <= 0 { return 0 }
102 if mode == NX_TXS_WRAP_CLAMP {
103 if x < 0 { return 0 }
104 if x >= dim { return dim - 1 }
105 return x
106 }
107 if mode == NX_TXS_WRAP_REPEAT {
108 var r: nx_int = x - (x / dim) * dim
109 if r < 0 { r = r + dim }
110 return r
111 }
112 if mode == NX_TXS_WRAP_MIRROR {
113 let period: nx_int = 2 * dim
114 var r: nx_int = x - (x / period) * period
115 if r < 0 { r = r + period }
116 if r >= dim { r = period - 1 - r }
117 return r
118 }
119 return 0
120}
121
122// ===== Nearest-neighbor sample ====================================
123
124func nx_txs_sample_nearest(tex: *Image, u_q10: nx_int, v_q10: nx_int,
125 wrap: nx_int) -> nx_int {
126 let w: nx_int = tex.width
127 let h: nx_int = tex.height
128 let tx_raw: nx_int = (u_q10 * w) / NX_TXS_Q10
129 let ty_raw: nx_int = (v_q10 * h) / NX_TXS_Q10
130 let tx: nx_int = _txs_wrap_coord(tx_raw, w, wrap)
131 let ty: nx_int = _txs_wrap_coord(ty_raw, h, wrap)
132 return nx_image_get(tex, tx, ty, 0)
133}
134
135// ===== Bilinear sample ============================================
136//
137// 4-tap weighted-average bilinear. Pixel-centre alignment via
138// -Q10/2 shift (canonical convention; otherwise filter biased
139// by half a texel).
140
141func nx_txs_sample_bilinear(tex: *Image, u_q10: nx_int, v_q10: nx_int,
142 wrap: nx_int) -> nx_int {
143 let w: nx_int = tex.width
144 let h: nx_int = tex.height
145 let u_t: nx_int = (u_q10 * w) - NX_TXS_Q10 / 2
146 let v_t: nx_int = (v_q10 * h) - NX_TXS_Q10 / 2
147
148 var tx0: nx_int = u_t / NX_TXS_Q10
149 var ty0: nx_int = v_t / NX_TXS_Q10
150 if u_t < 0 {
151 if u_t - tx0 * NX_TXS_Q10 != 0 { tx0 = tx0 - 1 }
152 }
153 if v_t < 0 {
154 if v_t - ty0 * NX_TXS_Q10 != 0 { ty0 = ty0 - 1 }
155 }
156
157 let frac_u: nx_int = u_t - tx0 * NX_TXS_Q10
158 let frac_v: nx_int = v_t - ty0 * NX_TXS_Q10
159
160 let tx0_wr: nx_int = _txs_wrap_coord(tx0, w, wrap)
161 let ty0_wr: nx_int = _txs_wrap_coord(ty0, h, wrap)
162 let tx1_wr: nx_int = _txs_wrap_coord(tx0 + 1, w, wrap)
163 let ty1_wr: nx_int = _txs_wrap_coord(ty0 + 1, h, wrap)
164
165 let t00: nx_int = nx_image_get(tex, tx0_wr, ty0_wr, 0)
166 let t10: nx_int = nx_image_get(tex, tx1_wr, ty0_wr, 0)
167 let t01: nx_int = nx_image_get(tex, tx0_wr, ty1_wr, 0)
168 let t11: nx_int = nx_image_get(tex, tx1_wr, ty1_wr, 0)
169
170 let one_minus_u: nx_int = NX_TXS_Q10 - frac_u
171 let one_minus_v: nx_int = NX_TXS_Q10 - frac_v
172
173 let top: nx_int = (t00 * one_minus_u + t10 * frac_u) / NX_TXS_Q10
174 let bot: nx_int = (t01 * one_minus_u + t11 * frac_u) / NX_TXS_Q10
175 return (top * one_minus_v + bot * frac_v) / NX_TXS_Q10
176}
177
178// ===== Dispatch wrapper ===========================================
179
180func nx_txs_sample(tex: *Image, u_q10: nx_int, v_q10: nx_int,
181 wrap: nx_int, filter: nx_int) -> nx_int {
182 if filter == NX_TXS_FILTER_NEAREST { return nx_txs_sample_nearest(tex, u_q10, v_q10, wrap) }
183 if filter == NX_TXS_FILTER_BILINEAR { return nx_txs_sample_bilinear(tex, u_q10, v_q10, wrap) }
184 return 0
185}
186
187// ===== Self-test ==================================================
188//
189// Closed-form invariants:
190// (a) Constant texture: every sample = constant value.
191// (b) Nearest at exact texel center: hits that texel.
192// (c) Bilinear at exact texel center: within +/- 2 of texel.
193// (d) Wrap CLAMP at u_q10 >= Q10: clamped to last texel.
194// (e) Wrap REPEAT at u_q10 > Q10: wrapped to first column.
195// (f) Wrap MIRROR: ping-pong mirroring.
196// (g) Verdict + wrap + filter range gates.
197
198func main() -> i64 {
199 let W: nx_int = 4
200 let H: nx_int = 4
201 let tex: *Image = nx_image_alloc(W, H, 1)
202
203 var r: nx_int = 0
204 while r < H {
205 var c: nx_int = 0
206 while c < W {
207 nx_image_set(tex, c, r, 0, 10 + r * 40 + c * 10)
208 c = c + 1
209 }
210 r = r + 1
211 }
212
213 // --- (a) Constant texture sample ---
214 let const_tex: *Image = nx_image_alloc(W, H, 1)
215 var ri: nx_int = 0
216 while ri < H {
217 var ci: nx_int = 0
218 while ci < W {
219 nx_image_set(const_tex, ci, ri, 0, 77)
220 ci = ci + 1
221 }
222 ri = ri + 1
223 }
224 if nx_txs_sample_nearest(const_tex, 0, 0, NX_TXS_WRAP_CLAMP) != 77 { return 10 }
225 if nx_txs_sample_nearest(const_tex, 512, 512, NX_TXS_WRAP_CLAMP) != 77 { return 11 }
226 if nx_txs_sample_bilinear(const_tex, 384, 256, NX_TXS_WRAP_CLAMP) != 77 { return 12 }
227
228 // --- (b) Nearest at exact texel index ---
229 // u_q10 = 256 -> texel 1 (256 * 4 / 1024 = 1).
230 let n0: nx_int = nx_txs_sample_nearest(tex, 256, 256, NX_TXS_WRAP_CLAMP)
231 if n0 != 60 { return 20 } // tex[1, 1] = 10 + 40 + 10 = 60
232
233 // --- (c) Bilinear at texel center ---
234 let b0: nx_int = nx_txs_sample_bilinear(tex, 384, 384, NX_TXS_WRAP_CLAMP)
235 let drift_b0: nx_int = b0 - 60
236 if drift_b0 > 2 { return 30 }
237 if drift_b0 < -2 { return 31 }
238
239 // --- (d) CLAMP at u >= Q10 ---
240 let cl: nx_int = nx_txs_sample_nearest(tex, 2048, 0, NX_TXS_WRAP_CLAMP)
241 if cl != 40 { return 50 } // tex[3, 0] = 40
242
243 // --- (e) REPEAT at u_q10 = 1.25 Q10 ---
244 let rp: nx_int = nx_txs_sample_nearest(tex, 1280, 0, NX_TXS_WRAP_REPEAT)
245 if rp != 20 { return 60 } // wraps to u_q10 = 256, tex[1, 0] = 20
246
247 // --- (f) MIRROR at u_q10 = 1.25 Q10 ---
248 let mr: nx_int = nx_txs_sample_nearest(tex, 1280, 0, NX_TXS_WRAP_MIRROR)
249 if mr != 30 { return 70 } // tx_raw=5, period 8, r = 8-1-5 = 2 -> tex[2, 0] = 30
250
251 // --- (g) Verdict + wrap + filter gates ---
252 var vi: nx_int = 0
253 while vi < NX_TXS_N_VERDICTS {
254 if nx_txs_verdict_is_valid(vi) != 1 { return 80 + vi }
255 vi = vi + 1
256 }
257 var wi: nx_int = 0
258 while wi < NX_TXS_WRAP_N {
259 if nx_txs_wrap_is_valid(wi) != 1 { return 90 + wi }
260 wi = wi + 1
261 }
262 var fi: nx_int = 0
263 while fi < NX_TXS_FILTER_N {
264 if nx_txs_filter_is_valid(fi) != 1 { return 100 + fi }
265 fi = fi + 1
266 }
267
268 return 0
269}