OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / src / dc_gpu_mesh_pipeline.cpp

932 lines · cpp ref:
#include "dc_gpu_mesh_pipeline.h"
#include "dc_gpu_params.h"   // MeshPushConstants (shared with the GPU stage oracle)

#include <godot_cpp/classes/rd_shader_source.hpp>
#include <godot_cpp/classes/rd_shader_spirv.hpp>
#include <godot_cpp/classes/rd_uniform.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

#include <cstring>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <string>

#ifdef __linux__
#include <dlfcn.h>   // dladdr — resolve the shader dir relative to this .so
#endif
#ifdef _WIN32
#include <windows.h> // GetModuleHandleEx/GetModuleFileName — the same, for a DLL
#endif

// -------------------------------------------------------------------------
// Shader loading
// -------------------------------------------------------------------------

// Resolve the directory holding dc-terrain's .glsl compute shaders. Computed
// once and cached. Resolution order (first hit wins):
//   1. $DC_TERRAIN_SHADER_DIR            — explicit override (dev / testing)
//   2. <dir of this loaded .so/.dll>/shaders — shipped next to the GDExtension
//   3. compile-time DC_SHADER_DIR        — source-tree fallback if -D'd
// (2) is the production path: CI ships the shaders into bin/linux/shaders/
// alongside libdc_terrain…so, so this resolves on ANY host with no hardcoded
// absolute dev path. The old default ("/storage/projects/code/godot/
// dc-terrain/shaders") only existed on the build box, so on every other
// machine the GPU mesh pipeline failed to load its shaders and silently
// CPU-fell-back to dual-contouring meshing → ~4fps.
static const std::string& _shader_dir() {
    static const std::string dir = []() -> std::string {
        if (const char* env = std::getenv("DC_TERRAIN_SHADER_DIR")) {
            if (env[0]) return std::string(env);
        }
#ifdef __linux__
        Dl_info info;
        if (dladdr(reinterpret_cast<const void*>(&_shader_dir), &info) && info.dli_fname) {
            const std::string so_path(info.dli_fname);
            const size_t slash = so_path.find_last_of('/');
            if (slash != std::string::npos) return so_path.substr(0, slash) + "/shaders";
        }
#endif
#ifdef _WIN32
        // The same resolution, for a DLL. FROM_ADDRESS resolves the module that
        // CONTAINS this function — the GDExtension, not the host .exe — which is
        // the distinction dladdr makes above and the reason this is not simply
        // argv[0]: the game binary and the extension live in different
        // directories, and the shaders ship beside the extension.
        //
        // Without this, Windows fell through to the "shaders" relative path,
        // which resolves against the process's CURRENT DIRECTORY — fine when
        // launched from the install dir, wrong from a shortcut or a different
        // drive, and the failure mode is the silent CPU fall-back to
        // dual-contouring meshing that cost ~4fps on Linux before (2) existed.
        HMODULE hm = nullptr;
        if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
                                       GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                               reinterpret_cast<LPCSTR>(&_shader_dir), &hm)) {
            char buf[MAX_PATH];
            const DWORD n = GetModuleFileNameA(hm, buf, (DWORD)sizeof(buf));
            // n == sizeof(buf) means the path was truncated (MAX_PATH), so fall
            // through to the next resolution step rather than open the wrong dir.
            if (n > 0 && n < (DWORD)sizeof(buf)) {
                const std::string dll_path(buf, n);
                const size_t slash = dll_path.find_last_of("\\/");
                if (slash != std::string::npos) return dll_path.substr(0, slash) + "/shaders";
            }
        }
#endif
#ifdef DC_SHADER_DIR
        return std::string(DC_SHADER_DIR);
#else
        return std::string("shaders");
#endif
    }();
    return dir;
}

// Read a .glsl file from disk. Returns empty string on failure.
static std::string _read_shader_file(const char* filename) {
    const std::string path = _shader_dir() + "/" + filename;
    std::ifstream file(path);
    if (!file.is_open()) {
        UtilityFunctions::print("DCGpuMeshPipeline: FAILED to open shader: ", path.c_str());
        return "";
    }
    std::ostringstream ss;
    ss << file.rdbuf();
    return ss.str();
}

// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------

DCGpuMeshPipeline::~DCGpuMeshPipeline() {
    cleanup();
}

bool DCGpuMeshPipeline::initialize(RenderingDevice* rd) {
    rd_ = rd;
    if (!rd_) return false;

    if (!_compile_all_shaders()) {
        fprintf(stderr, "DCGpuMeshPipeline: Shader compilation failed\n");
        cleanup();
        return false;
    }

    available_ = true;
    return true;
}

void DCGpuMeshPipeline::cleanup() {
    if (!rd_) return;

    _free_buffers();
    _free_shader_pipeline(sp_edge_classify_);
    _free_shader_pipeline(sp_scan_blocks_);
    _free_shader_pipeline(sp_scan_single_);
    _free_shader_pipeline(sp_scan_propagate_);
    _free_shader_pipeline(sp_compact_index_);
    _free_shader_pipeline(sp_prepare_dispatch_);
    _free_shader_pipeline(sp_edge_intersect_);
    _free_shader_pipeline(sp_voxel_classify_);
    _free_shader_pipeline(sp_bake_planeset_);
    _free_shader_pipeline(sp_qef_solve_);
    _free_shader_pipeline(sp_vertex_polish_);
    _free_shader_pipeline(sp_quad_emit_);

    rd_ = nullptr;
    available_ = false;
}

