OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / a4408a7

gpu pipeline: allow a generator to sample TEXTURES, not just buffers
_make_uniform_set only ever emitted UNIFORM_BUFFER (binding 4) and STORAGE_BUFFER, so a density generator could bring its own data only as a buffer. GPU Gems 3 ch.1 samples eight 16^3 noise volumes rather than computing noise, and hand-rolling trilinear over a storage buffer turns each of its ~25 hardware fetches into ~8 buffer loads -- which would have made dc-terrain's density several times more expensive than the same field costs a raymarcher, and quietly biased the A/B it exists to measure. Adds a SamplerBinding descriptor (two ids: sampler then texture) alongside the buffer list, so the 20+ existing call sites are untouched. DCCompute owns the volumes and both samplers -- linear for the plain volumes, NEAREST for the packed ones, whose gba channels carry neighbour values and must not be hardware-filtered. Placeholders follow the custom_data precedent: a descriptor in the set must point at a live object even when the shader never reads it, so eight 1^3 volumes exist from _create_buffers onward. The declarations are injected at a GENERATOR_SAMPLER_BINDINGS marker and ONLY when the injected body mentions noiseVol. A declared-but-unused binding can be optimised out of the SPIR-V, after which uniform_set_create is handed a descriptor the shader does not reflect and fails -- so the template and the uniform set have to agree on absence too, not just presence. No behaviour change for existing generators: sdf_uses_noise_ is false, the marker expands to nothing and no sampler descriptors are supplied. Verified by running map_dc_terrain -- all 11 shaders plus edge_intersect compile and the grid allocates exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a4408a75dea5bdcc2c3466621913dd9afc700117
by plankatron <fleeky@gmail.com> · 2026-09-07T22:23:22.000Z

Changed files (7) +181 -5

