diff --git a/docs/TESTING.md b/docs/TESTING.md index 24dcdd0..e716497 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -79,17 +79,21 @@ checks waking within eight ticks separately from the probabilistic consumption o `shots/play/`. Pixel look by default; `python3 test/play.py shots/play smooth` uses the smooth look. - `test/t_smooth.py` checks WebGPU smooth shapes, seams, joins, fog and the thermal lens. -- `test/t_gpu_golden.py` compares 28 cropped legacy reference PNGs (384x288, under +- `test/t_gpu_golden.py` compares 28 cropped frozen reference PNGs (384x288, under `test/fixtures/golden/`) against WebGPU: renderer, joins, sky and four light scenes, both looks at zoom 12 and 2. ImageMagick decodes RGB; the per-channel tolerance is 8, mean RGB error must stay below 0.5/255 and fewer than 0.3% of pixels may exceed 8 - (smooth AO/edge rounding). These are frozen pre-removal legacy images, not generated - from the new renderer. The compressed `golden-world-640x*.json.gz` fixtures retain the - Phase 2 world bytes that produced them, isolating renderer checks from worldgen RNG changes. Inspect the matching `shots/golden-*.png` after visible changes. + (smooth AO/edge rounding). Seven smooth crops were refreshed for t91 (renderer 12/2, + joins 2, sky 12/2, lamp 12, magma 12): the old noise had seams in the references too. + The remaining crops are frozen pre-removal legacy images. The compressed + `golden-world-640x*.json.gz` fixtures retain the Phase 2 world bytes that produced them, + isolating renderer checks from worldgen RNG changes. Inspect the matching + `shots/golden-*.png` after visible changes. - `test/t_gpu.py` checks default limits (four storage textures per stage), dirty-upload - readback after 240 busy ticks with scrolling and both looks, exact dirty/full image equality - with a frozen clock, world replacement, transparent overlays, drill contours, shader compilation timing, successful device recreation and failed recovery with - an autosave and Reload. `gpuCheckUploads()` reads actual resident GPU texture bytes and + readback after 240 busy ticks with scrolling and both looks, exact dirty/full image equality, + noise continuity and pan/zoom equality against fresh view textures/full uploads with a + frozen clock, world replacement, transparent overlays, drill contours, shader compilation + timing, successful device recreation and failed recovery with an autosave and Reload. `gpuCheckUploads()` reads actual resident GPU texture bytes and compares them to the full CPU arrays, including Float32 temperatures, machine and block data. - `test/t_render_dirty.js` checks invalidation of sleeping chunks, heat edges, swaps, fog and trees. - The old CPU light checks now run as `test/t_gpu_light.py` / `test/light-cases.js`, reading the diff --git a/docs/agent-log.md b/docs/agent-log.md index 1195777..b6fadaf 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,45 @@ What agents have done, newest first. Add an entry for every piece of work: the d note or request it was for, what changed, the files touched, the tests run, and anything left open or not verified. Keep each entry short; the git history has the details. +## 2026-10-28: t91 sky renders as mismatched blocks (Codex) + +- Reproduced the owner's cloud rectangles and stepped distant hills on NVIDIA Blackwell: + generated seed 424242 at 2048x1024, smooth look, 1320x916 viewport, 30 submitted frames + panning (400,20) to (565.3,32.47) while zooming 4 to 1.68. Inspected + `shots/t91-before.png`, `t91-after.png`, `t91-pixel.png` and the seven-pair + `t91-golden-review.png`. Clouds and hills are continuous after the fix; pixel sky intact. +- Cause: `vn` in `gpu/smooth-shader.js` hashed shared lattice corners differently depending + on whether they came from the current cell or its offset neighbour. Isolated GPU readback + measured a 255/255 jump across an infinitesimal cell boundary. Rounding the offset corner + sums before `h21` reduces the maximum to 1/255. This implicates fused/reassociated hash + arithmetic in the shader port, not stale camera inputs. The previously frozen sky/joins + references also contained these seams, so their presence does not establish a 2d regression. + Floating-point fusion/reassociation is permitted by + [WGSL](https://www.w3.org/TR/WGSL/#floating-point-evaluation) and + [Vulkan](https://docs.vulkan.org/spec/latest/appendices/spirvenv.html#spirvenv-precision-operation). + No backend disassembly was captured; the precise optimizer transformation is inferred. +- Kept dirty world uploads unchanged: a forced full upload of the broken frame was exactly + identical. Audited view-dependent inputs: main camera/view/zoom uniform, smooth time/surface + uniform, overrides and row/column tables upload each draw; colour dispatch covers the full + view. Pixel colour shares this path. Lighting refreshes its own uniform/grid on its selected + cadence and retains the matching origin between updates. Existing cadence check still passes. +- Added two checks to `test/t_gpu.py`: actual smooth noise helpers rendered to an offscreen + texture and read across horizontal/vertical lattice boundaries (failed before, passes after); + 12-frame pan/zoom paths compared to recreated view textures/light history plus full world + uploads, pixel/smooth with full/fast/off lighting (all six comparisons exactly equal). + Seven smooth golden crops intentionally refreshed after inspection: renderer 12/2, joins 2, + sky 12/2, lamp 12 and magma 12. Shared noise also shades surfaces, so those textures change + slightly. All pixel and remaining smooth references retained; tolerances unchanged. +- Files: `src/render/gpu/smooth-shader.js`, `test/t_gpu.py`, `test/t_gpu_golden.py`, seven + `test/fixtures/golden/*-smooth-*.png`, `docs/TESTING.md`, this log and t91 tracking. + No simulation changes, DESIGN/other task edits or commit. +- Verification: focused GPU suite passes (14 checks, no page/WebGPU validation errors). + Full `python3 test/run_all.py --browser` exited 0, ALL PASSED: all 32 headless suites; + gameplay 361, smooth 28, console 18, capabilities 7, GPU 14, goldens 28, light 71 and + Firefox 2 checks. Evidence: `/tmp/t91-all-tests.log`. Diff check and kandown doctor passed; + t91 moved to Review. Sandbox had no WebGPU adapter; host-GPU execution used for reproduction + and verification. Other GPUs and owner playtesting remain unverified. + ## 2026-10-28: t88 phase 4 hardware verification and handoff (Codex) - Reviewed the existing uncommitted Phase 4 changes rather than duplicating the implementation. diff --git a/src/render/gpu/smooth-shader.js b/src/render/gpu/smooth-shader.js index 69c5b6e..f9e1f85 100644 --- a/src/render/gpu/smooth-shader.js +++ b/src/render/gpu/smooth-shader.js @@ -15,7 +15,8 @@ fn modulo(x: f32,y: f32) -> f32 { return x-y*floor(x/y); } fn h21(inputP: vec2f) -> f32 { var p=inputP; p = fract(p * vec2f(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } fn vn(p: vec2f) -> f32 { var i: vec2f = floor(p); var f: vec2f = fract(p); f = f * f * (3.0 - 2.0 * f); - return mix(mix(h21(i), h21(i + vec2f(1, 0)), f.x), mix(h21(i + vec2f(0, 1)), h21(i + vec2f(1, 1)), f.x), f.y); + // Round corner sums before hashing so fused arithmetic cannot give shared corners different hashes. + return mix(mix(h21(i), h21(floor(i + vec2f(1, 0))), f.x), mix(h21(floor(i + vec2f(0, 1))), h21(floor(i + vec2f(1, 1))), f.x), f.y); } fn fbm(inputP: vec2f) -> f32 { var p=inputP; var a: f32 = 0.5; var s: f32 = 0.0; for (var k: i32 = 0; k < 4; k++) { s += a * vn(p); p = p * 2.03 + 17.1; a *= 0.5; } return s / 0.9375; } diff --git a/test/fixtures/golden/joins-smooth-2.png b/test/fixtures/golden/joins-smooth-2.png index 6beeea3..7e3d487 100644 Binary files a/test/fixtures/golden/joins-smooth-2.png and b/test/fixtures/golden/joins-smooth-2.png differ diff --git a/test/fixtures/golden/lamp-smooth-12.png b/test/fixtures/golden/lamp-smooth-12.png index 2c6d72c..1dc3a4e 100644 Binary files a/test/fixtures/golden/lamp-smooth-12.png and b/test/fixtures/golden/lamp-smooth-12.png differ diff --git a/test/fixtures/golden/magma-smooth-12.png b/test/fixtures/golden/magma-smooth-12.png index 722df54..7724887 100644 Binary files a/test/fixtures/golden/magma-smooth-12.png and b/test/fixtures/golden/magma-smooth-12.png differ diff --git a/test/fixtures/golden/renderer-smooth-12.png b/test/fixtures/golden/renderer-smooth-12.png index 40663a6..1514a2a 100644 Binary files a/test/fixtures/golden/renderer-smooth-12.png and b/test/fixtures/golden/renderer-smooth-12.png differ diff --git a/test/fixtures/golden/renderer-smooth-2.png b/test/fixtures/golden/renderer-smooth-2.png index c30d489..bd49537 100644 Binary files a/test/fixtures/golden/renderer-smooth-2.png and b/test/fixtures/golden/renderer-smooth-2.png differ diff --git a/test/fixtures/golden/sky-smooth-12.png b/test/fixtures/golden/sky-smooth-12.png index 0ffb3cd..f33f1c6 100644 Binary files a/test/fixtures/golden/sky-smooth-12.png and b/test/fixtures/golden/sky-smooth-12.png differ diff --git a/test/fixtures/golden/sky-smooth-2.png b/test/fixtures/golden/sky-smooth-2.png index ce04884..07357d8 100644 Binary files a/test/fixtures/golden/sky-smooth-2.png and b/test/fixtures/golden/sky-smooth-2.png differ diff --git a/test/t_gpu.py b/test/t_gpu.py index 74bc1b2..e84022e 100644 --- a/test/t_gpu.py +++ b/test/t_gpu.py @@ -57,6 +57,61 @@ with launch(viewport={'width':960,'height':600}) as (_,pg,url): }""") assert all(r['max']==0 for r in parity),parity print('PASS dirty and forced full uploads render identically in',len(parity),'look/light/lens/fog configurations') + # Adjacent noise cells must use the same hash for their shared lattice corners. + noise=pg.evaluate("""async()=>{ + const code=GPU_SMOOTH.slice(GPU_SMOOTH.indexOf('fn h21'),GPU_SMOOTH.indexOf('fn lin'))+` + @vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f { + let p=array(vec2f(-1,-1),vec2f(3,-1),vec2f(-1,3));return vec4f(p[i],0,1); + } + @fragment fn fs(@builtin(position) p:vec4f)->@location(0) vec4f { + let edge=floor(p.x/2.);let along=floor(p.y)*0.1; + let epsilon=select(-0.0001,0.0001,u32(p.x)%2u==1u); + return vec4f(vn(vec2f(edge+epsilon,along)),vn(vec2f(along,edge+epsilon)),0,1); + }`; + const module=GPU.device.createShaderModule({code}); + const pipeline=await GPU.device.createRenderPipelineAsync({layout:'auto', + vertex:{module,entryPoint:'vs'},fragment:{module,entryPoint:'fs',targets:[{format:'rgba8unorm'}]}}); + const texture=GPU.device.createTexture({size:[960,100],format:'rgba8unorm', + usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.COPY_SRC}); + const buffer=GPU.device.createBuffer({size:960*100*4,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}); + const encoder=GPU.device.createCommandEncoder(); + const pass=encoder.beginRenderPass({colorAttachments:[{ + view:texture.createView(),loadOp:'clear',storeOp:'store'}]}); + pass.setPipeline(pipeline);pass.draw(3);pass.end(); + encoder.copyTextureToBuffer({texture},{buffer,bytesPerRow:960*4},[960,100]); + GPU.device.queue.submit([encoder.finish()]);await buffer.mapAsync(GPUMapMode.READ); + const pixels=new Uint8Array(buffer.getMappedRange());let max=0; + for(let y=0;y<100;y++)for(let x=0;x<960;x+=2)for(let c=0;c<2;c++) { + const i=(y*960+x)*4+c;max=Math.max(max,Math.abs(pixels[i]-pixels[i+4])); + } + buffer.unmap();buffer.destroy();texture.destroy();return max; + }""") + assert noise<=1,noise + print('PASS smooth noise is continuous across horizontal and vertical lattice boundaries',noise) + motion=pg.evaluate("""async()=>{ + const results=[],now=performance.now.bind(performance);performance.now=()=>5000; + try { + SET.fog=false;SET.glow='low';SET.lightEvery=1;R.lens=false; + for(const look of ['pixel','smooth'])for(const light of ['full','fast','off']) { + SET.look=look;SET.lighting=light; + for(let n=0;n<12;n++) { + SET.zoom=6-n*0.37;resizeRender();R.camX=80.25+n*7.3;R.camY=5.5+n*0.43; + R.rain=0;R.frame++;drawWorld();await GPU.device.queue.onSubmittedWorkDone(); + } + const a=worldPixelContext().getImageData(0,0,960,600).data; + const cleanCells=GPU.uploadCells; + // Discard all view textures/light history as well as the resident-world cache. + GPU.resident.fill(0);GPU.viewKey=null;GPU.lightFrame=undefined; + R.rain=0;drawWorld();await GPU.device.queue.onSubmittedWorkDone(); + const b=worldPixelContext().getImageData(0,0,960,600).data; + let max=0;for(let i=0;i{ SET.lightEvery=3;R.frame=1002;drawWorld();const old=GPU.lightFrame,origin=R.lx0; R.camX+=4;R.frame=1003;drawWorld();const held=GPU.lightFrame===old && R.lx0===origin; diff --git a/test/t_gpu_golden.py b/test/t_gpu_golden.py index 54375fe..b946d97 100644 --- a/test/t_gpu_golden.py +++ b/test/t_gpu_golden.py @@ -1,4 +1,4 @@ -"""Frozen legacy references, captured before retirement; compare the WebGPU world only.""" +"""Frozen world references; seven smooth crops corrected for continuous noise in t91.""" import base64, gzip, json, pathlib, sys, subprocess sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools')) from use_venv import ensure_playwright; ensure_playwright()