/// Merge all tile stages into a pixel array
@group(2) @binding(0) var<storage, read_write> tile64_zmin: array<atomic<u32>>;
@group(2) @binding(1) var<storage, read_write> tile16_zmin: array<atomic<u32>>;
@group(2) @binding(2) var<storage, read_write> tile4_zmin: array<atomic<u32>>;
@group(2) @binding(3) var<storage, read_write> voxels: array<u32>;
// Dispatched as an 2D workgroup across render_size pixels
@compute @workgroup_size(8, 8)
fn merge_main(
@builtin(global_invocation_id) global_id: vec3u
) {
// Clamp to render size (which is always >= image size)
if global_id.x >= config.render_size.x ||
global_id.y >= config.render_size.y
{
return;
}
// Precompute useful sizes
let size64 = config.render_size / 64;
let size16 = size64 * 4u;
let size4 = size16 * 4u;
// Compute indices within tile data (using render size bounds)
let index64 = global_id.x / 64 + global_id.y / 64 * size64.x;
let index16 = global_id.x / 16 + global_id.y / 16 * size16.x;
let index4 = global_id.x / 4 + global_id.y / 4 * size4.x;
let index1 = global_id.x + global_id.y * config.render_size.x;
// Merge larger tiles into the voxels image
var out = voxels[index1];
out = max(out, atomicLoad(&tile64_zmin[index64]));
out = max(out, atomicLoad(&tile16_zmin[index16]));
out = max(out, atomicLoad(&tile4_zmin[index4]));
// Clamp to image z size and write to the heightmap
voxels[index1] = min(out, config.image_size.z);
// Copying from high-res to low-res tiles is deliberately racey, because
// it's to improve the odds of raymarching early-exit (but is not required
// for correctness)
// Copy from voxels to tile4_zmin
if (global_id.x % 4) == 0 && (global_id.y % 4) == 0 {
var all_present = true;
var new_zmin = 0xFFFFFFFu;
let corner = global_id.xy;
for (var i=0u; i < 4u && all_present; i++) {
for (var j=0u; j < 4u && all_present; j++) {
let pos = corner + vec2u(i, j);
let index = pos.x + pos.y * config.render_size.x;
let v = voxels[index];
if v != 0 {
new_zmin = min(new_zmin, v);
} else {
all_present = false;
}
}
}
if all_present {
atomicMax(&tile4_zmin[index4], new_zmin);
}
}
// Copy from tile4_zmin to tile16_zmin
if (global_id.x % 16) == 0 && (global_id.y % 16) == 0 {
var all_present = true;
var new_zmin = 0xFFFFFFFu;
let corner = global_id.xy / 4;
for (var i=0u; i < 4u && all_present; i++) {
for (var j=0u; j < 4u && all_present; j++) {
let pos = corner + vec2u(i, j);
let index = pos.x + pos.y * size4.x;
let v = atomicLoad(&tile4_zmin[index]);
if v != 0 {
new_zmin = min(new_zmin, v);
} else {
all_present = false;
}
}
}
if all_present {
atomicMax(&tile16_zmin[index16], new_zmin);
}
}
// Copy from tile16_zmin to tile64_zmin
if (global_id.x % 64) == 0 && (global_id.y % 64) == 0 {
var all_present = true;
var new_zmin = 0xFFFFFFFu;
let corner = global_id.xy / 16;
for (var i=0u; i < 4u && all_present; i++) {
for (var j=0u; j < 4u && all_present; j++) {
let pos = corner + vec2u(i, j);
let index = pos.x + pos.y * size16.x;
let v = atomicLoad(&tile16_zmin[index]);
if v != 0 {
new_zmin = min(new_zmin, v);
} else {
all_present = false;
}
}
}
if all_present {
atomicMax(&tile64_zmin[index64], new_zmin);
}
}
}