code wiki / (root) / nx_texpaint_gate.nx

nx_texpaint_gate.nx source

↩ module page · 548 lines · 35320 B

1// nx_texpaint_gate.nx -- the gate for nx_texpaint_lib (/compare/dcc DC7, texture painting). 2// 3// THE HAZARD THIS GATE IS SHAPED AGAINST is the one the task names: a paint function that writes THE WHOLE 4// ATLAS trivially passes "the known texel changed". So the load-bearing cell here is not the positive 5// control on its own -- it is the pair of BOUNDED-WRITE teeth beside it, which count the changed texels 6// against a HAND-COMPUTED disc area and then count the changed texels OUTSIDE the brush and require zero. 7// A whole-atlas painter fails both, a do-nothing painter fails the first, and only a correct one passes 8// both. Every disc area, every texel address and every painted BYTE VALUE below was worked out by hand 9// before the code was run; they are written as literals with their arithmetic in the comment, so a fixture 10// that agrees with a wrong implementation would have to be wrong in exactly the same way. 11// 12// THE ANTI-TEAR PROOF IS A PERMANENT NEGATIVE CONTROL, NOT AN ANECDOTE. tp_stroke_core is the naive painter 13// -- shipped, public, callable -- so the tooth that proves the seam bleed matters RUNS the bleed-free 14// version and shows the far side of the cut untouched, then runs the full stroke and shows it painted. That 15// is a gv_bite that can be re-run by anyone, rather than a claim that the author once deleted the fix and 16// watched a test go red. 17// 18// FIXTURE GEOMETRY, chosen so every boundary is exact and nothing rests on a rounding accident: 19// res 64, grid 4 -> tile width in UV units 65536/4 = 16384, chart bounds 16384*64/65536 = 16 texels. 20// So chart 0 is texels [0,16) x [0,16) and the four charts tile the atlas exactly, with no remainder. 21// A SECOND map set at grid 3 is carried on purpose: 65536/3 truncates to 21845, the charts land at 0,21,42 22// and stop at 63, so ONE texel column belongs to no chart at all. That is the case where a partition claim 23// could quietly leak, and it is asserted to SUM rather than assumed to. 24// 100% sovereign. No hardware writes (Rule 26). license_tier: ORIGINAL expect_exit: 0 25import "nx_syscalls.nx" 26import "nx_gate_verdict.nx" 27import "nx_vecmath.nx" 28import "nx_editstack_lib.nx" 29import "nx_texpaint_lib.nx" 30 31const TG_RES: i64 = 64 32const TG_GRID: i64 = 4 33const TG_GRID_ODD: i64 = 3 // the grid that does NOT divide the UV span evenly 34const TG_NMAPS: i64 = 4 // albedo, specular, gloss, normal -- the shape of ntb_bake_pbr's map set 35const TG_CAP: i64 = 8 // stroke-log capacity for the session fixtures 36const TG_ALL_CH: i64 = 7 // every channel: mask 111 37const TG_CH_G: i64 = 2 // green only: mask 010 38const TG_CH_R: i64 = 1 // red only: mask 001 39 40// THE HAND-COMPUTED FIXTURE STROKE. Centre (8,8) is the middle of chart 0, radius 3, full strength, target 41// 255 onto a black base. 42// AREA, counted by hand over dx,dy with dx*dx + dy*dy < 9 (the falloff is zero AT the rim, so the written 43// set is the OPEN disc): dy=0 -> dx in {0,1,2} = 5 texels; dy=1 -> dx*dx < 8 -> 5; dy=2 -> dx*dx < 5 -> 5; 44// dy=3 -> dx*dx < 0 -> none. 5 + 5 + 5 + 5 + 5 = 25. 45const TG_CX: i64 = 8 46const TG_CY: i64 = 8 47const TG_R: i64 = 3 48const TG_DISC_AREA: i64 = 25 49// The centre texel takes the target EXACTLY at full strength: d=0 -> t=1000 -> S=1000 -> weight 1000 -> 50// 0 + (255-0)*1000/1000 = 255. 51const TG_CENTRE_BYTE: i64 = 255 52// The texel two out along u: d=2 -> t=(3-2)*1000/3=333 -> S = 333*333*(3000-2*333)/1000000 53// = 110889*2334/1000000 = 258 54// -> weight 1000*258/1000 = 258 -> 0 + 255*258/1000 = 65790/1000 = 65. 55const TG_RIM_IN_DX: i64 = 2 56const TG_RIM_IN_BYTE: i64 = 65 57// The texel three out along u is AT the rim: 9 is not < 9, so it is never written and keeps its base byte. 58const TG_RIM_OUT_DX: i64 = 3 59 60// THE SEAM STROKE. Same brush, centred one texel from chart 0's cut at (1,8). 61// PLANAR half, by hand: xx runs 0..4 so dx in {1,0,1,2,3}; with dy in 0..3 and dx*dx+dy*dy < 9 that is 62// dy=3 -> none; dy=2 -> dx in {0,1,2} -> 4 texels (xx 0,1,2,3); dy=1 -> 4; dy=0 -> 4. 63// dy=2 appears twice (yy 6 and 10), dy=1 twice, dy=0 once: 4*2 + 4*2 + 4 = 20. 64// WRAPPED half, by hand: chart width 16, margin min(3, 8) = 3, so the far band is xx in {13,14,15}. 65// xx=13 -> cylindrical dx 4 -> 16 >= 9 -> no. xx=14 -> dx 3 -> 9 >= 9 -> no. 66// xx=15 -> dx 2 -> 4 + dy*dy < 9 -> dy in {0,1,2} -> yy in {6,7,8,9,10} -> 5 texels. 67// 20 + 5 = 25, WHICH IS THE INTERIOR DISC AREA. That equality is the whole anti-tear claim in one number: 68// no texel of the stroke was lost at the cut and none was written twice. 69const TG_SEAM_CX: i64 = 1 70const TG_SEAM_PLANAR: i64 = 20 71const TG_SEAM_BLEED: i64 = 5 72const TG_SEAM_FAR_X: i64 = 15 73 74const TG_STRENGTH: i64 = 1000 75const TG_VALUE: i64 = 255 76const TG_HALFSTR: i64 = 500 77 78// THE SESSION FIXTURE, three strokes, every parameter named so the log and the direct application below are 79// reading one description rather than two copies of a number. 80const TG_NSTROKES: i64 = 3 81// stroke 2: red channel of the SPECULAR map, centred one texel from the cut, partial strength. 82const TG_S2_MAP: i64 = 1 83const TG_S2_STRENGTH: i64 = 600 84const TG_S2_VALUE: i64 = 10 85// stroke 3: green channel of the ALBEDO map, deliberately OVERLAPPING stroke 1's disc, so that 86// re-evaluating stroke 1 has to be recomposed UNDER it. That makes the re-evaluation tooth an ordering 87// test and not merely a parameter test -- a stack that re-applied the later strokes in the wrong order, or 88// dropped them, would still change the state and would still look like a working re-evaluation. 89const TG_S3_CX: i64 = 9 90const TG_S3_CY: i64 = 9 91const TG_S3_R: i64 = 2 92const TG_S3_STRENGTH: i64 = 300 93const TG_S3_VALUE: i64 = 200 94const TG_REEVAL_VALUE: i64 = 100 95 96// the surface-point fixture, worked out by hand in the header of the tooth that uses it 97const TG_UVSTRIDE_TEXC: i64 = 3 98const TG_UVSTRIDE_PAIR: i64 = 2 99const TG_NVERT: i64 = 3 100const TG_NTRI: i64 = 1 101const TG_U1: i64 = 32768 102const TG_V2: i64 = 32768 103const TG_B0: i64 = 200 104const TG_B1: i64 = 300 105const TG_B2: i64 = 500 106const TG_EXP_U: i64 = 9830 // (200*0 + 300*32768 + 500*0)/1000 = 9830400/1000 = 9830 (floor of 9830.4) 107const TG_EXP_V: i64 = 16384 // (200*0 + 300*0 + 500*32768)/1000 = 16384000/1000 = 16384 exactly 108const TG_EXP_X: i64 = 9 // 9830*64/65536 = 629120/65536 = 9 (floor of 9.5996) 109const TG_EXP_Y: i64 = 16 // 16384*64/65536 = 1048576/65536 = 16 exactly 110 111// grid-3 chart bounds, by hand: 65536/3 = 21845; 1*21845*64/65536 = 21; 2*... = 42; 3*... = 63. 112const TG_ODD_COVER: i64 = 63 113const TG_ODD_UNCOVERED: i64 = 1 114 115func tg_eq(a: i64, b: i64) -> i64 { if a == b { return 1 } return 0 } 116func tg_ne(a: i64, b: i64) -> i64 { if a != b { return 1 } return 0 } 117func tg_gt(a: i64, b: i64) -> i64 { if a > b { return 1 } return 0 } 118func tg_ge(a: i64, b: i64) -> i64 { if a >= b { return 1 } return 0 } 119func tg_le(a: i64, b: i64) -> i64 { if a <= b { return 1 } return 0 } 120func tg_say(s: *u8, v: i64) -> i64 { gv_puts(s); gv_num(v); gv_puts("\n" as *u8); return 0 } 121 122// A base that is NOT uniform, so a copy that drops a map or a row cannot pass the replay teeth. Derived 123// entirely from the address, so it carries no free constant of its own. 124func tg_fill(ms: *i64) -> i64 { 125 let res: i64 = tp_res(ms) 126 let nm: i64 = tp_nmaps(ms) 127 var m: i64 = 0 128 while m < nm { 129 let p: *u8 = tp_map_ptr(ms, m) 130 var y: i64 = 0 131 while y < res { 132 var x: i64 = 0 133 while x < res { 134 var c: i64 = 0 135 while c < TP_CHAN { 136 p[(y * res + x) * TP_CHAN + c] = ((m * TP_CHAN + c + y * res + x) % tp_value_span()) as u8 137 c = c + 1 138 } 139 x = x + 1 140 } 141 y = y + 1 142 } 143 m = m + 1 144 } 145 return 0 146} 147 148func tg_differs(a: *i64, b: *i64, m: i64, x: i64, y: i64) -> i64 { 149 var c: i64 = 0 150 while c < TP_CHAN { 151 if tp_get(a, m, x, y, c) != tp_get(b, m, x, y, c) { return 1 } 152 c = c + 1 153 } 154 return 0 155} 156func tg_changed_map(a: *i64, b: *i64, m: i64) -> i64 { 157 let res: i64 = tp_res(a) 158 var n: i64 = 0 159 var y: i64 = 0 160 while y < res { 161 var x: i64 = 0 162 while x < res { n = n + tg_differs(a, b, m, x, y); x = x + 1 } 163 y = y + 1 164 } 165 return n 166} 167// 1 when the texel is outside the stroke under the CYLINDRICAL metric -- including "in a different chart", 168// which is the leak that would let paint cross onto another bone. 169func tg_out_of_disc(ms: *i64, cx: i64, cy: i64, r: i64, x: i64, y: i64) -> i64 { 170 let res: i64 = tp_res(ms) 171 let g: i64 = tp_grid(ms) 172 let cch: i64 = tp_chart_of_texel(res, g, cx, cy) 173 if tp_chart_of_texel(res, g, x, y) != cch { return 1 } 174 let tx: i64 = cch % g 175 let w: i64 = tp_chart_x1(res, g, tx) - tp_chart_x0(res, g, tx) 176 let dxp: i64 = vm_abs(x - cx) 177 var dx: i64 = dxp 178 if w - dxp < dxp { dx = w - dxp } 179 let dy: i64 = vm_abs(y - cy) 180 if dx * dx + dy * dy >= r * r { return 1 } 181 return 0 182} 183func tg_changed_outside(a: *i64, b: *i64, m: i64, cx: i64, cy: i64, r: i64) -> i64 { 184 let res: i64 = tp_res(a) 185 var n: i64 = 0 186 var y: i64 = 0 187 while y < res { 188 var x: i64 = 0 189 while x < res { 190 if tg_differs(a, b, m, x, y) == 1 { 191 if tg_out_of_disc(a, cx, cy, r, x, y) == 1 { n = n + 1 } 192 } 193 x = x + 1 194 } 195 y = y + 1 196 } 197 return n 198} 199// changed texels in every map OTHER than m -- a paint op that splashes across the map set fails this 200func tg_changed_other_maps(a: *i64, b: *i64, m: i64) -> i64 { 201 var n: i64 = 0 202 var k: i64 = 0 203 while k < tp_nmaps(a) { 204 if k != m { n = n + tg_changed_map(a, b, k) } 205 k = k + 1 206 } 207 return n 208} 209func tg_column_changed(a: *i64, b: *i64, m: i64, x: i64) -> i64 { 210 let res: i64 = tp_res(a) 211 var n: i64 = 0 212 var y: i64 = 0 213 while y < res { n = n + tg_differs(a, b, m, x, y); y = y + 1 } 214 return n 215} 216 217// a fresh black map set with the standard fixture geometry 218func tg_black() -> *i64 { return tp_maps_new(TG_RES, TG_NMAPS, TG_GRID) } 219// a fresh patterned map set 220func tg_patterned() -> *i64 { 221 let ms: *i64 = tp_maps_new(TG_RES, TG_NMAPS, TG_GRID) 222 if (ms as i64) == 0 { return ms } 223 tg_fill(ms) 224 return ms 225} 226// the three strokes of the session fixture, applied DIRECTLY, in order 227func tg_apply_session(ms: *i64, first_value: i64) -> i64 { 228 tp_stroke(ms, 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, first_value) 229 tp_stroke(ms, TG_S2_MAP, TG_CH_R, TG_SEAM_CX, TG_CY, TG_R, TG_S2_STRENGTH, TG_S2_VALUE) 230 tp_stroke(ms, 0, TG_CH_G, TG_S3_CX, TG_S3_CY, TG_S3_R, TG_S3_STRENGTH, TG_S3_VALUE) 231 return 0 232} 233func tg_record_session(st: *i64, ms: *i64, first_value: i64) -> i64 { 234 tp_record(st, ms, 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, first_value) 235 tp_record(st, ms, TG_S2_MAP, TG_CH_R, TG_SEAM_CX, TG_CY, TG_R, TG_S2_STRENGTH, TG_S2_VALUE) 236 tp_record(st, ms, 0, TG_CH_G, TG_S3_CX, TG_S3_CY, TG_S3_R, TG_S3_STRENGTH, TG_S3_VALUE) 237 return 0 238} 239 240func main(argc: i64, argv: *i64) -> i64 { 241 let ctr: *i64 = gv_ctr() 242 gv_head("nx_texpaint_gate -- a brush stroke that lands in the right texel and does not tear at the cut" as *u8) 243 244 // ================= THE POSITIVE CONTROL: geometry that must come back clean ================= 245 let base: *i64 = tg_black() 246 gv_check("fixture-map-set-allocated" as *u8, tg_ne(base as i64, 0), ctr) 247 gv_check("fixture-geometry-is-accepted" as *u8, tg_eq(tp_geom_ok(TG_RES, TG_NMAPS, TG_GRID), TP_OK), ctr) 248 gv_check("fixture-reports-back-its-own-resolution" as *u8, tg_eq(tp_res(base), TG_RES), ctr) 249 gv_check("fixture-reports-back-its-own-map-count" as *u8, tg_eq(tp_nmaps(base), TG_NMAPS), ctr) 250 gv_check("a-new-map-set-is-black" as *u8, tg_eq(tp_get(base, 0, TG_CX, TG_CY, 0), 0), ctr) 251 gv_check("channel-mask-span-is-two-to-the-channel-count" as *u8, tg_eq(tp_ch_span_derived(), TP_CH_SPAN), ctr) 252 253 // ---- the chart partition. A partition is a claim: check that the parts SUM. ---- 254 var covered: i64 = 0 255 var t: i64 = 0 256 while t < TG_GRID { covered = covered + tp_chart_x1(TG_RES, TG_GRID, t) - tp_chart_x0(TG_RES, TG_GRID, t); t = t + 1 } 257 tg_say(" grid4 chart columns covered = " as *u8, covered) 258 gv_check("grid-4-charts-tile-the-atlas-axis-exactly" as *u8, tg_eq(covered, TG_RES), ctr) 259 gv_check("grid-4-chart-zero-starts-at-zero" as *u8, tg_eq(tp_chart_x0(TG_RES, TG_GRID, 0), 0), ctr) 260 gv_check("the-known-texel-resolves-to-the-known-chart" as *u8, tg_eq(tp_chart_of_texel(TG_RES, TG_GRID, TG_CX, TG_CY), 0), ctr) 261 gv_check("a-texel-in-the-next-tile-resolves-to-a-different-chart" as *u8, tg_eq(tp_chart_of_texel(TG_RES, TG_GRID, TG_RES / TG_GRID, TG_CY), 1), ctr) 262 263 // grid 3 does NOT divide the UV span, so the last column belongs to no chart. The remainder is NAMED 264 // and the two parts are asserted to sum -- an unexplained residual is a leak, an explained one is a fact. 265 var covered3: i64 = 0 266 var t3: i64 = 0 267 while t3 < TG_GRID_ODD { covered3 = covered3 + tp_chart_x1(TG_RES, TG_GRID_ODD, t3) - tp_chart_x0(TG_RES, TG_GRID_ODD, t3); t3 = t3 + 1 } 268 tg_say(" grid3 chart columns covered = " as *u8, covered3) 269 tg_say(" grid3 columns belonging to no chart = " as *u8, TG_RES - covered3) 270 gv_check("grid-3-coverage-is-the-hand-computed-63" as *u8, tg_eq(covered3, TG_ODD_COVER), ctr) 271 gv_check("grid-3-covered-plus-uncovered-sums-to-the-atlas" as *u8, tg_eq(covered3 + TG_ODD_UNCOVERED, TG_RES), ctr) 272 gv_check("the-uncovered-column-honestly-resolves-to-no-chart" as *u8, tg_eq(tp_chart_axis(TG_RES, TG_GRID_ODD, TG_RES - 1), 0 - 1), ctr) 273 274 // ================= 1. SURFACE POINT -> TEXEL, HAND-COMPUTED ================= 275 // v0 at UV (0,0), v1 at (32768,0), v2 at (0,32768); one triangle; barycentric (200,300,500) permil. 276 // The expected u, v, x and y are each derived in the const block above, arithmetic included. 277 let uv3: *i64 = sys_mmap(TG_NVERT * TG_UVSTRIDE_TEXC * 8) as *i64 278 uv3[0 * TG_UVSTRIDE_TEXC + 0] = 0 279 uv3[0 * TG_UVSTRIDE_TEXC + 1] = 0 280 uv3[1 * TG_UVSTRIDE_TEXC + 0] = TG_U1 281 uv3[1 * TG_UVSTRIDE_TEXC + 1] = 0 282 uv3[2 * TG_UVSTRIDE_TEXC + 0] = 0 283 uv3[2 * TG_UVSTRIDE_TEXC + 1] = TG_V2 284 let uv2: *i64 = sys_mmap(TG_NVERT * TG_UVSTRIDE_PAIR * 8) as *i64 285 uv2[0] = 0 286 uv2[1] = 0 287 uv2[TG_UVSTRIDE_PAIR + 0] = TG_U1 288 uv2[TG_UVSTRIDE_PAIR + 1] = 0 289 uv2[TG_UVSTRIDE_PAIR * 2 + 0] = 0 290 uv2[TG_UVSTRIDE_PAIR * 2 + 1] = TG_V2 291 let tris: *i64 = sys_mmap(TG_NTRI * 3 * 8) as *i64 292 tris[0] = 0 293 tris[1] = 1 294 tris[2] = 2 295 let o2: *i64 = sys_mmap(2 * 8) as *i64 296 297 let urc: i64 = tp_surface_to_uv(uv3, TG_UVSTRIDE_TEXC, tris, 0, TG_NTRI, TG_NVERT, TG_B0, TG_B1, TG_B2, o2) 298 tg_say(" interpolated u = " as *u8, o2[0]) 299 tg_say(" interpolated v = " as *u8, o2[1]) 300 gv_check("surface-to-uv-accepts-a-valid-barycentric-point" as *u8, tg_eq(urc, TP_OK), ctr) 301 gv_check("POSITIVE-CONTROL-interpolated-u-is-the-hand-computed-9830" as *u8, tg_eq(o2[0], TG_EXP_U), ctr) 302 gv_check("POSITIVE-CONTROL-interpolated-v-is-the-hand-computed-16384" as *u8, tg_eq(o2[1], TG_EXP_V), ctr) 303 let trc: i64 = tp_uv_to_texel(TG_EXP_U, TG_EXP_V, TG_RES, o2) 304 tg_say(" texel x = " as *u8, o2[0]) 305 tg_say(" texel y = " as *u8, o2[1]) 306 gv_check("uv-to-texel-accepts-an-in-range-uv" as *u8, tg_eq(trc, TP_OK), ctr) 307 gv_check("POSITIVE-CONTROL-the-known-uv-lands-in-the-hand-computed-texel-x" as *u8, tg_eq(o2[0], TG_EXP_X), ctr) 308 gv_check("POSITIVE-CONTROL-the-known-uv-lands-in-the-hand-computed-texel-y" as *u8, tg_eq(o2[1], TG_EXP_Y), ctr) 309 let src: i64 = tp_surface_to_texel(uv3, TG_UVSTRIDE_TEXC, tris, 0, TG_NTRI, TG_NVERT, TG_B0, TG_B1, TG_B2, TG_RES, o2) 310 gv_check("the-composition-agrees-with-its-two-halves-on-x" as *u8, tg_eq(o2[0], TG_EXP_X), ctr) 311 gv_check("the-composition-agrees-with-its-two-halves-on-y" as *u8, tg_eq(o2[1], TG_EXP_Y), ctr) 312 gv_check("the-composition-reports-success" as *u8, tg_eq(src, TP_OK), ctr) 313 let s2rc: i64 = tp_surface_to_texel(uv2, TG_UVSTRIDE_PAIR, tris, 0, TG_NTRI, TG_NVERT, TG_B0, TG_B1, TG_B2, TG_RES, o2) 314 gv_check("a-stride-2-uv-array-gives-the-identical-address" as *u8, tg_eq(o2[0] * TG_RES + o2[1], TG_EXP_X * TG_RES + TG_EXP_Y), ctr) 315 gv_check("a-stride-2-uv-array-is-accepted" as *u8, tg_eq(s2rc, TP_OK), ctr) 316 317 // ================= THE DECLARED FALLOFF CURVE ================= 318 gv_check("smoothstep-is-zero-at-zero" as *u8, tg_eq(tp_smoothstep(0), 0), ctr) 319 gv_check("smoothstep-is-unity-at-unity" as *u8, tg_eq(tp_smoothstep(TP_ONE), TP_ONE), ctr) 320 gv_check("smoothstep-is-one-half-at-one-half" as *u8, tg_eq(tp_smoothstep(TP_ONE / 2), TP_ONE / 2), ctr) 321 var mono: i64 = 1 322 var prev: i64 = 0 - 1 323 var tt: i64 = 0 324 while tt <= TP_ONE { 325 let s: i64 = tp_smoothstep(tt) 326 if s < prev { mono = 0 } 327 prev = s 328 tt = tt + 1 329 } 330 gv_check("smoothstep-is-monotone-over-its-whole-domain" as *u8, mono, ctr) 331 gv_check("falloff-is-full-at-the-centre" as *u8, tg_eq(tp_falloff(0, TG_R), TP_ONE), ctr) 332 gv_check("falloff-is-exactly-zero-AT-the-rim" as *u8, tg_eq(tp_falloff(TG_R, TG_R), 0), ctr) 333 gv_check("falloff-is-zero-beyond-the-rim" as *u8, tg_eq(tp_falloff(TG_R + 1, TG_R), 0), ctr) 334 335 // ================= THE PACKING ROUND-TRIPS ================= 336 let pw: i64 = tp_pack_what(TG_NMAPS - 1, TG_ALL_CH, TG_VALUE) 337 gv_check("what-word-round-trips-the-map-id" as *u8, tg_eq(tp_what_map(pw), TG_NMAPS - 1), ctr) 338 gv_check("what-word-round-trips-the-channel-mask" as *u8, tg_eq(tp_what_chmask(pw), TG_ALL_CH), ctr) 339 gv_check("what-word-round-trips-the-value" as *u8, tg_eq(tp_what_value(pw), TG_VALUE), ctr) 340 let pc: i64 = tp_pack_centre(TG_RES, TG_RES - 1, TG_RES - 1) 341 gv_check("centre-word-round-trips-x" as *u8, tg_eq(tp_centre_x(TG_RES, pc), TG_RES - 1), ctr) 342 gv_check("centre-word-round-trips-y" as *u8, tg_eq(tp_centre_y(TG_RES, pc), TG_RES - 1), ctr) 343 let pb: i64 = tp_pack_brush(TG_R, TP_STRENGTH_MAX) 344 gv_check("brush-word-round-trips-the-radius" as *u8, tg_eq(tp_brush_r(pb), TG_R), ctr) 345 gv_check("brush-word-round-trips-the-strength-at-its-maximum" as *u8, tg_eq(tp_brush_strength(pb), TP_STRENGTH_MAX), ctr) 346 347 // ================= 2. THE BOUNDED WRITE -- THE ANTI-VACUITY CELL ================= 348 // A paint function that wrote the WHOLE ATLAS would pass "the known texel changed". These are the teeth 349 // that kill it: the changed count must equal the hand-counted disc area, and the changed count OUTSIDE 350 // the brush must be zero. Both directions, and both are needed -- the first alone admits a painter that 351 // writes 25 wrong texels, the second alone admits a painter that writes nothing. 352 let paint: *i64 = tg_black() 353 let n_in: i64 = tp_stroke(paint, 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 354 let ch_in: i64 = tg_changed_map(paint, base, 0) 355 tg_say(" interior stroke: texels painted = " as *u8, n_in) 356 tg_say(" interior stroke: texels changed = " as *u8, ch_in) 357 tg_say(" interior stroke: texels changed outside the radius = " as *u8, tg_changed_outside(paint, base, 0, TG_CX, TG_CY, TG_R)) 358 gv_check("interior-stroke-paints-the-hand-counted-disc-area" as *u8, tg_eq(n_in, TG_DISC_AREA), ctr) 359 gv_check("ANTI-VACUITY-changed-texel-count-equals-the-hand-counted-disc-area" as *u8, tg_eq(ch_in, TG_DISC_AREA), ctr) 360 gv_check("two-independent-counters-of-one-population-agree" as *u8, tg_eq(n_in, ch_in), ctr) 361 gv_check("ANTI-VACUITY-nothing-outside-the-radius-changed" as *u8, tg_eq(tg_changed_outside(paint, base, 0, TG_CX, TG_CY, TG_R), 0), ctr) 362 gv_check("no-other-map-of-the-set-changed" as *u8, tg_eq(tg_changed_other_maps(paint, base, 0), 0), ctr) 363 gv_check("the-centre-texel-reaches-the-target-EXACTLY-at-full-strength" as *u8, tg_eq(tp_get(paint, 0, TG_CX, TG_CY, 0), TG_CENTRE_BYTE), ctr) 364 gv_check("a-texel-inside-the-rim-carries-the-hand-computed-falloff-byte" as *u8, tg_eq(tp_get(paint, 0, TG_CX + TG_RIM_IN_DX, TG_CY, 0), TG_RIM_IN_BYTE), ctr) 365 gv_check("FIXTURE-REACHED-IT-the-texel-inside-the-rim-actually-changed" as *u8, tg_ne(tp_get(paint, 0, TG_CX + TG_RIM_IN_DX, TG_CY, 0), 0), ctr) 366 gv_check("a-texel-AT-the-rim-is-byte-identical-to-the-base" as *u8, tg_eq(tp_get(paint, 0, TG_CX + TG_RIM_OUT_DX, TG_CY, 0), 0), ctr) 367 gv_check("every-painted-channel-took-the-stroke" as *u8, tg_eq(tp_get(paint, 0, TG_CX, TG_CY, TP_CHAN - 1), TG_CENTRE_BYTE), ctr) 368 369 // painting ONE channel must leave the other two alone -- the "one channel or several" contract 370 let onech: *i64 = tg_black() 371 tp_stroke(onech, 0, TG_CH_G, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 372 gv_check("a-single-channel-mask-paints-that-channel" as *u8, tg_eq(tp_get(onech, 0, TG_CX, TG_CY, 1), TG_CENTRE_BYTE), ctr) 373 gv_check("a-single-channel-mask-leaves-the-other-channels-untouched" as *u8, tg_eq(tp_get(onech, 0, TG_CX, TG_CY, 0) + tp_get(onech, 0, TG_CX, TG_CY, 2), 0), ctr) 374 375 // a partial-strength stroke moves toward the target and NEVER past it, in both directions 376 let up: *i64 = tg_black() 377 tp_stroke(up, 0, TG_CH_R, TG_CX, TG_CY, TG_R, TG_HALFSTR, TG_VALUE) 378 let upv: i64 = tp_get(up, 0, TG_CX, TG_CY, 0) 379 tg_say(" half-strength onto black, centre byte = " as *u8, upv) 380 gv_check("partial-strength-moves-toward-the-target" as *u8, tg_gt(upv, 0), ctr) 381 gv_check("partial-strength-never-overshoots-the-target" as *u8, tg_le(upv, TG_VALUE), ctr) 382 let down: *i64 = tg_black() 383 tp_stroke(down, 0, TG_CH_R, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 384 tp_stroke(down, 0, TG_CH_R, TG_CX, TG_CY, TG_R, TG_HALFSTR, 0) 385 let dnv: i64 = tp_get(down, 0, TG_CX, TG_CY, 0) 386 tg_say(" half-strength back toward zero, centre byte = " as *u8, dnv) 387 gv_check("a-darkening-stroke-moves-down" as *u8, tg_gt(TG_VALUE, dnv), ctr) 388 gv_check("a-darkening-stroke-never-undershoots-its-target" as *u8, tg_ge(dnv, 0), ctr) 389 390 // ================= 3. THE SEAM: THE ANTI-TEAR PROOF ================= 391 tg_say(" derived bleed margin for the seam stroke = " as *u8, tp_bleed_margin(base, TG_SEAM_CX, TG_CY, TG_R)) 392 gv_check("derived-margin-is-the-radius-when-the-brush-fits-the-chart" as *u8, tg_eq(tp_bleed_margin(base, TG_SEAM_CX, TG_CY, TG_R), TG_R), ctr) 393 gv_check("derived-margin-clamps-to-half-the-chart-width-for-an-oversized-brush" as *u8, tg_eq(tp_bleed_margin(base, TG_SEAM_CX, TG_CY, TG_RES), (TG_RES / TG_GRID) / 2), ctr) 394 395 // (a) THE NAIVE PAINTER ALONE. This is the tear: the far side of the cut is untouched. 396 let torn: *i64 = tg_black() 397 let n_core: i64 = tp_stroke_core(torn, 0, TG_ALL_CH, TG_SEAM_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 398 let far_torn: i64 = tg_column_changed(torn, base, 0, TG_SEAM_FAR_X) 399 // (b) THE FULL STROKE. Same brush, same centre, same map -- the ONLY difference is the seam bleed. 400 let whole: *i64 = tg_black() 401 let n_full: i64 = tp_stroke(whole, 0, TG_ALL_CH, TG_SEAM_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 402 let far_whole: i64 = tg_column_changed(whole, base, 0, TG_SEAM_FAR_X) 403 // (c) the bleed on its own, so its count can be asserted against the hand-computed 5 404 let bleedonly: *i64 = tg_black() 405 let n_bleed: i64 = tp_seam_bleed(bleedonly, 0, TG_ALL_CH, TG_SEAM_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 406 tg_say(" seam stroke: planar half painted = " as *u8, n_core) 407 tg_say(" seam stroke: bleed half painted = " as *u8, n_bleed) 408 tg_say(" seam stroke: full stroke painted = " as *u8, n_full) 409 tg_say(" far side of the cut, naive painter, changed texels = " as *u8, far_torn) 410 tg_say(" far side of the cut, full stroke, changed texels = " as *u8, far_whole) 411 gv_check("seam-planar-half-is-the-hand-counted-20" as *u8, tg_eq(n_core, TG_SEAM_PLANAR), ctr) 412 gv_check("seam-bleed-half-is-the-hand-counted-5" as *u8, tg_eq(n_bleed, TG_SEAM_BLEED), ctr) 413 gv_check("the-two-halves-sum-to-the-full-stroke" as *u8, tg_eq(n_core + n_bleed, n_full), ctr) 414 // THE ANTI-TEAR CLAIM IN ONE NUMBER: a stroke that crosses the cut paints exactly as many texels as the 415 // same stroke in the chart interior -- so no texel was lost at the cut and none was written twice. 416 gv_check("a-seam-crossing-stroke-paints-the-SAME-AREA-as-an-interior-one" as *u8, tg_eq(n_full, TG_DISC_AREA), ctr) 417 gv_bite("neg-control-seam-TEARS-without-the-bleed" as *u8, tg_eq(far_torn, 0), tg_eq(far_whole, 0), ctr) 418 gv_check("FIXTURE-REACHED-IT-the-far-side-really-is-painted-by-the-full-stroke" as *u8, tg_eq(far_whole, TG_SEAM_BLEED), ctr) 419 gv_check("the-seam-stroke-writes-nothing-outside-the-cylindrical-disc" as *u8, tg_eq(tg_changed_outside(whole, base, 0, TG_SEAM_CX, TG_CY, TG_R), 0), ctr) 420 gv_check("the-seam-stroke-leaks-into-no-other-chart" as *u8, tg_eq(tg_column_changed(whole, base, 0, TG_RES / TG_GRID), 0), ctr) 421 // the bleed is a targeted repair of the cut, NOT a blanket dilation of everything painted 422 let interior_bleed: *i64 = tg_black() 423 let n_ib: i64 = tp_seam_bleed(interior_bleed, 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 424 gv_check("the-bleed-writes-NOTHING-for-a-stroke-that-does-not-reach-the-cut" as *u8, tg_eq(n_ib, 0), ctr) 425 gv_check("an-interior-stroke-needs-no-bleed-so-core-alone-equals-the-full-stroke" as *u8, tg_eq(tp_stroke_core(tg_black(), 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE), TG_DISC_AREA), ctr) 426 427 // ================= 4. THE OPERATION STACK: REPLAY, UNDO, RE-EVALUATION ================= 428 let sbase: *i64 = tg_patterned() 429 gv_check("session-base-is-not-uniform-so-a-dropped-map-cannot-pass" as *u8, tg_ne(tp_get(sbase, 0, 0, 0, 0), tp_get(sbase, TG_NMAPS - 1, TG_RES - 1, TG_RES - 1, TP_CHAN - 1)), ctr) 430 let direct: *i64 = tg_patterned() 431 tg_apply_session(direct, TG_VALUE) 432 let st: *i64 = tp_session_new(TG_CAP) 433 gv_check("session-allocated" as *u8, tg_ne(st as i64, 0), ctr) 434 tg_record_session(st, sbase, TG_VALUE) 435 gv_check("the-log-holds-exactly-the-strokes-that-were-recorded" as *u8, tg_eq(tp_strokes(st), TG_NSTROKES), ctr) 436 let r1: *i64 = tg_patterned() 437 let nap: i64 = tp_render(st, sbase, r1) 438 tg_say(" strokes replayed = " as *u8, nap) 439 gv_check("render-applies-every-recorded-stroke" as *u8, tg_eq(nap, TG_NSTROKES), ctr) 440 gv_check("the-recorded-log-reproduces-the-DIRECTLY-painted-atlas-byte-for-byte" as *u8, tg_eq(tp_maps_equal(r1, direct), 1), ctr) 441 gv_check("FIXTURE-REACHED-IT-the-session-actually-changed-the-base" as *u8, tg_eq(tp_maps_equal(r1, sbase), 0), ctr) 442 let r2: *i64 = tg_patterned() 443 tp_render(st, sbase, r2) 444 gv_check("REPLAY-IS-BIT-EXACT-two-renders-of-one-log-are-byte-identical" as *u8, tg_eq(tp_maps_equal(r1, r2), 1), ctr) 445 let d1: i64 = tp_maps_digest(r1) 446 gv_check("the-map-digest-agrees-with-itself-across-renders" as *u8, tg_eq(d1, tp_maps_digest(r2)), ctr) 447 448 // undo: the state must be an EARLIER one, not merely a different one 449 es_undo(st) 450 let r3: *i64 = tg_patterned() 451 tp_render(st, sbase, r3) 452 let d3: i64 = tp_maps_digest(r3) 453 gv_check("undo-drops-exactly-one-stroke-from-the-head" as *u8, tg_eq(tp_strokes(st), TG_NSTROKES - 1), ctr) 454 gv_check("undo-preserves-the-redo-tail-in-the-count" as *u8, tg_eq(es_count(st), TG_NSTROKES), ctr) 455 gv_check("ANTI-VACUITY-the-digest-DISCRIMINATES-the-undone-state" as *u8, tg_ne(d3, d1), ctr) 456 gv_check("undo-does-not-collapse-to-the-base" as *u8, tg_eq(tp_maps_equal(r3, sbase), 0), ctr) 457 es_redo(st) 458 let r4: *i64 = tg_patterned() 459 tp_render(st, sbase, r4) 460 gv_check("REDO-RESTORES-THE-STATE-BYTE-FOR-BYTE" as *u8, tg_eq(tp_maps_equal(r4, r1), 1), ctr) 461 es_undo(st) 462 es_undo(st) 463 es_undo(st) 464 let r5: *i64 = tg_patterned() 465 tp_render(st, sbase, r5) 466 gv_check("UNDOING-EVERY-STROKE-RESTORES-THE-BASE-BYTE-FOR-BYTE" as *u8, tg_eq(tp_maps_equal(r5, sbase), 1), ctr) 467 gv_check("a-fully-undone-session-still-holds-its-redo-tail" as *u8, tg_eq(es_count(st), TG_NSTROKES), ctr) 468 es_redo(st) 469 es_redo(st) 470 es_redo(st) 471 let r6: *i64 = tg_patterned() 472 tp_render(st, sbase, r6) 473 gv_check("REDOING-EVERY-STROKE-RETURNS-TO-THE-EXACT-SAME-BYTES" as *u8, tg_eq(tp_maps_equal(r6, r1), 1), ctr) 474 475 // re-evaluation at depth: change stroke 0 and the LATER strokes must survive on top of the new value. 476 // Proven not merely to differ, but to EQUAL an independently built log carrying the new parameter -- 477 // the tooth that separates a correct re-evaluation from a corrupted state. 478 tp_reeval(st, sbase, 0, 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_REEVAL_VALUE) 479 let ra: *i64 = tg_patterned() 480 tp_render(st, sbase, ra) 481 let st2: *i64 = tp_session_new(TG_CAP) 482 tg_record_session(st2, sbase, TG_REEVAL_VALUE) 483 let rb: *i64 = tg_patterned() 484 tp_render(st2, sbase, rb) 485 gv_check("reeval-equals-an-INDEPENDENTLY-BUILT-log-carrying-the-new-parameter" as *u8, tg_eq(tp_maps_equal(ra, rb), 1), ctr) 486 gv_check("FIXTURE-REACHED-IT-the-reevaluated-state-really-differs-from-the-original" as *u8, tg_eq(tp_maps_equal(ra, r1), 0), ctr) 487 gv_check("reeval-preserves-the-stroke-count" as *u8, tg_eq(tp_strokes(st), TG_NSTROKES), ctr) 488 489 // ================= REFUSALS: every guard is a negative control ================= 490 // Each fires on its own planted defect and is SILENT on the good input, and the good call is the same 491 // stroke in every case, so a kill attributes to the planted argument and to nothing else. 492 let good_rc: i64 = tp_stroke(tg_black(), 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 493 gv_check("the-shared-good-stroke-of-every-negative-control-is-accepted" as *u8, tg_ge(good_rc, 0), ctr) 494 495 let bad_bary: i64 = tp_surface_to_texel(uv3, TG_UVSTRIDE_TEXC, tris, 0, TG_NTRI, TG_NVERT, 0 - 100, 600, TG_B2, TG_RES, o2) 496 gv_bite("neg-control-barycentric-outside-the-triangle" as *u8, tg_eq(bad_bary, TP_E_BARY), tg_eq(src, TP_E_BARY), ctr) 497 let bad_sum: i64 = tp_surface_to_texel(uv3, TG_UVSTRIDE_TEXC, tris, 0, TG_NTRI, TG_NVERT, 100, TG_B1, TG_B2, TG_RES, o2) 498 gv_bite("neg-control-barycentric-does-not-sum-to-unity" as *u8, tg_eq(bad_sum, TP_E_BARYSUM), tg_eq(src, TP_E_BARYSUM), ctr) 499 gv_check("the-two-barycentric-failures-are-NOT-the-same-refusal" as *u8, tg_ne(bad_bary, bad_sum), ctr) 500 let bad_tri: i64 = tp_surface_to_texel(uv3, TG_UVSTRIDE_TEXC, tris, TG_NTRI, TG_NTRI, TG_NVERT, TG_B0, TG_B1, TG_B2, TG_RES, o2) 501 gv_bite("neg-control-triangle-index-out-of-range" as *u8, tg_eq(bad_tri, TP_E_TRI), tg_eq(src, TP_E_TRI), ctr) 502 let bad_uv: i64 = tp_uv_to_texel(TP_UV_SPAN, 0, TG_RES, o2) 503 gv_bite("neg-control-uv-outside-the-q16-codomain" as *u8, tg_eq(bad_uv, TP_E_TEXEL), tg_eq(trc, TP_E_TEXEL), ctr) 504 505 let bad_texel: i64 = tp_stroke(tg_black(), 0, TG_ALL_CH, TG_RES, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 506 gv_bite("neg-control-texel-address-outside-the-atlas" as *u8, tg_eq(bad_texel, TP_E_TEXEL), tg_eq(good_rc, TP_E_TEXEL), ctr) 507 let bad_r: i64 = tp_stroke(tg_black(), 0, TG_ALL_CH, TG_CX, TG_CY, 0, TG_STRENGTH, TG_VALUE) 508 gv_bite("neg-control-zero-radius" as *u8, tg_eq(bad_r, TP_E_RADIUS), tg_eq(good_rc, TP_E_RADIUS), ctr) 509 let bad_s: i64 = tp_stroke(tg_black(), 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TP_STRENGTH_MAX + 1, TG_VALUE) 510 gv_bite("neg-control-strength-above-its-declared-range" as *u8, tg_eq(bad_s, TP_E_STRENGTH), tg_eq(good_rc, TP_E_STRENGTH), ctr) 511 let bad_s2: i64 = tp_stroke(tg_black(), 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, 0 - 1, TG_VALUE) 512 gv_bite("neg-control-strength-below-its-declared-range" as *u8, tg_eq(bad_s2, TP_E_STRENGTH), tg_eq(good_rc, TP_E_STRENGTH), ctr) 513 let bad_v: i64 = tp_stroke(tg_black(), 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TP_VALUE_MAX + 1) 514 gv_bite("neg-control-value-outside-the-channel-range" as *u8, tg_eq(bad_v, TP_E_VALUE), tg_eq(good_rc, TP_E_VALUE), ctr) 515 let bad_m: i64 = tp_stroke(tg_black(), TG_NMAPS, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 516 gv_bite("neg-control-map-id-outside-the-map-set" as *u8, tg_eq(bad_m, TP_E_MAP), tg_eq(good_rc, TP_E_MAP), ctr) 517 let bad_ch: i64 = tp_stroke(tg_black(), 0, 0, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE) 518 gv_bite("neg-control-empty-channel-mask-selects-nothing" as *u8, tg_eq(bad_ch, TP_E_CHMASK), tg_eq(good_rc, TP_E_CHMASK), ctr) 519 let bad_rc2: i64 = tp_stroke(tg_black(), 0, TG_ALL_CH, TG_CX, TG_CY, TG_RES, TG_STRENGTH, TG_VALUE) 520 gv_bite("neg-control-brush-wider-than-the-derived-seam-margin" as *u8, tg_eq(bad_rc2, TP_E_RADIUS_CHART), tg_eq(good_rc, TP_E_RADIUS_CHART), ctr) 521 let odd: *i64 = tp_maps_new(TG_RES, TG_NMAPS, TG_GRID_ODD) 522 let bad_chart: i64 = tp_stroke(odd, 0, TG_ALL_CH, TG_RES - 1, 0, 1, TG_STRENGTH, TG_VALUE) 523 let ok_chart: i64 = tp_stroke(odd, 0, TG_ALL_CH, 0, 0, 1, TG_STRENGTH, TG_VALUE) 524 gv_bite("neg-control-centre-texel-belongs-to-no-chart" as *u8, tg_eq(bad_chart, TP_E_CHART), tg_eq(ok_chart, TP_E_CHART), ctr) 525 let bad_geom: i64 = tp_geom_ok(TP_UV_SPAN + 1, TG_NMAPS, TG_GRID) 526 gv_bite("neg-control-resolution-beyond-q16-addressability" as *u8, tg_eq(bad_geom, TP_E_RES), tg_eq(tp_geom_ok(TG_RES, TG_NMAPS, TG_GRID), TP_E_RES), ctr) 527 528 // A REFUSED STROKE MUST LEAVE THE MAP SET EXACTLY AS IT FOUND IT -- otherwise "refused" is only half a 529 // guarantee and a caller cannot retry safely. 530 let untouched: *i64 = tg_patterned() 531 let ref_before: i64 = tp_maps_digest(untouched) 532 tp_stroke(untouched, 0, TG_ALL_CH, TG_CX, TG_CY, 0, TG_STRENGTH, TG_VALUE) 533 gv_check("a-refused-stroke-leaves-the-map-set-byte-identical" as *u8, tg_eq(tp_maps_digest(untouched), ref_before), ctr) 534 // ... and never enters the log, or a replay would refuse halfway and leave a half-rendered atlas 535 let st3: *i64 = tp_session_new(TG_CAP) 536 let rec_bad: i64 = tp_record(st3, base, 0, TG_ALL_CH, TG_CX, TG_CY, 0, TG_STRENGTH, TG_VALUE) 537 gv_bite("neg-control-a-refused-stroke-never-enters-the-log" as *u8, tg_eq(rec_bad, TP_E_RADIUS), tg_eq(tp_record(st3, base, 0, TG_ALL_CH, TG_CX, TG_CY, TG_R, TG_STRENGTH, TG_VALUE), TP_E_RADIUS), ctr) 538 gv_check("the-log-grew-by-exactly-the-one-accepted-stroke" as *u8, tg_eq(tp_strokes(st3), 1), ctr) 539 540 // ---- the instrument itself ---- 541 gv_check("every-refusal-class-carries-a-distinct-name" as *u8, tg_ne(tp_err_name(TP_E_BARY) as i64, tp_err_name(TP_E_BARYSUM) as i64), ctr) 542 gv_check("an-unclassified-code-does-not-borrow-a-real-name" as *u8, tg_ne(tp_err_name(0 - 999) as i64, tp_err_name(TP_E_ARGS) as i64), ctr) 543 gv_check("map-sets-of-different-geometry-refuse-to-be-compared" as *u8, tg_eq(tp_maps_equal(base, odd), 0), ctr) 544 gv_check("a-copy-across-different-geometry-is-refused-by-name" as *u8, tg_eq(tp_maps_copy(base, odd), TP_E_GEOM), ctr) 545 gv_check("the-digest-of-two-different-atlases-differs" as *u8, tg_ne(tp_maps_digest(paint), tp_maps_digest(base)), ctr) 546 547 return gv_verdict("nx_texpaint_gate" as *u8, ctr, "the rivals let a person notice a torn seam and paint over it; this one makes the cut a property of the metric, so a stroke that crosses it is the same stroke it would have been in the chart interior, provably to the texel" as *u8) 548}