shaders/edge_intersect.glsl +4 -0
……@@ -154,6 +154,10 @@ layout(set = 0, binding = 10, std430) readonly buffer BrickBuf { float brick_del
154154// reference to it.
155155layout(set = 0, binding = 13, std430) readonly buffer CustomDataBuf { float custom_data[]; };
156156
157// GENERATOR_SAMPLER_BINDINGS
158// Bindings 14..21 (eight 16^3 noise volumes) are injected HERE, and only when
159// the generator body actually samples them -- see _build_edge_intersect_glsl.
160
157161
158162layout(push_constant) uniform PushConstants {
159163 vec3 cell_origin;
src/dc_compute.cpp +70 -0
……@@ -9,6 +9,7 @@
99#include <godot_cpp/classes/rd_uniform.hpp>
1010#include <godot_cpp/classes/rd_texture_format.hpp>
1111#include <godot_cpp/classes/rd_texture_view.hpp>
12#include <godot_cpp/classes/rd_sampler_state.hpp>
1213#include <godot_cpp/variant/utility_functions.hpp>
1314
1415#include <cstring>
……@@ -189,6 +190,9 @@ void DCCompute::cleanup() {
189190 // The custom data outlives stride changes by design (see _destroy_buffers),
190191 // so this is the only place it is released.
191192 if (custom_data_buf_.is_valid()) { rd_->free_rid(custom_data_buf_); custom_data_buf_ = RID(); custom_data_n_ = 0; }
193 _free_noise();
194 if (noise_sampler_linear_.is_valid()) { rd_->free_rid(noise_sampler_linear_); noise_sampler_linear_ = RID(); }
195 if (noise_sampler_nearest_.is_valid()) { rd_->free_rid(noise_sampler_nearest_); noise_sampler_nearest_ = RID(); }
192196 // The atlas pair. Textures are RIDs like any buffer and cleanup() did not
193197 // free them — the first real run reported
194198 // WARNING: 1 RID of type "Texture" was leaked.
……@@ -277,6 +281,8 @@ void DCCompute::_create_buffers(int stride) {
277281 custom_data_buf_ = rd_->storage_buffer_create(4, hz);
278282 }
279283
284 _create_noise_placeholders();
285
280286 // Mesh BVH buffers: start with small empties (resized on first mesh upload)
281287 // Binding 8: BVH nodes (16-byte header + nodes)
282288 PackedByteArray bvh_zero;
……@@ -357,6 +363,70 @@ void DCCompute::_destroy_buffers() {
357363// Upload a baked custom data block. Reallocates, so the uniform set is
358364// invalidated -- a storage buffer cannot be resized in place and the old RID
359365// stays referenced by the set until it is rebuilt.
366// A sampler descriptor, like a buffer descriptor, MUST point at a live object
367// even when the injected SDF body never reads it. Eight 1^3 volumes cost
368// nothing and keep every uniform set valid for generators that use no noise.
369void DCCompute::_create_noise_placeholders() {
370 if (!noise_sampler_linear_.is_valid()) {
371 for (int pass = 0; pass < 2; pass++) {
372 Ref<RDSamplerState> ss; ss.instantiate();
373 RenderingDevice::SamplerFilter f = pass == 0
374 ? RenderingDevice::SAMPLER_FILTER_LINEAR
375 : RenderingDevice::SAMPLER_FILTER_NEAREST;
376 ss->set_min_filter(f);
377 ss->set_mag_filter(f);
378 ss->set_repeat_u(RenderingDevice::SAMPLER_REPEAT_MODE_REPEAT);
379 ss->set_repeat_v(RenderingDevice::SAMPLER_REPEAT_MODE_REPEAT);
380 ss->set_repeat_w(RenderingDevice::SAMPLER_REPEAT_MODE_REPEAT);
381 RID r = rd_->sampler_create(ss);
382 if (pass == 0) noise_sampler_linear_ = r; else noise_sampler_nearest_ = r;
383 }
384 }
385 if (noise_tex_[0].is_valid()) return;
386 Ref<RDTextureFormat> fmt; fmt.instantiate();
387 fmt->set_format(RenderingDevice::DATA_FORMAT_R32G32B32A32_SFLOAT);
388 fmt->set_width(1); fmt->set_height(1); fmt->set_depth(1);
389 fmt->set_array_layers(1); fmt->set_mipmaps(1);
390 fmt->set_texture_type(RenderingDevice::TEXTURE_TYPE_3D);
391 fmt->set_usage_bits(RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT
392 | RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
393 Ref<RDTextureView> view; view.instantiate();
394 PackedByteArray zero; zero.resize(16); memset(zero.ptrw(), 0, 16);
395 TypedArray<PackedByteArray> data; data.push_back(zero);
396 for (int i = 0; i < 8; i++) noise_tex_[i] = rd_->texture_create(fmt, view, data);
397 noise_dim_ = 1;
398}
399
400void DCCompute::_free_noise() {
401 for (int i = 0; i < 8; i++) {
402 if (noise_tex_[i].is_valid()) { rd_->free_rid(noise_tex_[i]); noise_tex_[i] = RID(); }
403 }
404 noise_dim_ = 0;
405}
406
407void DCCompute::set_noise_volumes(const float* const* volumes, int dim) {
408 if (rd_ == nullptr) return;
409 _free_noise();
410 if (volumes == nullptr || dim <= 0) { _create_noise_placeholders(); return; }
411
412 Ref<RDTextureFormat> fmt; fmt.instantiate();
413 fmt->set_format(RenderingDevice::DATA_FORMAT_R32G32B32A32_SFLOAT);
414 fmt->set_width(dim); fmt->set_height(dim); fmt->set_depth(dim);
415 fmt->set_array_layers(1); fmt->set_mipmaps(1);
416 fmt->set_texture_type(RenderingDevice::TEXTURE_TYPE_3D);
417 fmt->set_usage_bits(RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT
418 | RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
419 Ref<RDTextureView> view; view.instantiate();
420 const int64_t n_bytes = int64_t(dim) * dim * dim * 4 * 4;
421 for (int i = 0; i < 8; i++) {
422 PackedByteArray b; b.resize(n_bytes);
423 memcpy(b.ptrw(), volumes[i], size_t(n_bytes));
424 TypedArray<PackedByteArray> data; data.push_back(b);
425 noise_tex_[i] = rd_->texture_create(fmt, view, data);
426 }
427 noise_dim_ = dim;
428}
429
360430void DCCompute::set_custom_data(const float* data, int n) {
361431 if (!rd_) return;
362432 if (custom_data_buf_.is_valid()) { rd_->free_rid(custom_data_buf_); custom_data_buf_ = RID(); }
src/dc_compute.h +17 -0
……@@ -97,6 +97,17 @@ public:
9797 RID edit_buf() const { return edit_buf_; }
9898 RID brick_buf() const { return brick_buf_; }
9999 RID custom_data_buf() const { return custom_data_buf_; }
100
101 // Noise VOLUMES for texture-fetch generators (GPU Gems 3 ch.1 samples eight
102 // 16^3 RGBA volumes instead of computing noise). Placeholders exist from
103 // _create_buffers so the descriptors are always live; set_noise_volumes()
104 // replaces them with real data.
105 RID noise_sampler_linear() const { return noise_sampler_linear_; }
106 RID noise_sampler_nearest() const { return noise_sampler_nearest_; }
107 RID noise_tex(int i) const { return noise_tex_[i]; }
108 // data: 8 volumes of dim^3 RGBA float32, plain 0-3 then packed 4-7.
109 // Pass nullptr to drop back to the placeholders.
110 void set_noise_volumes(const float* const* volumes, int dim);
100111 // Upload a baked custom data block (metres, n*n row-major, row = Z).
101112 // Pass n = 0 to release it back to the one-float placeholder.
102113 void set_custom_data(const float* data, int n);
……@@ -125,6 +136,12 @@ private:
125136 RID edit_buf_;
126137 RID brick_buf_;
127138 RID custom_data_buf_; // baked diffusion custom data; binding 13 in BOTH shaders
139 RID noise_sampler_linear_; // edge_intersect bindings 14..21
140 RID noise_sampler_nearest_;
141 RID noise_tex_[8] = {};
142 int noise_dim_ = 0;
143 void _create_noise_placeholders();
144 void _free_noise();
128145 // ---- ATLAS TRANSPORT for the same data, binding 14 ----------------------
129146 //
130147 // The SSBO above must be re-uploaded WHOLE when the field changes: 22.4 M
src/dc_gpu_mesh_pipeline.cpp +51 -2
……@@ -248,6 +248,29 @@ std::string DCGpuMeshPipeline::_build_edge_intersect_glsl(const char* sdf_body)
248248 body = noise_sdf;
249249 }
250250
251 // Noise volume samplers, only if the body samples them. A declared-but-
252 // unused binding can be optimised away, and then the uniform set supplies a
253 // descriptor the shader does not reflect and creation fails.
254 {
255 const std::string smarker = "// GENERATOR_SAMPLER_BINDINGS";
256 size_t spos = tmpl.find(smarker);
257 if (spos != std::string::npos) {
258 std::string decls;
259 if (body.find("noiseVol") != std::string::npos) {
260 for (int i = 0; i < 4; i++) {
261 decls += "layout(set = 0, binding = " + std::to_string(14 + i)
262 + ") uniform sampler3D noiseVol" + std::to_string(i) + ";\n";
263 }
264 for (int i = 0; i < 4; i++) {
265 decls += "layout(set = 0, binding = " + std::to_string(18 + i)
266 + ") uniform sampler3D packedNoiseVol" + std::to_string(i) + ";\n";
267 }
268 }
269 tmpl.replace(spos, smarker.length(), decls);
270 }
271 }
272 sdf_uses_noise_ = body.find("noiseVol") != std::string::npos;
273
251274 // Replace the marker with the SDF body
252275 const std::string marker = "// GENERATOR_SDF_BODY";
253276 size_t pos = tmpl.find(marker);
……@@ -362,9 +385,24 @@ void DCGpuMeshPipeline::_free_buffers() {
362385// Uniform set helper
363386// -------------------------------------------------------------------------
364387
388// Bindings 14..21 of edge_intersect: the eight noise volumes, plain 0-3 with a
389// linear sampler and packed 4-7 with a nearest one (the packed volumes carry
390// neighbour values in gba and must NOT be hardware-filtered).
391std::vector<SamplerBinding> DCGpuMeshPipeline::_noise_sampler_bindings(
392 const DensityBuffers& bufs) const {
393 std::vector<SamplerBinding> out;
394 if (!sdf_uses_noise_) return out; // template declared none; supply none
395 for (int i = 0; i < 8; i++) {
396 RID smp = (i < 4) ? bufs.noise_sampler_linear : bufs.noise_sampler_nearest;
397 out.push_back({14 + i, smp, bufs.noise_tex[i]});
398 }
399 return out;
400}
401
365402RID DCGpuMeshPipeline::_make_uniform_set(const RID& shader,
366403 const std::vector<std::pair<int, RID>>& bindings,
367 bool is_ubo_binding_4) {
404 bool is_ubo_binding_4,
405 const std::vector<SamplerBinding>& samplers) {
368406 TypedArray<RDUniform> uniforms;
369407 for (auto& [binding, rid] : bindings) {
370408 Ref<RDUniform> u;
……@@ -379,6 +417,17 @@ RID DCGpuMeshPipeline::_make_uniform_set(const RID& shader,
379417 u->add_id(rid);
380418 uniforms.push_back(u);
381419 }
420 // Combined image samplers. Order matters: sampler id FIRST, then texture.
421 for (auto& sb : samplers) {
422 if (!sb.sampler.is_valid() || !sb.texture.is_valid()) continue;
423 Ref<RDUniform> u;
424 u.instantiate();
425 u->set_uniform_type(RenderingDevice::UNIFORM_TYPE_SAMPLER_WITH_TEXTURE);
426 u->set_binding(sb.binding);
427 u->add_id(sb.sampler);
428 u->add_id(sb.texture);
429 uniforms.push_back(u);
430 }
382431 return rd_->uniform_set_create(uniforms, shader, 0);
383432}
384433
……@@ -496,7 +545,7 @@ void DCGpuMeshPipeline::_build_uniform_sets(const DensityBuffers& bufs) {
496545 {4, bufs.params}, {5, bufs.voronoi}, {6, bufs.perm}, {7, bufs.edit},
497546 {8, active_edges_buf_}, {9, counts_buf_}, {10, bufs.brick},
498547 {11, bufs.convex_planes}, {13, bufs.custom_data}
499 }, true);
548 }, true, _noise_sampler_bindings(bufs));
500549
501550 // Stage 7: voxel_classify
502551 us_voxel_classify_ = _make_uniform_set(sp_voxel_classify_.shader, {
src/dc_gpu_mesh_pipeline.h +27 -1
……@@ -35,6 +35,26 @@ struct DensityBuffers {
3535 RID convex_planes; // edge_intersect binding 11: convex-brush half-space planes
3636 RID convex_faceproj; // bake_planeset binding 12: per-face projection index (parallel to convex_planes)
3737 RID custom_data; // binding 13 in BOTH density_fill and edge_intersect: baked custom data
38
39 // Noise VOLUMES for texture-fetch generators (GPU Gems 3 ch.1 samples eight
40 // 16^3 volumes rather than computing noise). Bound as SAMPLER_WITH_TEXTURE
41 // at bindings 14..21 of edge_intersect, which unlike a buffer descriptor
42 // takes TWO ids: the sampler, then the texture.
43 //
44 // Like custom_data, these must point at LIVE objects even when the injected
45 // SDF body never reads them -- a descriptor in the set cannot be invalid --
46 // so DCCompute creates 1^3 placeholders when no generator supplies volumes.
47 RID noise_sampler_linear; // NLQ/NMQ: filter_linear, repeat
48 RID noise_sampler_nearest; // NHQ: filter_nearest, repeat (packed volumes)
49 RID noise_tex[8]; // 0-3 plain, 4-7 packed
50};
51
52// A combined image sampler binding. Kept separate from the buffer bindings
53// because it needs two ids and a different uniform type.
54struct SamplerBinding {
55 int binding;
56 RID sampler;
57 RID texture;
3858};
3959
4060class DCGpuMeshPipeline {
……@@ -151,9 +171,15 @@ private:
151171 void _prefix_scan(int64_t compute_list, RID input_buf, RID output_buf, int N);
152172
153173 // Create a uniform set for a specific dispatch stage
174 // True when the compiled edge_intersect declares bindings 14..21, i.e. the
175 // injected SDF body samples the noise volumes.
176 bool sdf_uses_noise_ = false;
177 std::vector<SamplerBinding> _noise_sampler_bindings(const DensityBuffers& bufs) const;
178
154179 RID _make_uniform_set(const RID& shader,
155180 const std::vector<std::pair<int, RID>>& bindings,
156 bool is_ubo_binding_4 = false);
181 bool is_ubo_binding_4 = false,
182 const std::vector<SamplerBinding>& samplers = {});
157183};
158184
159185#endif // DC_GPU_MESH_PIPELINE_H
src/dc_gpu_thread.h +3 -0
……@@ -407,6 +407,9 @@ private:
407407 bufs.bvh_tris = compute_.bvh_tri_buf();
408408 bufs.bvh_indices = compute_.bvh_idx_buf();
409409 bufs.custom_data = compute_.custom_data_buf();
410 bufs.noise_sampler_linear = compute_.noise_sampler_linear();
411 bufs.noise_sampler_nearest = compute_.noise_sampler_nearest();
412 for (int ni = 0; ni < 8; ni++) bufs.noise_tex[ni] = compute_.noise_tex(ni);
410413 mesh_pipeline_.prepare_mesh(grid_dims, bufs);
411414
412415 // Single compute list: density + mesh pipeline
src/dc_terrain_generator_procnodes.cpp +9 -2
……@@ -1548,10 +1548,17 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
15481548 + String::num(submarine_buffer_end_, 3) + ", sea_depth);\n"
15491549 " if (sub_w > 0.0) {\n"
15501550 " float sub_f = 1.0 / " + String::num(submarine_wavelength_, 4) + ";\n"
1551 // AMPLITUDE IS CAPPED BY THE WATER OVER IT. A 1500 m feature in 400 m
1552 // of water pushes the seabed straight through the surface and puts an
1553 // island where there should be a reef. Half the local depth is the
1554 // most that can be added without breaching, and it makes the caves
1555 // scale with the basin they are in -- big ones offshore, gentle relief
1556 // in the shallows, which is what a real seabed does.
1557 " float sub_a = min(" + String::num(submarine_amp_, 4)
1558 + ", max(sea_depth, 0.0) * 0.5);\n"
15511559 " d += ps_fbm_noise_3d(x * sub_f, y * sub_f, z * sub_f, "
15521560 + String::num_int64(submarine_octaves_) + ", "
1553 + String::num(submarine_roughness_, 4) + ", 2.0) * "
1554 + String::num(submarine_amp_, 4) + " * sub_w;\n"
1561 + String::num(submarine_roughness_, 4) + ", 2.0) * sub_a * sub_w;\n"
15551562 " }\n" : String())
15561563 + String(" // Clip: solid below floor_y, air above ceiling_y.\n"
15571564 " d = max(d, (") + floor_str + " - y) * 2.0);\n"