// -------------------------------------------------------------------------
// Shader compilation
// -------------------------------------------------------------------------

DCGpuMeshPipeline::ShaderPipeline DCGpuMeshPipeline::_compile_shader(const char* glsl_source) {
    ShaderPipeline sp;

    Ref<RDShaderSource> src;
    src.instantiate();
    src->set_stage_source(RenderingDevice::SHADER_STAGE_COMPUTE, String(glsl_source));

    Ref<RDShaderSPIRV> spirv = rd_->shader_compile_spirv_from_source(src);
    if (spirv.is_null()) {
        fprintf(stderr, "DCGpuMeshPipeline: SPIR-V compilation returned null\n");
        return sp;
    }

    String err = spirv->get_stage_compile_error(RenderingDevice::SHADER_STAGE_COMPUTE);
    if (!err.is_empty()) {
        fprintf(stderr, "DCGpuMeshPipeline: Shader compile error: %s\n", err.utf8().get_data());
        return sp;
    }

    sp.shader = rd_->shader_create_from_spirv(spirv);
    if (!sp.shader.is_valid()) {
        fprintf(stderr, "DCGpuMeshPipeline: Failed to create shader from SPIR-V\n");
        return sp;
    }

    sp.pipeline = rd_->compute_pipeline_create(sp.shader);
    if (!sp.pipeline.is_valid()) {
        fprintf(stderr, "DCGpuMeshPipeline: Failed to create compute pipeline\n");
        rd_->free_rid(sp.shader);
        sp.shader = RID();
        return sp;
    }

    return sp;
}

void DCGpuMeshPipeline::_free_shader_pipeline(ShaderPipeline& sp) {
    if (!rd_) return;
    if (sp.pipeline.is_valid()) { rd_->free_rid(sp.pipeline); sp.pipeline = RID(); }
    if (sp.shader.is_valid()) { rd_->free_rid(sp.shader); sp.shader = RID(); }
}

bool DCGpuMeshPipeline::_compile_all_shaders() {
    // Load and compile all static shaders
    struct ShaderFile {
        const char* filename;
        ShaderPipeline* target;
    };

    ShaderFile files[] = {
        { "edge_classify.glsl",       &sp_edge_classify_ },
        { "scan_blocks.glsl",         &sp_scan_blocks_ },
        { "scan_single.glsl",         &sp_scan_single_ },
        { "scan_propagate.glsl",      &sp_scan_propagate_ },
        { "stream_compact_index.glsl",&sp_compact_index_ },
        { "prepare_dispatch.glsl",    &sp_prepare_dispatch_ },
        { "voxel_classify.glsl",      &sp_voxel_classify_ },
        { "bake_planeset.glsl",       &sp_bake_planeset_ },
        { "qef_solve.glsl",           &sp_qef_solve_ },
        { "vertex_polish.glsl",       &sp_vertex_polish_ },
        { "quad_emit.glsl",           &sp_quad_emit_ },
    };

    for (auto& sf : files) {
        std::string source = _read_shader_file(sf.filename);
        if (source.empty()) {
            UtilityFunctions::print("DCGpuMeshPipeline: Failed to load ", sf.filename);
            return false;
        }

        *sf.target = _compile_shader(source.c_str());
        if (!sf.target->shader.is_valid()) {
            UtilityFunctions::print("DCGpuMeshPipeline: Failed to compile ", sf.filename);
            return false;
        }
        UtilityFunctions::print("DCGpuMeshPipeline: Compiled ", sf.filename);
    }

    // edge_intersect is a template — compile with default SDF body
    if (!_compile_edge_intersect(nullptr)) {
        fprintf(stderr, "DCGpuMeshPipeline: Failed to compile edge_intersect (default)\n");
        return false;
    }

    return true;
}

std::string DCGpuMeshPipeline::_build_edge_intersect_glsl(const char* sdf_body) {
    std::string tmpl = _read_shader_file("edge_intersect.glsl");
    if (tmpl.empty()) return "";

    // The default SDF body is the noise generator from density_fill
    std::string body;
    if (sdf_body) {
        body = sdf_body;
    } else {
        // Load the default noise SDF generator
        std::string noise_sdf = _read_shader_file("generators/noise_sdf.glsl");
        if (noise_sdf.empty()) {
            fprintf(stderr, "DCGpuMeshPipeline: Failed to load noise_sdf.glsl\n");
            return "";
        }
        body = noise_sdf;
    }

    // Noise volume samplers, only if the body samples them. A declared-but-
    // unused binding can be optimised away, and then the uniform set supplies a
    // descriptor the shader does not reflect and creation fails.
    {
        const std::string smarker = "// GENERATOR_SAMPLER_BINDINGS";
        size_t spos = tmpl.find(smarker);
        if (spos != std::string::npos) {
            std::string decls;
            if (body.find("noiseVol") != std::string::npos) {
                for (int i = 0; i < 4; i++) {
                    decls += "layout(set = 0, binding = " + std::to_string(14 + i)
                           + ") uniform sampler3D noiseVol" + std::to_string(i) + ";\n";
                }
                for (int i = 0; i < 4; i++) {
                    decls += "layout(set = 0, binding = " + std::to_string(18 + i)
                           + ") uniform sampler3D packedNoiseVol" + std::to_string(i) + ";\n";
                }
            }
            tmpl.replace(spos, smarker.length(), decls);
        }
    }
    sdf_uses_noise_ = body.find("noiseVol") != std::string::npos;

    // Replace the marker with the SDF body
    const std::string marker = "// GENERATOR_SDF_BODY";
    size_t pos = tmpl.find(marker);
    if (pos == std::string::npos) {
        fprintf(stderr, "DCGpuMeshPipeline: GENERATOR_SDF_BODY marker not found\n");
        return "";
    }
    tmpl.replace(pos, marker.length(), body);
    return tmpl;
}

