code wiki / _hdl_build / nx_craft_erosion_shot.nx
nx_craft_erosion_shot.nx source
↩ module page · 419 lines · 22014 B
1// nx_craft_erosion_shot.nx -- THE BEFORE/AFTER PICTURE OF EROSION, TAKEN FROM THE WASM A VISITOR LOADS.
2//
3// WHY THIS EXISTS (operator standing rule, 2026-08-25): "if it's not publishing on a live page then
4// it's not truly deployed, as I can't validate." Talus erosion ships in /craft and is proven by
5// NUMBERS -- nx_wasm_craft_vm_gate measures talus 154/256, 509 of 576 lattice heights moved, rugosity
6// 88 -> 75 permil -- and not one of those is a PICTURE. A number the operator cannot see is a claim he
7// has to take on trust, which is the one thing this estate exists to stop doing.
8//
9// * WHY NOT nx_world_snap, THE INCUMBENT CAPTURE INSTRUMENT. Two reasons, and either alone is
10// decisive. (1) IT RENDERS NATIVELY: it imports nx_wasm_craft as a LIB and calls init_impl_v /
11// render_impl in this process's own memory. That is precisely the "a gate that tests a different
12// build than the one that ships is not testing the product" defect nx_wasm_craft_vm_gate exists to
13// correct -- /craft shipped BLACK TWICE while the native gate read 59/59 GREEN both times, because
14// the native build never meets the wasm memory bound at all. A before/after picture drawn by the
15// native twin would prove the twin erodes, not the product. (2) IT CANNOT REACH THE COLUMN ANYWAY:
16// its style poke is bounded 7..58 and its gen-row allowlist is 0..6, 59, 60, 61 -- so spec idx 64,
17// THE EROSION BUDGET, is unreachable from nx_world_snap in BOTH of its lanes. Measured, not assumed.
18//
19// So this organ drives the emitted .wasm through the estate's own nx_wasm_vm, in a linear memory
20// sized to exactly what the module declares -- the same subject, the same bound and the same VM the
21// vm-gate uses. It opts in through the GENP block byte-for-byte the way craft.html's NXGP loop does,
22// so the erosion it photographs arrives by the SHIPPED door and not through a private back channel.
23// The PNG encoder is COMPOSED, never rewritten: nx_png_write_rgb is the estate's one encoder.
24//
25// usage: nx_craft_erosion_shot <outbase> <variant> <seed> [q] [budgetA] [budgetB] [flat]
26// writes <outbase>_off.png (budgetA, default 0) and <outbase>_on.png (budgetB, default 1),
27// same variant + same seed + same camera; the erosion budget is the only difference.
28// q omitted or 0 -> DERIVED (see es_q_mem / es_q_rays). q given -> explicit, still refused
29// if it breaks the module's declared memory.
30//
31// flat=1 -> ALSO zero P_TAMP1 and P_TAMP2 through the same GENP door, i.e. ask for a world
32// with NO RELIEF. THIS IS THE CONTROL THAT MATTERS, and it was earned the hard way: the
33// obvious control (render /beach, which never opts in) was run first and CAME BACK 680 PERMIL
34// DIFFERENT -- so it proved nothing about the pipeline and could not be published. beach's
35// sand is NEAR-flat (P_TAMP1=2, P_TAMP2=1), not flat, and on a two-block relief a one-block
36// shed moves most columns. A flat world has no slope steeper than its own average at all, so
37// the measured talus is zero and the operator turns itself off. Because budgetA and budgetB
38// then describe the SAME world, one such run proves BOTH that the capture is deterministic
39// AND that erosion does nothing without slopes: an image pipeline that painted noise, drifted
40// with wall-clock time, or added an offset would fail it.
41// exit: 0 both frames written | 2 usage | 4 wasm unreadable/unparsable | 5 png write failed
42// | 6 REFUSED: the requested frame does not fit the module's declared linear memory
43// No hw writes (Rule 26). license_tier: ORIGINAL
44import "nx_syscalls.nx"
45import "nx_wasm_vm.nx"
46import "nx_wasm_craft.nx"
47import "nx_png_write.nx"
48import "nx_lineconf_lib.nx"
49
50const ES_WASM: *u8 = "buildroot/_build/nx_wasm_craft_emit.wasm"
51const ES_CONF: *u8 = "knowledge/craft_erosion_shot.conf"
52const ES_KEY_RAYS: *u8 = "shot-ray-budget"
53const ES_W64: i64 = 8 // bytes in an i64 -- every plane below is made of these
54const ES_RGB: i64 = 3 // bytes per pixel handed to nx_png_write_rgb
55const ES_BYTE: i64 = 256 // one colour channel's modulus
56const ES_PATHCAP: i64 = 512
57const ES_COLCAP: i64 = 4096 // distinct-colour census ceiling. A frame that reaches it is
58 // varied by any standard, but the number is then a FLOOR and
59 // not a value -- it is printed so a reader can see that.
60const ES_MS_PER_S: i64 = 1000
61const ES_NS_PER_MS: i64 = 1000000
62const ES_PERMIL: i64 = 1000
63const ES_SCRATCH: i64 = 32
64const ES_BOX: i64 = 16
65// GENP block layout -- the SAME slots craft.html's NXGP loop writes (GP[0]=magic, GP[1]=count, then
66// (idx,val) pairs). Named as indices so this wire cannot drift from the page's.
67const ES_GP_MAGIC: i64 = 0
68const ES_GP_N: i64 = 1
69const ES_GP_IDX0: i64 = 2
70const ES_GP_VAL0: i64 = 3
71const ES_GP_STRIDE: i64 = 2 // words per (idx,val) pair
72const ES_GENP_N: i64 = 1 // the erosion pair (P_ERODE, budget) is always written
73const ES_GENP_FLAT: i64 = 3 // ...plus P_TAMP1=0 and P_TAMP2=0 when a FLAT world is asked for
74// out-box slots
75const ES_O_RW: i64 = 0
76const ES_O_RH: i64 = 1
77const ES_O_INITMS: i64 = 2
78const ES_O_RENDMS: i64 = 3
79const ES_O_FBOFF: i64 = 4
80const ES_O_DISTINCT: i64 = 5
81const ES_O_GENP: i64 = 6 // S_GENP: how many overrides the ENGINE says it applied. World
82 // truth, not intent -- a silently dropped opt-in would
83 // otherwise be indistinguishable from a world that erodes to
84 // nothing, and those two need opposite investigations.
85// THE CAMERA THIS SHOT ACTUALLY STOOD AT, recorded BEFORE any pinning, so the pair can report how
86// far erosion moved the spawn ground -- see the pinning note on es_shoot.
87const ES_O_CX: i64 = 7
88const ES_O_CY: i64 = 8
89const ES_O_CZ: i64 = 9
90const ES_O_YAW: i64 = 10
91const ES_O_PITCH: i64 = 11
92const ES_OBOX: i64 = 16
93const ES_EXIT_USAGE: i64 = 2
94const ES_EXIT_WASM: i64 = 4
95const ES_EXIT_PNG: i64 = 5
96const ES_EXIT_NOFIT: i64 = 6
97
98func es_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
99func es_w(s: *u8) -> i64 { sys_write(1, s, es_slen(s)); return 0 }
100func es_n(v: i64) -> i64 {
101 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
102 var m: i64 = v
103 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
104 let t: *u8 = sys_mmap(ES_SCRATCH)
105 var k: i64 = 0
106 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
107 let o: *u8 = sys_mmap(ES_SCRATCH)
108 var i: i64 = 0
109 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
110 sys_write(1, o, k)
111 sys_munmap(t, ES_SCRATCH)
112 sys_munmap(o, ES_SCRATCH)
113 return 0
114}
115func es_int(s: *u8) -> i64 {
116 var v: i64 = 0
117 var i: i64 = 0
118 while s[i] != (0 as u8) {
119 let c: i64 = s[i] as i64
120 if c >= 48 { if c <= 57 { v = v*10 + (c - 48) } }
121 i = i + 1
122 }
123 return v
124}
125func es_ms(ts: *i64) -> i64 {
126 sys_clock_gettime_mono(ts)
127 return ts[0]*ES_MS_PER_S + ts[1]/ES_NS_PER_MS
128}
129// <outbase><suffix> into dst; returns dst so it reads as an expression at the call site
130func es_path(dst: *u8, base: *u8, sfx: *u8) -> *u8 {
131 var i: i64 = 0
132 while base[i] != (0 as u8) { dst[i] = base[i]; i = i + 1 }
133 var j: i64 = 0
134 while sfx[j] != (0 as u8) { dst[i+j] = sfx[j]; j = j + 1 }
135 dst[i+j] = 0 as u8
136 return dst
137}
138
139// ===== THE RESOLUTION IS DERIVED, FROM TWO BOUNDS, AND THE COARSER ONE WINS ==================
140// (a) MEMORY. The module declares WASM_MEM_BYTES and its framebuffer TAILS the arena, so a frame is
141// admissible only while fb_off + (W/q)*(H/q)*8 <= WASM_MEM_BYTES. This is not a style rule: the
142// vm-gate's FITS tooth exists because a frame that outgrew this actually shipped, and a browser
143// traps with "index out of bounds" the instant it does not fit -- painting NOTHING at all.
144// (b) WORK. This is an INTERPRETER, not a browser JIT. Rays are the unit of work and the ceiling is
145// a conf row whose value came from THIS ORGAN'S OWN PUBLISHED RUNS (it prints rays and ms on
146// every scenario, so the row is re-derivable from any log line and is never a taste constant).
147// Both bounds are printed with the answer and the binding one is NAMED, so a later reader can see
148// which constraint actually chose the picture's size. An ABSENT conf row leaves the work bound
149// UNSTATED rather than silently defaulted -- it is announced as ABSENT and memory decides alone.
150func es_q_mem(fboff: i64) -> i64 {
151 var q: i64 = 1
152 while q <= Q_MAX {
153 if fboff + (W/q)*(H/q)*ES_W64 <= WASM_MEM_BYTES { return q }
154 q = q + 1
155 }
156 return 0 - 1
157}
158func es_q_rays(budget: i64) -> i64 {
159 var q: i64 = 1
160 while q <= Q_MAX {
161 if (W/q)*(H/q) <= budget { return q }
162 q = q + 1
163 }
164 return 0 - 1
165}
166
167// write pair k of the GENP block
168func es_genp(mem: *u8, k: i64, idx: i64, val: i64) -> i64 {
169 wm_st64(mem, O_GENP + (ES_GP_IDX0 + k*ES_GP_STRIDE)*ES_W64, idx)
170 wm_st64(mem, O_GENP + (ES_GP_VAL0 + k*ES_GP_STRIDE)*ES_W64, val)
171 return 0
172}
173
174// ONE SCENARIO: fresh memory, opt in (or do not), generate the world, render it, lift the frame.
175// * WHY THE SECOND SHOT BORROWS THE FIRST SHOT'S CAMERA (pin), MEASURED NOT ASSUMED. init places the
176// player on the ground it just generated: s[S_CY] = (sh + 1)*256 + EYE, where sh is the SPAWN
177// COLUMN'S ground height. Erosion changes that column like any other, so an un-pinned pair is shot
178// from two DIFFERENT eye heights -- and then almost every pixel differs for a reason that is not the
179// shape of the land. That is not a before/after, it is two photographs of one place from two
180// tripods, and the pixel-difference number it produces silently overstates the terrain change.
181// (Measured on the first, un-pinned publish: craft read 702 permil and near-flat BEACH read 680 --
182// two worlds of completely different relief moving by almost the same amount, which is the
183// fingerprint of a global shift rather than of local weathering.)
184// Passing the first shot's camera into the second makes the viewpoint a CONSTANT, so every
185// remaining differing pixel is the ground itself. The camera each shot would NATURALLY have used is
186// still recorded first, so the displacement erosion caused at the spawn column is reported rather
187// than hidden. pin = 0 means "use your own camera".
188func es_shoot(mod: *WasmMod, v: i64, seed: i64, erode: i64, flat: i64, q: i64, rgb: *u8, ob: *i64, ts: *i64, pin: *i64) -> i64 {
189 // FRESH LINEAR MEMORY PER SCENARIO. mmap hands back zeroed pages and a zero GENP region is
190 // "no overrides" BY CONSTRUCTION, so nothing from the previous world can survive into this one
191 // -- the two frames are two cold starts, not one world poked twice.
192 mod.mem = sys_mmap(WASM_MEM_BYTES)
193 mod.mem_bytes = WASM_MEM_BYTES
194 // THE SHIPPED OPT-IN DOOR, BYTE FOR BYTE. craft.html writes magic, count, then (idx,val) pairs
195 // into genp_off() and calls init. init applies them AFTER the identity defaults and BEFORE
196 // genworld reads the heightfield, and its allowlist admits idx 64 for exactly this reason.
197 wm_st64(mod.mem, O_GENP + ES_GP_MAGIC*ES_W64, GENP_MAGIC)
198 var npairs: i64 = ES_GENP_N
199 es_genp(mod.mem, 0, P_ERODE, erode)
200 if flat == 1 {
201 // THE FLAT CONTROL, BUILT THROUGH THE SAME SHIPPED DOOR. P_TAMP1 and P_TAMP2 are gen-time
202 // terrain-shape rows (spec idx 1 and 2, both inside init's own 0..6 allowlist), so zeroing
203 // them here is the page's NXGP mechanism asking for a world with no relief -- not a private
204 // back channel, and not a different code path from the subject.
205 es_genp(mod.mem, 1, P_TAMP1, 0)
206 es_genp(mod.mem, 2, P_TAMP2, 0)
207 npairs = ES_GENP_FLAT
208 }
209 wm_st64(mod.mem, O_GENP + ES_GP_N*ES_W64, npairs)
210 let t0: i64 = es_ms(ts)
211 // init_v(0, seed) IS the shipped init(): init_impl(base,seed) is literally
212 // init_impl_v(base,0,seed), and craft.html takes the init() branch because its NXV is 0.
213 wm_run(mod, "init_v" as *u8, v, seed, 0, 0, 0, 2)
214 let t1: i64 = es_ms(ts)
215 // AFTER init, never before: init zeroes all 64 state slots AND sets S_Q = Q_START at its end, so
216 // a quality written earlier is wiped twice over (the zero-loop-order class the engine's own
217 // header names twice).
218 wm_st64(mod.mem, O_ST + S_Q*ES_W64, q)
219 // record the camera THIS world would naturally have used, before any pinning overwrites it
220 ob[ES_O_CX] = wm_ld64(mod.mem, O_ST + S_CX*ES_W64)
221 ob[ES_O_CY] = wm_ld64(mod.mem, O_ST + S_CY*ES_W64)
222 ob[ES_O_CZ] = wm_ld64(mod.mem, O_ST + S_CZ*ES_W64)
223 ob[ES_O_YAW] = wm_ld64(mod.mem, O_ST + S_YAW*ES_W64)
224 ob[ES_O_PITCH] = wm_ld64(mod.mem, O_ST + S_PITCH*ES_W64)
225 if (pin as i64) != 0 {
226 wm_st64(mod.mem, O_ST + S_CX*ES_W64, pin[ES_O_CX])
227 wm_st64(mod.mem, O_ST + S_CY*ES_W64, pin[ES_O_CY])
228 wm_st64(mod.mem, O_ST + S_CZ*ES_W64, pin[ES_O_CZ])
229 wm_st64(mod.mem, O_ST + S_YAW*ES_W64, pin[ES_O_YAW])
230 wm_st64(mod.mem, O_ST + S_PITCH*ES_W64, pin[ES_O_PITCH])
231 }
232 wm_run(mod, "render" as *u8, 0, 0, 0, 0, 0, 0)
233 let t2: i64 = es_ms(ts)
234 // ONE OWNER OF THE WRITTEN EXTENT: ww()/hh() are what the renderer actually wrote, one pixel
235 // per ray. A consumer that assumed W*H would read unwritten memory and call it a measurement.
236 let rw: i64 = wm_run(mod, "ww" as *u8, 0, 0, 0, 0, 0, 0)
237 let rh: i64 = wm_run(mod, "hh" as *u8, 0, 0, 0, 0, 0, 0)
238 let off: i64 = wm_run(mod, "fb_off" as *u8, 0, 0, 0, 0, 0, 0)
239 let seen: *i64 = sys_mmap(ES_COLCAP*ES_W64) as *i64
240 var distinct: i64 = 0
241 var p: i64 = 0
242 while p < rw*rh {
243 let px: i64 = wm_ld64(mod.mem, off + p*ES_W64)
244 // channel order is the PAGE'S: it blits m[i*2]|0xff000000 straight into RGBA ImageData,
245 // so the low byte is RED. Same extraction nx_world_snap uses -- one convention, not two.
246 rgb[p*ES_RGB] = (px % ES_BYTE) as u8
247 rgb[p*ES_RGB + 1] = ((px/ES_BYTE) % ES_BYTE) as u8
248 rgb[p*ES_RGB + 2] = ((px/(ES_BYTE*ES_BYTE)) % ES_BYTE) as u8
249 var j: i64 = 0
250 var dup: i64 = 0
251 while j < distinct { if seen[j] == px { dup = 1; j = distinct } else { j = j + 1 } }
252 if dup == 0 { if distinct < ES_COLCAP { seen[distinct] = px; distinct = distinct + 1 } }
253 p = p + 1
254 }
255 sys_munmap(seen as *u8, ES_COLCAP*ES_W64)
256 ob[ES_O_RW] = rw
257 ob[ES_O_RH] = rh
258 ob[ES_O_INITMS] = t1 - t0
259 ob[ES_O_RENDMS] = t2 - t1
260 ob[ES_O_FBOFF] = off
261 ob[ES_O_DISTINCT] = distinct
262 ob[ES_O_GENP] = wm_ld64(mod.mem, O_ST + S_GENP*ES_W64)
263 sys_munmap(mod.mem, WASM_MEM_BYTES)
264 return rw*rh
265}
266
267func es_report(tag: *u8, ob: *i64) -> i64 {
268 es_w("SHOT " as *u8); es_w(tag)
269 es_w(" " as *u8); es_n(ob[ES_O_RW]); es_w("x" as *u8); es_n(ob[ES_O_RH])
270 es_w(" rays=" as *u8); es_n(ob[ES_O_RW]*ob[ES_O_RH])
271 es_w(" init_ms=" as *u8); es_n(ob[ES_O_INITMS])
272 es_w(" render_ms=" as *u8); es_n(ob[ES_O_RENDMS])
273 es_w(" distinct=" as *u8); es_n(ob[ES_O_DISTINCT])
274 if ob[ES_O_DISTINCT] >= ES_COLCAP { es_w("(AT-CENSUS-CAP: a FLOOR, not a value)" as *u8) }
275 es_w(" genp_applied=" as *u8); es_n(ob[ES_O_GENP]); es_w("\n" as *u8)
276 return 0
277}
278
279func main(argc: i64, argv: *i64) -> i64 {
280 if argc < 4 {
281 es_w("usage: nx_craft_erosion_shot <outbase> <variant> <seed> [q] [budgetA] [budgetB] [flat]\n" as *u8)
282 es_w(" writes <outbase>_off.png (budgetA) and <outbase>_on.png (budgetB) from the SHIPPED\n" as *u8)
283 es_w(" wasm, same variant+seed, opted in through the page's own GENP door.\n" as *u8)
284 es_w(" flat=1 also zeroes P_TAMP1/P_TAMP2 -> a world with NO RELIEF, whose two frames must\n" as *u8)
285 es_w(" come out IDENTICAL. That single run is both the determinism control and the proof\n" as *u8)
286 es_w(" that the erosion operator does nothing where there is no slope to shed.\n" as *u8)
287 return ES_EXIT_USAGE
288 }
289 let outbase: *u8 = argv[1] as *u8
290 let v: i64 = es_int(argv[2] as *u8)
291 let seed: i64 = es_int(argv[3] as *u8)
292 var qarg: i64 = 0
293 if argc > 4 { qarg = es_int(argv[4] as *u8) }
294 var bud_a: i64 = 0
295 var bud_b: i64 = 1
296 var flat: i64 = 0
297 if argc > 5 { bud_a = es_int(argv[5] as *u8) }
298 if argc > 6 { bud_b = es_int(argv[6] as *u8) }
299 if argc > 7 { flat = es_int(argv[7] as *u8) }
300 es_w("CONFIG variant=" as *u8); es_n(v)
301 es_w(" seed=" as *u8); es_n(seed)
302 es_w(" budgetA=" as *u8); es_n(bud_a)
303 es_w(" budgetB=" as *u8); es_n(bud_b)
304 es_w(" flat=" as *u8); es_n(flat); es_w("\n" as *u8)
305
306 let ts: *i64 = sys_mmap(ES_SCRATCH) as *i64
307 let box: *i64 = sys_mmap(ES_BOX) as *i64
308 let wasm: *u8 = sys_read_file(ES_WASM, box)
309 if (wasm as i64) == 0 {
310 es_w("CRAFT-EROSION-SHOT REFUSED: cannot read " as *u8); es_w(ES_WASM)
311 es_w(" -- run nx_craft_emit first\n" as *u8)
312 return ES_EXIT_WASM
313 }
314 let mod: *WasmMod = wm_new(wasm, box[0])
315 if wm_parse(mod) != 0 {
316 es_w("CRAFT-EROSION-SHOT REFUSED: the emitted wasm does not parse\n" as *u8)
317 return ES_EXIT_WASM
318 }
319 // fb_off is a trivial accessor -- callable before any world exists, which is what lets the
320 // memory bound be decided BEFORE paying for worldgen.
321 mod.mem = sys_mmap(WASM_MEM_BYTES)
322 mod.mem_bytes = WASM_MEM_BYTES
323 let fboff: i64 = wm_run(mod, "fb_off" as *u8, 0, 0, 0, 0, 0, 0)
324 sys_munmap(mod.mem, WASM_MEM_BYTES)
325
326 let qmem: i64 = es_q_mem(fboff)
327 let budget: i64 = lcf_int_of(ES_CONF, ES_KEY_RAYS)
328 var qray: i64 = 0 - 1
329 if budget != LCF_MISS { qray = es_q_rays(budget) }
330 var q: i64 = qarg
331 var bound: *u8 = "explicit-argument" as *u8
332 if qarg <= 0 {
333 q = qmem
334 bound = "declared-memory" as *u8
335 if qray > q { q = qray; bound = "ray-budget-conf" as *u8 }
336 }
337 es_w("Q-DERIVE q_mem=" as *u8); es_n(qmem)
338 es_w(" q_rays=" as *u8); es_n(qray)
339 es_w(" ray_budget=" as *u8)
340 if budget == LCF_MISS { es_w("ABSENT" as *u8) } else { es_n(budget) }
341 es_w(" -> q=" as *u8); es_n(q)
342 es_w(" bound=" as *u8); es_w(bound); es_w("\n" as *u8)
343 if q < 1 { q = 1 }
344 if q > Q_MAX { q = Q_MAX }
345
346 let rw: i64 = W/q
347 let rh: i64 = H/q
348 let need: i64 = fboff + rw*rh*ES_W64
349 es_w("FITS fb_off=" as *u8); es_n(fboff)
350 es_w(" frame=" as *u8); es_n(rw); es_w("x" as *u8); es_n(rh)
351 es_w(" needs=" as *u8); es_n(need)
352 es_w(" declared=" as *u8); es_n(WASM_MEM_BYTES)
353 es_w(" headroom=" as *u8); es_n(WASM_MEM_BYTES - need); es_w("\n" as *u8)
354 if need > WASM_MEM_BYTES {
355 // REFUSE rather than exceed. A frame past the declared memory is not a big picture, it is
356 // a trap, and a trapping module paints nothing at all.
357 es_w("CRAFT-EROSION-SHOT REFUSED: q=" as *u8); es_n(q)
358 es_w(" exceeds the module's declared linear memory by " as *u8); es_n(need - WASM_MEM_BYTES)
359 es_w(" bytes -- a browser traps here\n" as *u8)
360 return ES_EXIT_NOFIT
361 }
362
363 let np: i64 = rw*rh
364 let rgb0: *u8 = sys_mmap(np*ES_RGB + ES_BOX)
365 let rgb1: *u8 = sys_mmap(np*ES_RGB + ES_BOX)
366 let ob0: *i64 = sys_mmap(ES_OBOX*ES_W64) as *i64
367 let ob1: *i64 = sys_mmap(ES_OBOX*ES_W64) as *i64
368
369 es_shoot(mod, v, seed, bud_a, flat, q, rgb0, ob0, ts, 0 as *i64)
370 es_report("budgetA" as *u8, ob0)
371 // THE VIEWPOINT IS HELD CONSTANT ACROSS THE PAIR: shot B stands exactly where shot A stood.
372 es_shoot(mod, v, seed, bud_b, flat, q, rgb1, ob1, ts, ob0)
373 es_report("budgetB" as *u8, ob1)
374 // ...and the camera shot B would have used on its own is reported, never discarded: that delta
375 // IS how far erosion moved the ground under the player's feet at the spawn column.
376 es_w("CAMERA pinned=1 shotA cx=" as *u8); es_n(ob0[ES_O_CX])
377 es_w(" cy=" as *u8); es_n(ob0[ES_O_CY])
378 es_w(" cz=" as *u8); es_n(ob0[ES_O_CZ])
379 es_w(" | shotB natural cy=" as *u8); es_n(ob1[ES_O_CY])
380 es_w(" spawn_ground_delta_q8=" as *u8); es_n(ob1[ES_O_CY] - ob0[ES_O_CY])
381 es_w(" (256 = one block)\n" as *u8)
382
383 // THE PIXEL DIFFERENCE IS THE HEADLINE NUMBER, because "the images differ" is exactly what a
384 // reader wants proven and a hash pair cannot say HOW MUCH. Reported in permil of the frame too,
385 // so it stays comparable across resolutions.
386 var diff: i64 = 0
387 var i: i64 = 0
388 while i < np {
389 var d: i64 = 0
390 if rgb0[i*ES_RGB] != rgb1[i*ES_RGB] { d = 1 }
391 if rgb0[i*ES_RGB + 1] != rgb1[i*ES_RGB + 1] { d = 1 }
392 if rgb0[i*ES_RGB + 2] != rgb1[i*ES_RGB + 2] { d = 1 }
393 diff = diff + d
394 i = i + 1
395 }
396 var permil: i64 = 0
397 if np > 0 { permil = diff*ES_PERMIL/np }
398 es_w("PIXEL-DIFF differing=" as *u8); es_n(diff)
399 es_w(" of=" as *u8); es_n(np)
400 es_w(" permil=" as *u8); es_n(permil)
401 if flat == 1 {
402 // On a flat world the two budgets describe the SAME world, so this number is a CONTROL and
403 // its only acceptable value is zero. Said out loud in the artifact, because a control whose
404 // expected value is not written beside it gets read as just another measurement.
405 es_w(" [FLAT CONTROL -- expected 0; anything else means the capture is not deterministic" as *u8)
406 es_w(" or erosion acts without slopes]" as *u8)
407 }
408 es_w("\n" as *u8)
409
410 let p0: *u8 = es_path(sys_mmap(ES_PATHCAP), outbase, "_off.png" as *u8)
411 let p1: *u8 = es_path(sys_mmap(ES_PATHCAP), outbase, "_on.png" as *u8)
412 let w0: i64 = nx_png_write_rgb(p0, rgb0, ob0[ES_O_RW], ob0[ES_O_RH])
413 let w1: i64 = nx_png_write_rgb(p1, rgb1, ob1[ES_O_RW], ob1[ES_O_RH])
414 es_w("WROTE " as *u8); es_w(p0); es_w(" rc=" as *u8); es_n(w0)
415 es_w(" | " as *u8); es_w(p1); es_w(" rc=" as *u8); es_n(w1); es_w("\n" as *u8)
416 if w0 != 0 { return ES_EXIT_PNG }
417 if w1 != 0 { return ES_EXIT_PNG }
418 return 0
419}