| … | … | @@ -1,6 +1,8 @@ |
| 1 | 1 | #ifndef DC_GENERATOR_H |
| 2 | 2 | #define DC_GENERATOR_H |
| 3 | 3 | |
| 4 | #include <cstdint> | |
| 5 | ||
| 4 | 6 | #include <cmath> |
| 5 | 7 | |
| 6 | 8 | // Abstract base class for density/SDF evaluation. |
| … | … | @@ -57,6 +59,18 @@ public: |
| 57 | 59 | // moving one alone gives either a truncated buffer or an attempt to |
| 58 | 60 | // allocate count² floats, which is 7.9 TB for a 1026 tile and a segfault. |
| 59 | 61 | virtual int gpu_custom_data_count() const { return 0; } |
| 62 | // A CONTENT VERSION, because the STABLE-pointer contract above cannot | |
| 63 | // express mutation. | |
| 64 | // | |
| 65 | // The GPU thread re-uploads when the POINTER changes. That is correct for a | |
| 66 | // buffer that is only ever replaced wholesale, and WRONG the moment a | |
| 67 | // generator can grow its data in place: a std::vector assigned to within | |
| 68 | // its existing capacity keeps the same data() address, so the new contents | |
| 69 | // never reach the GPU and every log line still reads healthy. A tile | |
| 70 | // fetched from a peer mid-session is exactly that case. | |
| 71 | // | |
| 72 | // Bump this whenever the bytes change, whatever the pointer does. | |
| 73 | virtual uint32_t gpu_custom_data_version() const { return 0; } | |
| 60 | 74 | |
| 61 | 75 | // Optional: bounding box hint for grid allocation. |
| 62 | 76 | // Writes axis-aligned min/max into min[3]/max[3]. Returns false if unbounded. |
| … | … | @@ -40,6 +40,7 @@ struct GpuFillRequest { |
| 40 | 40 | const char* custom_glsl = nullptr; // optional: proc-nodes compiled shader |
| 41 | 41 | const float* custom_data = nullptr; // optional: baked custom data (binding 13) |
| 42 | 42 | int custom_data_n = 0; |
| 43 | uint32_t custom_data_version = 0; // content version; see DCGenerator | |
| 43 | 44 | const std::vector<TerrainEdit>* edits = nullptr; // terrain edits for GPU evaluation |
| 44 | 45 | bool wants_gpu_mesh = false; // request GPU mesh generation (skip CPU DC) |
| 45 | 46 | DCMeshData* mesh_output = nullptr; // where to write GPU-generated mesh |
| … | … | @@ -101,7 +102,8 @@ public: |
| 101 | 102 | DCMeshData* mesh_output = nullptr, |
| 102 | 103 | const float* cell_origin = nullptr, float voxel_size = 0.0f, |
| 103 | 104 | uint32_t mesh_quality_flags = 0, |
| 104 | const float* custom_data = nullptr, int custom_data_n = 0) { | |
| 105 | const float* custom_data = nullptr, int custom_data_n = 0, | |
| 106 | uint32_t custom_data_version = 0) { | |
| 105 | 107 | if (!running_.load() || !gpu_available_.load()) { |
| 106 | 108 | return false; |
| 107 | 109 | } |
| … | … | @@ -117,6 +119,7 @@ public: |
| 117 | 119 | req.custom_glsl = custom_glsl; |
| 118 | 120 | req.custom_data = custom_data; |
| 119 | 121 | req.custom_data_n = custom_data_n; |
| 122 | req.custom_data_version = custom_data_version; | |
| 120 | 123 | req.edits = edits; |
| 121 | 124 | if (mesh_output && cell_origin) { |
| 122 | 125 | req.wants_gpu_mesh = true; |
| … | … | @@ -155,6 +158,7 @@ private: |
| 155 | 158 | bool mesh_pipeline_available_ = false; |
| 156 | 159 | const char* current_custom_glsl_ = nullptr; // track to avoid recompiling same shader |
| 157 | 160 | const float* current_custom_data_ = nullptr; // same trick: re-upload only on change |
| 161 | uint32_t current_custom_data_version_ = 0; // ...and on content version, for growth | |
| 158 | 162 | |
| 159 | 163 | void _run() { |
| 160 | 164 | // Create the RenderingDevice on THIS thread (thread affinity). |
| … | … | @@ -270,15 +274,25 @@ private: |
| 270 | 274 | // A new shader means a new uniform set, so force the custom data to |
| 271 | 275 | // be re-uploaded even if the generator handed back the same pointer. |
| 272 | 276 | current_custom_data_ = nullptr; |
| 277 | current_custom_data_version_ = 0u; | |
| 273 | 278 | } |
| 274 | 279 | |
| 275 | 280 | // Baked custom data, same change-detection contract as the shader above. |
| 276 | 281 | // Uploading reallocates the buffer and dirties the uniform set, so doing |
| 277 | 282 | // it per chunk would rebuild the set thousands of times a second. |
| 283 | // | |
| 284 | // POINTER *OR* VERSION. The pointer alone was right while custom data | |
| 285 | // was only ever replaced wholesale; it is wrong for a generator that | |
| 286 | // GROWS its buffer, because a std::vector written to within its | |
| 287 | // existing capacity keeps the same data() address and the upload is | |
| 288 | // silently skipped -- new terrain on disk, old terrain on screen, no | |
| 289 | // error anywhere. A tile arriving from a peer mid-session is that case. | |
| 278 | 290 | const float* hb = batch[0].custom_data; |
| 279 | if (hb != current_custom_data_) { | |
| 291 | const uint32_t hv = batch[0].custom_data_version; | |
| 292 | if (hb != current_custom_data_ || hv != current_custom_data_version_) { | |
| 280 | 293 | compute_.set_custom_data(hb, batch[0].custom_data_n); |
| 281 | 294 | current_custom_data_ = hb; |
| 295 | current_custom_data_version_ = hv; | |
| 282 | 296 | } |
| 283 | 297 | |
| 284 | 298 | // Process each request INDIVIDUALLY with ITS OWN culled edit list. |
| … | … | @@ -3043,9 +3043,10 @@ PackedFloat32Array DCTerrain::debug_gpu_density(const Vector3 &origin, float vox |
| 3043 | 3043 | ? generator_ptr_->gpu_shader_source() : nullptr; |
| 3044 | 3044 | const float* hb = generator_ptr_->gpu_custom_data(); |
| 3045 | 3045 | int hbn = generator_ptr_->gpu_custom_data_count(); |
| 3046 | uint32_t hbv = generator_ptr_->gpu_custom_data_version(); | |
| 3046 | 3047 | |
| 3047 | 3048 | if (!gpu_thread_->submit(grid, density_, glsl, nullptr, nullptr, |
| 3048 | nullptr, 0.0f, 0u, hb, hbn)) { | |
| 3049 | nullptr, 0.0f, 0u, hb, hbn, hbv)) { | |
| 3049 | 3050 | return out; // empty: the GPU declined, and that is the finding |
| 3050 | 3051 | } |
| 3051 | 3052 | out.resize((int64_t)grid.data.size()); |
| … | … | @@ -442,7 +442,7 @@ void DCTerrainGeneratorProcNodes::set_height_space_sqrt(bool p_val) { |
| 442 | 442 | // alternative was a Godot Image import path, and the field has to be readable |
| 443 | 443 | // identically by the standalone harness that gates it, which has no engine. |
| 444 | 444 | bool DCTerrainGeneratorProcNodes::_load_heightfield(const String &p_path) { |
| 445 | hblk_.clear(); hblk_n_ = 0; | |
| 445 | hblk_.clear(); hblk_n_ = 0; hblk_version_++; | |
| 446 | 446 | if (p_path.is_empty()) return true; |
| 447 | 447 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); |
| 448 | 448 | if (f.is_null()) { |
| … | … | @@ -474,6 +474,7 @@ bool DCTerrainGeneratorProcNodes::_load_heightfield(const String &p_path) { |
| 474 | 474 | hblk_[i] = (float)v; |
| 475 | 475 | } |
| 476 | 476 | hblk_n_ = (int)n; hblk_mpp_ = mpp; hblk_ox_ = ox; hblk_oz_ = oz; |
| 477 | hblk_version_++; | |
| 477 | 478 | UtilityFunctions::print("[DCTerrainGeneratorProcNodes] heightfield ", p_path, |
| 478 | 479 | ": ", (int)n, "x", (int)n, " @ ", mpp, " m/px, origin x ", ox, " z ", oz); |
| 479 | 480 | return true; |
| … | … | @@ -539,6 +540,7 @@ void DCTerrainGeneratorProcNodes::_collect_node_data() { |
| 539 | 540 | PackedFloat32Array d = node_obj->call("get_gpu_data"); |
| 540 | 541 | if (d.is_empty()) continue; |
| 541 | 542 | hblk_.assign(d.ptr(), d.ptr() + d.size()); |
| 543 | hblk_version_++; | |
| 542 | 544 | // ASK THE NODE FOR THE SIDE LENGTH; DO NOT DERIVE IT FROM THE ARRAY. |
| 543 | 545 | // |
| 544 | 546 | // get_gpu_data() returns the MIP PYRAMID, not one square: 1,403,226 |
| … | … | @@ -1437,6 +1439,14 @@ const char *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_shader_source() co |
| 1437 | 1439 | return owner_->cached_glsl_.empty() ? nullptr : owner_->cached_glsl_.c_str(); |
| 1438 | 1440 | } |
| 1439 | 1441 | |
| 1442 | // The CONTENT version. hblk_ is written by two producers and, once tiles can | |
| 1443 | // arrive at runtime, by growth in place -- and std::vector::assign into spare | |
| 1444 | // capacity keeps data() unchanged, so the pointer cannot be the signal. Every | |
| 1445 | // site that touches hblk_ bumps this. | |
| 1446 | uint32_t DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data_version() const { | |
| 1447 | return owner_->hblk_version_; | |
| 1448 | } | |
| 1449 | ||
| 1440 | 1450 | const float *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data() const { |
| 1441 | 1451 | return owner_->hblk_.empty() ? nullptr : owner_->hblk_.data(); |
| 1442 | 1452 | } |
| … | … | @@ -243,6 +243,8 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator { |
| 243 | 243 | // Cached GLSL shader source (adapter-wrapped for dc-terrain). |
| 244 | 244 | // Compiled eagerly on main thread — worker only reads cache. |
| 245 | 245 | std::string cached_glsl_; |
| 246 | // Bumped on every hblk_ mutation. See ProcNodesBridge::gpu_custom_data_version. | |
| 247 | uint32_t hblk_version_ = 0; | |
| 246 | 248 | |
| 247 | 249 | public: |
| 248 | 250 | // The ASSEMBLED shader string, exactly as handed to the compute pipeline. |
| … | … | @@ -281,6 +283,7 @@ private: |
| 281 | 283 | const char *gpu_shader_source() const override; |
| 282 | 284 | const float *gpu_custom_data() const override; |
| 283 | 285 | int gpu_custom_data_count() const override; |
| 286 | uint32_t gpu_custom_data_version() const override; | |
| 284 | 287 | }; |
| 285 | 288 | ProcNodesBridge bridge_{ this }; |
| 286 | 289 |
| … | … | @@ -236,10 +236,12 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us |
| 236 | 236 | const char* custom_glsl = nullptr; |
| 237 | 237 | const float* custom_data = nullptr; |
| 238 | 238 | int custom_data_n = 0; |
| 239 | uint32_t custom_data_version = 0; | |
| 239 | 240 | if (request.generator && request.generator->has_gpu_shader()) { |
| 240 | 241 | custom_glsl = request.generator->gpu_shader_source(); |
| 241 | 242 | custom_data = request.generator->gpu_custom_data(); |
| 242 | 243 | custom_data_n = request.generator->gpu_custom_data_count(); |
| 244 | custom_data_version = request.generator->gpu_custom_data_version(); | |
| 243 | 245 | } |
| 244 | 246 | const std::vector<TerrainEdit>* gpu_edit_ptr = |
| 245 | 247 | (request.edits && !request.edits->empty()) ? request.edits.get() : nullptr; |
| … | … | @@ -247,7 +249,7 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us |
| 247 | 249 | gpu_filled = gpu_thread_->submit(grid, *request.density, custom_glsl, gpu_edit_ptr, |
| 248 | 250 | &gpu_mesh, request.cell_origin, request.voxel_size, |
| 249 | 251 | (request.use_adaptive_qef ? 2u : 0u) | (request.use_shortest_diagonal ? 1u : 0u), |
| 250 | custom_data, custom_data_n); | |
| 252 | custom_data, custom_data_n, custom_data_version); | |
| 251 | 253 | if (!gpu_mesh.indices.empty()) { |
| 252 | 254 | gpu_meshed = true; |
| 253 | 255 | } |