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}