bool DCGpuMeshPipeline::_compile_edge_intersect(const char* sdf_body) {
    std::string glsl = _build_edge_intersect_glsl(sdf_body);
    if (glsl.empty()) return false;

    ShaderPipeline new_sp = _compile_shader(glsl.c_str());
    if (!new_sp.shader.is_valid()) return false;

    // 2a: us_edge_intersect_ binds the shader we're about to free. Freeing the
    // shader would auto-free that uniform set into a stale RID (the same
    // double-free class as the BVH-buffer bug). Free all cached sets first (marks
    // them invalid) so prepare_mesh rebuilds them against the new shader.
    _free_uniform_sets();
    _free_shader_pipeline(sp_edge_intersect_);
    sp_edge_intersect_ = new_sp;

    current_sdf_body_ = sdf_body ? sdf_body : "";
    fprintf(stderr, "DCGpuMeshPipeline: Compiled edge_intersect\n");
    return true;
}

bool DCGpuMeshPipeline::set_custom_sdf(const char* sdf_body) {
    std::string new_body = sdf_body ? sdf_body : "";
    if (new_body == current_sdf_body_) return true;
    return _compile_edge_intersect(sdf_body);
}

// -------------------------------------------------------------------------
// Buffer allocation
// -------------------------------------------------------------------------

void DCGpuMeshPipeline::_allocate_buffers(int grid_dims) {
    if (current_grid_dims_ == grid_dims) return;
    _free_buffers();

    int V = grid_dims;           // 32
    int V3 = V * V * V;          // 32768
    int edge_count = V3 * 3;     // 98304
    int scan_blocks = (edge_count + 1023) / 1024 + 1;  // ceil + 1 for safety

    auto make_buf = [&](int bytes) -> RID {
        PackedByteArray zero;
        zero.resize(bytes);
        memset(zero.ptrw(), 0, bytes);
        return rd_->storage_buffer_create(bytes, zero);
    };

    edge_flags_buf_       = make_buf(edge_count * 4);
    edge_scan_buf_        = make_buf(edge_count * 4);
    active_edges_buf_     = make_buf(edge_count * 4);
    edge_data_buf_        = make_buf(edge_count * 8 * 4);  // 8 floats per edge
    voxel_flags_buf_      = make_buf(V3 * 4);
    voxel_scan_buf_       = make_buf(V3 * 4);
    active_voxels_buf_    = make_buf(V3 * 4);
    vertex_buf_           = make_buf(V3 * 16 * 4);   // 16 floats per vertex (12 + curvature + pad(3))
    index_buf_            = make_buf(edge_count * 6 * 4);  // 6 indices per edge (worst case)
    counts_buf_           = make_buf(16);             // 4 uints
    // dispatch_args needs DISPATCH_INDIRECT usage flag for compute_list_dispatch_indirect
    {
        PackedByteArray zero;
        zero.resize(32);
        memset(zero.ptrw(), 0, 32);
        dispatch_args_buf_ = rd_->storage_buffer_create(32, zero,
            RenderingDevice::STORAGE_BUFFER_USAGE_DISPATCH_INDIRECT);
    }
    scan_block_sums_buf_  = make_buf(scan_blocks * 4);
    scan_block_sums_scanned_buf_ = make_buf(scan_blocks * 4);
    planeset_buf_         = make_buf(V3 * 64);   // per-cell plane-set brick (64 bytes/cell)

    current_grid_dims_ = grid_dims;
    fprintf(stderr, "DCGpuMeshPipeline: Allocated buffers for grid %d^3 (~%.1f MB)\n",
            grid_dims, (edge_count * 4 * 5 + V3 * 4 * 7 + edge_count * 32 + V3 * 48 + edge_count * 24) / (1024.0 * 1024.0));
}

void DCGpuMeshPipeline::_free_buffers() {
    if (!rd_) return;
    _free_uniform_sets();

    auto free = [&](RID& rid) {
        if (rid.is_valid()) { rd_->free_rid(rid); rid = RID(); }
    };

    free(edge_flags_buf_);
    free(edge_scan_buf_);
    free(active_edges_buf_);
    free(edge_data_buf_);
    free(voxel_flags_buf_);
    free(voxel_scan_buf_);
    free(active_voxels_buf_);
    free(vertex_buf_);
    free(planeset_buf_);
    free(index_buf_);
    free(counts_buf_);
    free(dispatch_args_buf_);
    free(scan_block_sums_buf_);
    free(scan_block_sums_scanned_buf_);

    current_grid_dims_ = 0;
}

// -------------------------------------------------------------------------
// Uniform set helper
// -------------------------------------------------------------------------

// Bindings 14..21 of edge_intersect: the eight noise volumes, plain 0-3 with a
// linear sampler and packed 4-7 with a nearest one (the packed volumes carry
// neighbour values in gba and must NOT be hardware-filtered).
std::vector<SamplerBinding> DCGpuMeshPipeline::_noise_sampler_bindings(
        const DensityBuffers& bufs) const {
    std::vector<SamplerBinding> out;
    if (!sdf_uses_noise_) return out;   // template declared none; supply none
    for (int i = 0; i < 8; i++) {
        RID smp = (i < 4) ? bufs.noise_sampler_linear : bufs.noise_sampler_nearest;
        out.push_back({14 + i, smp, bufs.noise_tex[i]});
    }
    return out;
}

