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}