code wiki / (root) / nx_tex_sample.nx

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}