RID DCGpuMeshPipeline::_make_uniform_set(const RID& shader,
                                          const std::vector<std::pair<int, RID>>& bindings,
                                          bool is_ubo_binding_4,
                                          const std::vector<SamplerBinding>& samplers) {
    TypedArray<RDUniform> uniforms;
    for (auto& [binding, rid] : bindings) {
        Ref<RDUniform> u;
        u.instantiate();
        // Binding 4 in edge_intersect is a UBO (std140), all others are SSBOs
        if (is_ubo_binding_4 && binding == 4) {
            u->set_uniform_type(RenderingDevice::UNIFORM_TYPE_UNIFORM_BUFFER);
        } else {
            u->set_uniform_type(RenderingDevice::UNIFORM_TYPE_STORAGE_BUFFER);
        }
        u->set_binding(binding);
        u->add_id(rid);
        uniforms.push_back(u);
    }
    // Combined image samplers. Order matters: sampler id FIRST, then texture.
    for (auto& sb : samplers) {
        if (!sb.sampler.is_valid() || !sb.texture.is_valid()) continue;
        Ref<RDUniform> u;
        u.instantiate();
        u->set_uniform_type(RenderingDevice::UNIFORM_TYPE_SAMPLER_WITH_TEXTURE);
        u->set_binding(sb.binding);
        u->add_id(sb.sampler);
        u->add_id(sb.texture);
        uniforms.push_back(u);
    }
    return rd_->uniform_set_create(uniforms, shader, 0);
}

// -------------------------------------------------------------------------
// Prefix scan (3-pass)
// -------------------------------------------------------------------------

void DCGpuMeshPipeline::_prefix_scan(int64_t compute_list,
                                      RID input_buf, RID output_buf, int N) {
    // Pass 1: scan_blocks — per-block exclusive scan, write block sums
    {
        RID us = _make_uniform_set(sp_scan_blocks_.shader, {
            {0, input_buf}, {1, output_buf}, {2, scan_block_sums_buf_}
        });
        rd_->compute_list_bind_compute_pipeline(compute_list, sp_scan_blocks_.pipeline);
        rd_->compute_list_bind_uniform_set(compute_list, us, 0);
        // Push constant: N
        PackedByteArray pc;
        pc.resize(16);
        memset(pc.ptrw(), 0, 16);
        uint32_t n = static_cast<uint32_t>(N);
        memcpy(pc.ptrw(), &n, 4);
        rd_->compute_list_set_push_constant(compute_list, pc, 16);
        int groups = (N + 1023) / 1024;
        rd_->compute_list_dispatch(compute_list, groups, 1, 1);
        rd_->compute_list_add_barrier(compute_list);
        rd_->free_rid(us);
    }

    // Pass 2: scan_single — scan the block sums (single workgroup)
    {
        int num_blocks = (N + 1023) / 1024;
        RID us = _make_uniform_set(sp_scan_single_.shader, {
            {0, scan_block_sums_buf_}, {1, scan_block_sums_scanned_buf_}
        });
        rd_->compute_list_bind_compute_pipeline(compute_list, sp_scan_single_.pipeline);
        rd_->compute_list_bind_uniform_set(compute_list, us, 0);
        PackedByteArray pc;
        pc.resize(16);
        memset(pc.ptrw(), 0, 16);
        uint32_t nb = static_cast<uint32_t>(num_blocks);
        memcpy(pc.ptrw(), &nb, 4);
        rd_->compute_list_set_push_constant(compute_list, pc, 16);
        rd_->compute_list_dispatch(compute_list, 1, 1, 1);
        rd_->compute_list_add_barrier(compute_list);
        rd_->free_rid(us);
    }

    // Pass 3: scan_propagate — add block prefix sums to per-block results
    {
        RID us = _make_uniform_set(sp_scan_propagate_.shader, {
            {0, output_buf}, {1, scan_block_sums_scanned_buf_}
        });
        rd_->compute_list_bind_compute_pipeline(compute_list, sp_scan_propagate_.pipeline);
        rd_->compute_list_bind_uniform_set(compute_list, us, 0);
        PackedByteArray pc;
        pc.resize(16);
        memset(pc.ptrw(), 0, 16);
        uint32_t n = static_cast<uint32_t>(N);
        memcpy(pc.ptrw(), &n, 4);
        rd_->compute_list_set_push_constant(compute_list, pc, 16);
        int groups = (N + 1023) / 1024;
        rd_->compute_list_dispatch(compute_list, groups, 1, 1);
        rd_->compute_list_add_barrier(compute_list);
        rd_->free_rid(us);
    }
}

// -------------------------------------------------------------------------
// Main pipeline dispatch
// -------------------------------------------------------------------------

// Push constants struct matching the shader layout
// MeshPushConstants now in dc_gpu_params.h (shared with the GPU stage oracle).

