Fix blocky WebGPU sky: view inputs refresh when the camera moves (t91)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 115 KiB |
@@ -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,3>(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<a.length;i++)max=Math.max(max,Math.abs(a[i]-b[i]));
|
||||
results.push({look,light,max,cleanCells});
|
||||
}
|
||||
SET.lighting='full';return results;
|
||||
} finally {performance.now=now;}
|
||||
}""")
|
||||
assert all(r['max']==0 for r in motion),motion
|
||||
print('PASS pan/zoom matches fresh full redraw in both looks and all light modes',motion)
|
||||
cadence=pg.evaluate("""()=>{
|
||||
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;
|
||||
|
||||
@@ -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()
|
||||
|
||||