#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);
}