void DCGpuMeshPipeline::_free_uniform_sets() {
    if (!rd_) return;
    auto fr = [&](RID& r) { if (r.is_valid()) { rd_->free_rid(r); r = RID(); } };
    fr(us_edge_classify_); fr(us_compact_edge_); fr(us_prepare_edge_);
    fr(us_edge_intersect_);
    fr(us_voxel_classify_); fr(us_compact_voxel_); fr(us_prepare_voxel_);
    fr(us_qef_solve_); fr(us_vertex_polish_); fr(us_quad_emit_); fr(us_bake_planeset_);
    for (int i = 0; i < 3; i++) { fr(us_scan_edge_[i]); fr(us_scan_voxel_[i]); }
    uniform_sets_valid_ = false;  // 2a: any teardown forces a rebuild next prepare
}

void DCGpuMeshPipeline::_build_uniform_sets(const DensityBuffers& bufs) {
    _free_uniform_sets();

    int V = current_grid_dims_;
    int V3 = V * V * V;
    int edge_count = V3 * 3;

    // Stage 2: edge_classify
    us_edge_classify_ = _make_uniform_set(sp_edge_classify_.shader, {
        {0, bufs.density}, {1, edge_flags_buf_}
    });

    // Stage 4: compact edges
    us_compact_edge_ = _make_uniform_set(sp_compact_index_.shader, {
        {0, edge_flags_buf_}, {1, edge_scan_buf_},
        {2, active_edges_buf_}, {3, counts_buf_}
    });

    // Stage 5a: prepare dispatch (edges)
    us_prepare_edge_ = _make_uniform_set(sp_prepare_dispatch_.shader, {
        {0, edge_scan_buf_}, {1, edge_flags_buf_},
        {2, dispatch_args_buf_}, {3, counts_buf_}
    });

    // Stage 6: edge_intersect
    us_edge_intersect_ = _make_uniform_set(sp_edge_intersect_.shader, {
        {0, bufs.density}, {1, edge_flags_buf_}, {2, edge_scan_buf_},
        {3, edge_data_buf_},
        {4, bufs.params}, {5, bufs.voronoi}, {6, bufs.perm}, {7, bufs.edit},
        {8, active_edges_buf_}, {9, counts_buf_}, {10, bufs.brick},
        {11, bufs.convex_planes}, {13, bufs.custom_data}
    }, true, _noise_sampler_bindings(bufs));

    // Stage 7: voxel_classify
    us_voxel_classify_ = _make_uniform_set(sp_voxel_classify_.shader, {
        {1, edge_flags_buf_}, {4, voxel_flags_buf_}
    });

    // Stage 9: compact voxels
    us_compact_voxel_ = _make_uniform_set(sp_compact_index_.shader, {
        {0, voxel_flags_buf_}, {1, voxel_scan_buf_},
        {2, active_voxels_buf_}, {3, counts_buf_}
    });

    // Stage 5b: prepare dispatch (voxels)
    us_prepare_voxel_ = _make_uniform_set(sp_prepare_dispatch_.shader, {
        {0, voxel_scan_buf_}, {1, voxel_flags_buf_},
        {2, dispatch_args_buf_}, {3, counts_buf_}
    });

    // Stage 10: qef_solve. Bindings 9/10 add the SAME edit + convex-plane buffers
    // edge_intersect uses, so qef_solve can capture the exact per-cell plane-set
    // (face/crease/corner) from the brush geometry for the feature-aware snap
    // (fixes the angled-edge sawtooth). No new buffer — reuses existing RIDs.
    us_qef_solve_ = _make_uniform_set(sp_qef_solve_.shader, {
        {0, bufs.density}, {1, edge_flags_buf_}, {2, edge_scan_buf_},
        {3, edge_data_buf_}, {4, voxel_flags_buf_}, {5, voxel_scan_buf_},
        {6, vertex_buf_}, {7, active_voxels_buf_}, {8, counts_buf_},
        {9, bufs.edit}, {10, bufs.convex_planes}, {11, planeset_buf_}
    });

    // Stage 9: VERTEX POLISH. Clamped gradient descent, pulling each QEF vertex
    // onto the true isosurface. The shader has existed and never been
    // dispatched. QEF on a near-planar surface is underdetermined in two axes --
    // one vertical edge crosses and the horizontal ones do not -- so the solver
    // falls back on centroid regularisation and the vertex lands wherever the
    // cell centre is. That reads as scattered misplaced facets, and it survives
    // dc_auto_smooth because a badly placed vertex makes faces beyond its 40
    // degree threshold. Normal precision cannot touch it: the GEOMETRY is wrong.
    us_vertex_polish_ = _make_uniform_set(sp_vertex_polish_.shader, {
        {0, bufs.density}, {4, voxel_flags_buf_}, {5, voxel_scan_buf_},
        {6, vertex_buf_}, {7, active_voxels_buf_}, {8, counts_buf_}
    });

    // Plane-set BAKE (runs per cell before qef_solve): edits + convex planes in,
    // the per-cell plane-set brick out.
    us_bake_planeset_ = _make_uniform_set(sp_bake_planeset_.shader, {
        {9, bufs.edit}, {10, bufs.convex_planes}, {11, planeset_buf_},
        {12, bufs.convex_faceproj}
    });

    // Stage 11: quad_emit
    us_quad_emit_ = _make_uniform_set(sp_quad_emit_.shader, {
        {0, bufs.density}, {1, edge_flags_buf_},
        {4, voxel_flags_buf_}, {5, voxel_scan_buf_},
        {6, vertex_buf_}, {8, index_buf_}, {9, counts_buf_}
    });

    // Scan uniform sets (edge scan)
    us_scan_edge_[0] = _make_uniform_set(sp_scan_blocks_.shader, {
        {0, edge_flags_buf_}, {1, edge_scan_buf_}, {2, scan_block_sums_buf_}
    });
    us_scan_edge_[1] = _make_uniform_set(sp_scan_single_.shader, {
        {0, scan_block_sums_buf_}, {1, scan_block_sums_scanned_buf_}
    });
    us_scan_edge_[2] = _make_uniform_set(sp_scan_propagate_.shader, {
        {0, edge_scan_buf_}, {1, scan_block_sums_scanned_buf_}
    });

    // Scan uniform sets (voxel scan)
    us_scan_voxel_[0] = _make_uniform_set(sp_scan_blocks_.shader, {
        {0, voxel_flags_buf_}, {1, voxel_scan_buf_}, {2, scan_block_sums_buf_}
    });
    us_scan_voxel_[1] = _make_uniform_set(sp_scan_single_.shader, {
        {0, scan_block_sums_buf_}, {1, scan_block_sums_scanned_buf_}
    });
    us_scan_voxel_[2] = _make_uniform_set(sp_scan_propagate_.shader, {
        {0, voxel_scan_buf_}, {1, scan_block_sums_scanned_buf_}
    });
}

void DCGpuMeshPipeline::prepare_mesh(int grid_dims, const DensityBuffers& bufs) {
    if (!available_ || !rd_) return;
    _allocate_buffers(grid_dims);  // invalidates uniform sets if grid_dims changed
    // 2a: build the 15 uniform sets once and reuse across chunks — they bind only
    // stable buffers (see uniform_sets_valid_ note). Was rebuilding all 15 every
    // chunk (15 uniform_set_create + 15 free_rid on the serial GPU thread) while
    // the GPU was idle. Rebuilt only when invalidated (grid_dims / SDF change).
    if (!uniform_sets_valid_) {
        _build_uniform_sets(bufs);
        uniform_sets_valid_ = true;
    }

    // Clear counts buffer
    PackedByteArray zero;
    zero.resize(16);
    memset(zero.ptrw(), 0, 16);
    rd_->buffer_update(counts_buf_, 0, 16, zero);
}

bool DCGpuMeshPipeline::dispatch_mesh_stages(int64_t cl,
                                               const DensityBuffers& bufs, int grid_dims,
                                               const float cell_origin[3], float voxel_size,
                                               uint32_t mesh_quality_flags, bool has_edits) {
    if (!available_ || !rd_) return false;

    int V = grid_dims;
    int V3 = V * V * V;
    int edge_count = V3 * 3;

    MeshPushConstants pc;
    memset(&pc, 0, sizeof(pc));
    pc.cell_origin[0] = cell_origin[0];
    pc.cell_origin[1] = cell_origin[1];
    pc.cell_origin[2] = cell_origin[2];
    pc.voxel_size = voxel_size;
    pc.grid_dims[0] = V;
    pc.grid_dims[1] = V;
    pc.grid_dims[2] = V;
    pc.total_elements = static_cast<uint32_t>(V3);
    pc.mesh_quality_flags = mesh_quality_flags;

    PackedByteArray pc_bytes;
    pc_bytes.resize(sizeof(MeshPushConstants));
    memcpy(pc_bytes.ptrw(), &pc, sizeof(MeshPushConstants));

    auto make_scan_pc = [](uint32_t N) -> PackedByteArray {
        PackedByteArray p;
        p.resize(16);
        memset(p.ptrw(), 0, 16);
        memcpy(p.ptrw(), &N, 4);
        return p;
    };

    // Stage 2: Edge Classify
    rd_->compute_list_bind_compute_pipeline(cl, sp_edge_classify_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_edge_classify_, 0);
    rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
    rd_->compute_list_dispatch(cl, (V3 + 255) / 256, 1, 1);
    rd_->compute_list_add_barrier(cl);

    // Stage 3: Prefix Scan (edges) — using cached uniform sets
    {
        rd_->compute_list_bind_compute_pipeline(cl, sp_scan_blocks_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_scan_edge_[0], 0);
        PackedByteArray spc = make_scan_pc(edge_count);
        rd_->compute_list_set_push_constant(cl, spc, 16);
        rd_->compute_list_dispatch(cl, (edge_count + 1023) / 1024, 1, 1);
        rd_->compute_list_add_barrier(cl);

        int num_blocks = (edge_count + 1023) / 1024;
        rd_->compute_list_bind_compute_pipeline(cl, sp_scan_single_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_scan_edge_[1], 0);
        spc = make_scan_pc(num_blocks);
        rd_->compute_list_set_push_constant(cl, spc, 16);
        rd_->compute_list_dispatch(cl, 1, 1, 1);
        rd_->compute_list_add_barrier(cl);

        rd_->compute_list_bind_compute_pipeline(cl, sp_scan_propagate_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_scan_edge_[2], 0);
        spc = make_scan_pc(edge_count);
        rd_->compute_list_set_push_constant(cl, spc, 16);
        rd_->compute_list_dispatch(cl, (edge_count + 1023) / 1024, 1, 1);
        rd_->compute_list_add_barrier(cl);
    }

    // Stage 4: Stream Compact edges
    rd_->compute_list_bind_compute_pipeline(cl, sp_compact_index_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_compact_edge_, 0);
    rd_->compute_list_set_push_constant(cl, make_scan_pc(edge_count), 16);
    rd_->compute_list_dispatch(cl, (edge_count + 255) / 256, 1, 1);
    rd_->compute_list_add_barrier(cl);

    // Stage 5a: Prepare Dispatch (edges)
    {
        rd_->compute_list_bind_compute_pipeline(cl, sp_prepare_dispatch_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_prepare_edge_, 0);
        uint32_t prep_vals[4] = { static_cast<uint32_t>(edge_count), 0, 0, 0 };
        PackedByteArray prep_pc;
        prep_pc.resize(16);
        memcpy(prep_pc.ptrw(), prep_vals, 16);
        rd_->compute_list_set_push_constant(cl, prep_pc, 16);
        rd_->compute_list_dispatch(cl, 1, 1, 1);
        rd_->compute_list_add_barrier(cl);
    }

    // Stage 6: Edge Intersect (INDIRECT)
    rd_->compute_list_bind_compute_pipeline(cl, sp_edge_intersect_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_edge_intersect_, 0);
    rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
    rd_->compute_list_dispatch_indirect(cl, dispatch_args_buf_, 0);
    rd_->compute_list_add_barrier(cl);

    // Stage 7: Voxel Classify
    rd_->compute_list_bind_compute_pipeline(cl, sp_voxel_classify_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_voxel_classify_, 0);
    rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
    rd_->compute_list_dispatch(cl, (V3 + 255) / 256, 1, 1);
    rd_->compute_list_add_barrier(cl);

    // Stage 8: Prefix Scan (voxels) — using cached uniform sets
    {
        rd_->compute_list_bind_compute_pipeline(cl, sp_scan_blocks_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_scan_voxel_[0], 0);
        PackedByteArray spc = make_scan_pc(V3);
        rd_->compute_list_set_push_constant(cl, spc, 16);
        rd_->compute_list_dispatch(cl, (V3 + 1023) / 1024, 1, 1);
        rd_->compute_list_add_barrier(cl);

        int num_blocks = (V3 + 1023) / 1024;
        rd_->compute_list_bind_compute_pipeline(cl, sp_scan_single_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_scan_voxel_[1], 0);
        spc = make_scan_pc(num_blocks);
        rd_->compute_list_set_push_constant(cl, spc, 16);
        rd_->compute_list_dispatch(cl, 1, 1, 1);
        rd_->compute_list_add_barrier(cl);

        rd_->compute_list_bind_compute_pipeline(cl, sp_scan_propagate_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_scan_voxel_[2], 0);
        spc = make_scan_pc(V3);
        rd_->compute_list_set_push_constant(cl, spc, 16);
        rd_->compute_list_dispatch(cl, (V3 + 1023) / 1024, 1, 1);
        rd_->compute_list_add_barrier(cl);
    }

    // Stage 9: Stream Compact voxels
    rd_->compute_list_bind_compute_pipeline(cl, sp_compact_index_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_compact_voxel_, 0);
    rd_->compute_list_set_push_constant(cl, make_scan_pc(V3), 16);
    rd_->compute_list_dispatch(cl, (V3 + 255) / 256, 1, 1);
    rd_->compute_list_add_barrier(cl);

    // Stage 5b: Prepare Dispatch (voxels)
    {
        rd_->compute_list_bind_compute_pipeline(cl, sp_prepare_dispatch_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_prepare_voxel_, 0);
        uint32_t prep_vals[4] = { static_cast<uint32_t>(V3), 3, 2, 0 };
        PackedByteArray prep_pc;
        prep_pc.resize(16);
        memcpy(prep_pc.ptrw(), prep_vals, 16);
        rd_->compute_list_set_push_constant(cl, prep_pc, 16);
        rd_->compute_list_dispatch(cl, 1, 1, 1);
        rd_->compute_list_add_barrier(cl);
    }

    // Stage 9b: Plane-set BAKE (per cell) — capture the exact convex-brush plane-set
    // from the brush geometry into the brick, so qef_solve READS it instead of
    // re-deriving it. GATED on has_edits: a no-edit chunk has no hard cells (no brush
    // edges), so qef never reads the brick — skip the bake entirely (stale slots are
    // harmless, unread). Runs over all cells; qef_solve (active cells) reads its slot.
    if (has_edits) {
        rd_->compute_list_bind_compute_pipeline(cl, sp_bake_planeset_.pipeline);
        rd_->compute_list_bind_uniform_set(cl, us_bake_planeset_, 0);
        rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
        rd_->compute_list_dispatch(cl, (V3 + 255) / 256, 1, 1);
        rd_->compute_list_add_barrier(cl);
    }

    // Stage 10: QEF Solve (INDIRECT)
    rd_->compute_list_bind_compute_pipeline(cl, sp_qef_solve_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_qef_solve_, 0);
    rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
    rd_->compute_list_dispatch_indirect(cl, dispatch_args_buf_, 12);
    rd_->compute_list_add_barrier(cl);

    // Stage 9b: Vertex Polish — refine the QEF positions onto the isosurface.
    rd_->compute_list_bind_compute_pipeline(cl, sp_vertex_polish_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_vertex_polish_, 0);
    rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
    rd_->compute_list_dispatch_indirect(cl, dispatch_args_buf_, 12);
    rd_->compute_list_add_barrier(cl);

    // Stage 11: Quad Emit
    rd_->compute_list_bind_compute_pipeline(cl, sp_quad_emit_.pipeline);
    rd_->compute_list_bind_uniform_set(cl, us_quad_emit_, 0);
    rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
    rd_->compute_list_dispatch(cl, (V3 + 255) / 256, 1, 1);

    return true;
}

bool DCGpuMeshPipeline::readback_mesh(DCMeshData& out_mesh) {
    if (!rd_ || !counts_buf_.is_valid()) return false;

    // Read counts: [0]=edge_count, [1]=index_count, [2]=voxel_count
    PackedByteArray count_data = rd_->buffer_get_data(counts_buf_, 0, 16);
    const uint32_t* counts = reinterpret_cast<const uint32_t*>(count_data.ptr());
    uint32_t active_edge_count = counts[0];
    uint32_t index_count = counts[1];
    uint32_t active_voxel_count = counts[2];

    if (active_voxel_count == 0 || index_count == 0) {
        return false;  // empty chunk
    }

    // Read vertices (12 floats per vertex)
    int vert_bytes = static_cast<int>(active_voxel_count) * 16 * 4;
    PackedByteArray vert_data = rd_->buffer_get_data(vertex_buf_, 0, vert_bytes);
    const float* verts = reinterpret_cast<const float*>(vert_data.ptr());

    // Read indices
    int idx_bytes = static_cast<int>(index_count) * 4;
    PackedByteArray idx_data = rd_->buffer_get_data(index_buf_, 0, idx_bytes);
    const uint32_t* idxs = reinterpret_cast<const uint32_t*>(idx_data.ptr());

    // Convert GPU vertex format to DCMeshData, sanitizing for RT BLAS safety.
    out_mesh.positions.resize(active_voxel_count * 3);
    out_mesh.normals.resize(active_voxel_count * 3);
    out_mesh.materials.resize(active_voxel_count);   // vertex slot 8 (route ② texturing)
    out_mesh.proj.resize(active_voxel_count);        // vertex slot 10 (Stage-2 per-face texturing)
    out_mesh.proj2.resize(active_voxel_count);       // vertex slot 11 (Stage-2 crease other face)
    out_mesh.curv.resize(active_voxel_count);        // vertex slot 12 (curvature, metres)
    uint32_t nan_count = 0;
    for (uint32_t i = 0; i < active_voxel_count; i++) {
        const float* v = &verts[i * 16];
        float px = v[0], py = v[1], pz = v[2];
        float nx = v[3], ny = v[4], nz = v[5];
        uint32_t mat = *reinterpret_cast<const uint32_t*>(&v[8]);  // material_id (uint bits)
        uint32_t prj = *reinterpret_cast<const uint32_t*>(&v[10]); // projection candidate 0 (uint bits)
        uint32_t prj2 = *reinterpret_cast<const uint32_t*>(&v[11]);// projection candidate 1 (uint bits)
        float curv = v[12];                                        // curvature (float, not bits)
        if (std::isnan(px) || std::isnan(py) || std::isnan(pz) ||
            std::isinf(px) || std::isinf(py) || std::isinf(pz)) {
            px = py = pz = 0.0f;
            nx = 0.0f; ny = 1.0f; nz = 0.0f;
            mat = 0u;
            prj = 0u;
            prj2 = 0u;
            curv = 0.0f;
            nan_count++;
        }
        out_mesh.positions[i * 3 + 0] = px;
        out_mesh.positions[i * 3 + 1] = py;
        out_mesh.positions[i * 3 + 2] = pz;
        out_mesh.normals[i * 3 + 0] = nx;
        out_mesh.normals[i * 3 + 1] = ny;
        out_mesh.normals[i * 3 + 2] = nz;
        out_mesh.materials[i] = static_cast<float>(mat);
        out_mesh.proj[i] = static_cast<float>(prj);
        out_mesh.proj2[i] = static_cast<float>(prj2);
        out_mesh.curv[i] = std::isfinite(curv) ? curv : 0.0f;
    }

    // Validate indices: drop out-of-bounds and degenerate triangles.
    out_mesh.indices.clear();
    out_mesh.indices.reserve(index_count);
    uint32_t oob_count = 0;
    uint32_t degen_count = 0;
    for (uint32_t i = 0; i + 2 < index_count; i += 3) {
        uint32_t a = idxs[i], b = idxs[i+1], c = idxs[i+2];
        if (a >= active_voxel_count || b >= active_voxel_count || c >= active_voxel_count) {
            oob_count++;
            continue;
        }
        if (a == b || b == c || a == c) {
            degen_count++;
            continue;
        }
        out_mesh.indices.push_back(static_cast<int>(a));
        out_mesh.indices.push_back(static_cast<int>(b));
        out_mesh.indices.push_back(static_cast<int>(c));
    }

    if (nan_count > 0 || oob_count > 0) {
        UtilityFunctions::push_warning("[GPU Mesh] Sanitized: ", nan_count, " NaN verts, ",
            oob_count, " OOB tris, ", degen_count, " degenerate tris");
    }

    return !out_mesh.indices.empty();
}

// Convenience: full pipeline in one call (prepare → begin → mesh stages → end → submit → sync → readback)
bool DCGpuMeshPipeline::generate_mesh(const DensityBuffers& bufs, int grid_dims,
                                       const float cell_origin[3], float voxel_size,
                                       uint32_t mesh_quality_flags,
                                       DCMeshData& out_mesh) {
    prepare_mesh(grid_dims, bufs);  // buffer updates + uniform sets before compute list
    int64_t cl = rd_->compute_list_begin();
    bool dispatched = dispatch_mesh_stages(cl, bufs, grid_dims, cell_origin, voxel_size, mesh_quality_flags);
    rd_->compute_list_end();
    if (!dispatched) return false;
    rd_->submit();
    rd_->sync();
    return readback_mesh(out_mesh);
}