OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / src / dc_terrain.h

578 lines · c ref:
#ifndef DC_TERRAIN_H
#define DC_TERRAIN_H

#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/classes/array_mesh.hpp>
// MeshInstance3D only used for debug seam visualization
#include <godot_cpp/classes/mesh_instance3d.hpp>
#include <godot_cpp/classes/immediate_mesh.hpp>
#include <godot_cpp/classes/standard_material3d.hpp>
#include <godot_cpp/classes/shader_material.hpp>
#include <godot_cpp/classes/shader.hpp>
#include <godot_cpp/classes/texture2d_array.hpp>
#include <godot_cpp/classes/texture2d.hpp>
#include <godot_cpp/classes/rendering_server.hpp>
#include <godot_cpp/classes/rendering_device.hpp>
#include <godot_cpp/classes/physics_server3d.hpp>
#include <godot_cpp/variant/color.hpp>
#include <godot_cpp/variant/rid.hpp>
#include <godot_cpp/variant/dictionary.hpp>

#include "dc_density.h"
#include "dc_density_grid.h"
#include "dc_contour.h"
#include "dc_generator.h"
#include "dc_generator_noise.h"
#include "dc_terrain_generator.h"
#include "dc_octree_clipmap.h"
#include "dc_seam.h"
#include "dc_worker_cpu.h"
#include "dc_collision_trimesh.h"
#include "dc_collision_sdf.h"
// Removed: dc_vulkan_ext.h, dc_clas_builder.h, dc_lod_dag.h (GPU RT path — re-add later)

// Plane was used for manual frustum culling (removed — Godot handles it via RS)

#include <thread>
#include <atomic>
#include <mutex>
#include <vector>
#include <chrono>
#include <deque>
#include <memory>
#include <functional>
#include <unordered_map>
#include <unordered_set>

namespace godot {

class DCTerrain : public Node3D {
    GDCLASS(DCTerrain, Node3D)

protected:
    static void _bind_methods();

public:
    DCTerrain();
    ~DCTerrain();

    void _ready() override;
    void _process(double delta);
    void _notification(int p_what);

    void rebuild();
    void warmup_shaders();
    void _on_generator_changed();

    // -- DensityConfig properties --
    void set_seed(int p_seed);
    int get_seed() const;

    void set_primary_frequency(float p_val);
    float get_primary_frequency() const;
    void set_primary_strength(float p_val);
    float get_primary_strength() const;

    void set_tunnel_frequency(float p_val);
    float get_tunnel_frequency() const;
    void set_tunnel_strength(float p_val);
    float get_tunnel_strength() const;
    void set_tunnel_radius(float p_val);
    float get_tunnel_radius() const;

    void set_ridge_frequency(float p_val);
    float get_ridge_frequency() const;
    void set_ridge_strength(float p_val);
    float get_ridge_strength() const;

    void set_warp_frequency(float p_val);
    float get_warp_frequency() const;
    void set_warp_strength(float p_val);
    float get_warp_strength() const;

    void set_detail_frequency(float p_val);
    float get_detail_frequency() const;
    void set_detail_strength(float p_val);
    float get_detail_strength() const;

    void set_ground_bias(float p_val);
    float get_ground_bias() const;

    void set_solid_floor_depth(float p_val);
    float get_solid_floor_depth() const;
    void set_sky_height(float p_val);
    float get_sky_height() const;

    void set_flat_center_radius(float p_val);
    float get_flat_center_radius() const;

    void set_voronoi_cell_count(int p_val);
    int get_voronoi_cell_count() const;

    void set_floor_strength(float p_val);
    float get_floor_strength() const;
    void set_floor_height_min(float p_val);
    float get_floor_height_min() const;
    void set_floor_height_max(float p_val);
    float get_floor_height_max() const;
    void set_room_height(float p_val);
    float get_room_height() const;
    void set_terrain_size(float p_val);
    float get_terrain_size() const;

    // -- Terrain-specific properties --
    void set_terrain_color(const Color &p_color);
    Color get_terrain_color() const;

    void set_custom_material(const Ref<Material> &p_material);
    Ref<Material> get_custom_material() const;

    void set_voxel_size(float p_val);
    float get_voxel_size() const;

    void set_octree_depth(int p_val);
    int get_octree_depth() const;

    void set_seamless_lod(bool p_val);
    bool get_seamless_lod() const;

    void set_use_adaptive_qef(bool p_val);
    bool get_use_adaptive_qef() const;
    void set_texture_atlas(const Ref<Texture2DArray> &p_atlas);   // WAD atlas, layer == material_id
    void set_glow_atlas(const Ref<Texture2DArray> &p_atlas);      // PBR emissive glow map
    void set_norm_atlas(const Ref<Texture2DArray> &p_atlas);      // PBR tangent-space normal map
    void set_projection_table(const PackedFloat32Array &p_table); // per-face UV projections (12 floats/entry)
    void set_use_textures(bool p_val);        // sample the atlas instead of flat debug colors
    bool get_use_textures() const;
    void set_adaptive_arch_lod(bool p_val);   // experimental: refine architecture region to 2x
    bool get_adaptive_arch_lod() const;
    void set_arch_min_lod(int p_lod);         // refinement floor when active (-1=2x, -2=4x)
    int  get_arch_min_lod() const;
    void set_use_shortest_diagonal(bool p_val);
    bool get_use_shortest_diagonal() const;
    void set_use_mesh_simplify(bool p_val);
    bool get_use_mesh_simplify() const;

    void set_debug_logging(bool p_val);
    bool get_debug_logging() const;

    void set_debug_seams(bool p_val);
    bool get_debug_seams() const;

    void set_debug_clipmap(bool p_val);
    bool get_debug_clipmap() const;

    // Collision mode: trimesh (physics bodies), SDF (density queries), or both
    enum CollisionMode { COLLISION_TRIMESH = 0, COLLISION_SDF = 1, COLLISION_BOTH = 2 };
    void set_collision_mode(int p_mode);
    int get_collision_mode() const;

    void set_collision_lod_max(int p_val);
    int get_collision_lod_max() const;
    void set_collision_distance(float p_val);
    float get_collision_distance() const;

    // -- Stats (read-only, for HUD) --
    int get_chunk_count() const;
    int get_active_chunk_count() const;
    int get_pending_count() const;
    int get_collision_queue_size() const;
    int get_splits_this_frame() const;

    // -- Density sampling (for spawn placement, etc.) --
    bool is_using_builtin_generator() const { return using_builtin_generator_; }

    // SCALAR QUERY VOLUME. hyper-games #64 NEXT 3 asks for a number instead of
    // a guess about the scalar path, and there was no counter to read.
    //
    // Two entry points cover all of it: sample_density (map scripts -- spawn
    // placement, _lift_out_of_terrain, the fall-respawn) and _eval_with_edits
    // (get_distance, get_surface_normal, get_capsule_push -- the SDF collision
    // family, which only runs when collision_mode is SDF).
    Dictionary get_scalar_query_counts() const;
    void reset_scalar_query_counts();
    float sample_density(const Vector3 &position) const;

    // THE GPU ORACLE. Runs the real density pipeline -- the same shader, the
    // same uniform set, the same buffers the mesher uses -- over a small grid
    // and hands the values back. Returns EMPTY if the GPU refused, which is
    // itself the answer.
    //
    // It exists because every gate written before it either ran outside Godot
    // or inspected GLSL text, and none could tell a correct shader from one
    // that never ran. sample_density() is the CPU twin; comparing the two is
    // the parity check test_erosion_parity.cpp does for the wrapper, applied
    // to the engine.
    //
    // A local RenderingDevice is null under --headless, so this only answers
    // in a real render session.
    PackedFloat32Array debug_gpu_density(const Vector3 &origin, float voxel_size, int cells);

    // ── BAKE A VOLUME TO A MESH, ON THE GPU ─────────────────────────────────
    //
    // Takes the GLSL a proc-nodes ProcVolume carries -- `float proc_density(vec3
    // pos)` -- fills a density grid with it and meshes it, both through the
    // SAME DCGpuThread the terrain already runs. Returns an ArrayMesh, or null
    // if the GPU declined.
    //
    // WHY HERE AND NOT IN proc-nodes. This needs DCGpuThread and a
    // RenderingDevice, which live in dc-terrain/src and cannot move to libdc
    // without making libdc godot-dependent. And a SECOND RenderingDevice is not
    // an option: a second VkDevice bring-up starves and left two meshers
    // running in one world (hyper-games #58). One device, one GPU thread, and
    // this borrows it.
    //
    // The GLSL takes a String rather than a ProcVolume because dc-terrain has
    // no compile-time dependency on proc-nodes' classes -- the same reason
    // DCTerrainGeneratorProcNodes reaches the graph through reflection.
    Ref<ArrayMesh> bake_volume_mesh(const String &p_glsl, const Vector3 &p_origin,
                                    float p_voxel_size, int p_cells);

    // THE COLLISION MESH, as triangles, so it can be drawn.
    //
    // Collision is built through PhysicsServer3D as bare RIDs hung off the
    // clipmap nodes, not as scene-tree CollisionShape3Ds -- so nothing in
    // GDScript could see it, and "is the collision where the terrain is" had no
    // answer but walking into things. This reads the concave shapes back and
    // returns their faces in WORLD space, ready for a debug overlay.
    //
    // Gaps in what comes back are the finding: collision is only built for
    // lod_level <= collision_lod_max (5 by default), so beyond that there is
    // nothing to stand on however solid the terrain looks.
    PackedVector3Array debug_collision_faces(const Vector3 &center, float radius) const;

    // The DRAWN triangles in world space, for comparison against the above.
    PackedVector3Array debug_visual_faces(const Vector3 &center, float radius) const;

    // WHICH BINARY IS THIS. Baked at compile time, so a report can name the
    // artifact it examined instead of assuming the .so matches the source --
    // the rule hyper-games #44 wrote and #45 broke within hours. The .so under
    // addons/ is a symlink into dc-terrain/bin and is not rebuilt by anything
    // automatic, so "I changed the C++" and "the game runs the change" are
    // independent facts.
    String build_stamp() const;

    // The streaming state of the leaf under a point: lod, voxel_size,
    // chunk_size, state, triangles, has_collision, collision_eligible, origin.
    // EMPTY dictionary when no leaf contains the point -- distinct from LOD 0.
    Dictionary debug_chunk_info(const Vector3 &position) const;

    // -- SDF collision API (direct density field queries) --
    float _eval_with_edits(float x, float y, float z) const;
    // Returns signed distance to terrain surface (positive = inside solid, negative = in air).
    // Use for character controller collision instead of mesh-based physics.
    float get_distance(const Vector3 &position) const;
    // Returns surface normal at position (normalized gradient of density field).
    Vector3 get_surface_normal(const Vector3 &position) const;
    // Capsule collision query: returns push-out vector if capsule intersects terrain.
    // Returns zero vector if no collision. Tests bottom, center, and top of capsule.
    Vector3 get_capsule_push(const Vector3 &position, float radius, float height) const;

    // -- Terrain editing --
    // interactive: true = a LIVE in-the-moment carve -> worker PRIORITY lane (instant
    // feedback). false = a replayed/durable-synced edit -> already baked into the edit
    // list, so re-mesh at NORMAL priority and do NOT flood the priority lane (which
    // starves LOD pre-mesh). Default true keeps every direct/legacy caller on the old
    // priority behavior.
    void edit_terrain(const Vector3 &position, float radius, float strength, int op = 0, bool interactive = true, int material_id = 0);
    // persistent = MAP-AUTHORED LANDSCAPE: survives clear_edits(), same as
    // stamp_mesh_arrays. The level shell's MOAT FALLBACK must pass true.
    void edit_terrain_box(const Vector3 &position, const Vector3 &half_extents, float strength,
                          bool interactive = true, int material_id = 0, bool persistent = false);
    void stamp_mesh_obj(const String &obj_path, const Vector3 &position, float scale, float strength, bool interactive = true, int material_id = 0);
    void debug_sdf_at(const Vector3 &position);  // Print SDF values from all edits at this position
    // persistent = MAP-AUTHORED LANDSCAPE: survives clear_edits(). See
    // TerrainEdit::persistent. The level shell subtract must pass true.
    void stamp_mesh_arrays(const PackedVector3Array &vertices, const PackedInt32Array &indices,
                           const Vector3 &position, float scale, float strength, bool interactive = true,
                           int material_id = 0, bool persistent = false);
    // Convex brush = intersection of half-space planes (a Quake brush). planes is a
    // flat [nx,ny,nz,d, ...] (outward normal, inside = n·p <= d). center+radius = the
    // bounding sphere for chunk culling.
    void edit_terrain_convex(const PackedFloat32Array &planes, const Vector3 &center,
                             float radius, float strength, bool interactive = true, int material_id = 0,
                             const PackedInt32Array &face_proj = PackedInt32Array());
    void clear_edits();
    // Drop map-authored landscape only (keeps the player journal), so a map can
    // re-derive its shell at runtime. Does not rebuild — stamp right after.
    void clear_landscape_edits();
    int get_edit_count() const;
    // Interactive brush editing (test mutability)
    int pick_brush(const Vector3 &p) const;
    Vector3 get_brush_center(int idx) const;
    float get_brush_radius(int idx) const;
    PackedVector3Array get_brush_hull_triangles(int idx) const;
    Dictionary lift_brush(int idx);
    void move_brush(int idx, const Vector3 &delta);
    Array get_edits_as_array() const;
    void load_edits_from_array(const Array &edits_array);

    // -- CSG primitives --
    // type: 0=sphere, 1=box. op: 0=union, 1=subtract, 2=intersect.
    // group: primitives in same group are combined, then UNION'd with terrain.
    // size: sphere=[radius,0,0], box=[half_x, half_y, half_z].
    void add_csg_primitive(int type, int op, int group,
                           const Vector3 &position, const Vector3 &size);
    void clear_csg_primitives();
    int get_csg_primitive_count() const;
    void set_csg_primitive_position(int index, const Vector3 &position);
    void set_csg_primitive_size(int index, const Vector3 &size);
    void remove_csg_primitive(int index);

    // -- Noise toggle --
    void set_use_noise(bool p_val);
    bool get_use_noise() const;

    // -- Spherical/planetoid mode --
    void set_spherical(bool p_val);
    bool get_spherical() const;
    void set_floating_islands(bool p_val);
    bool get_floating_islands() const;
    void set_island_radius(float p_val);
    float get_island_radius() const;
    void set_island_depth_ratio(float p_val);
    float get_island_depth_ratio() const;
    void set_island_spacing(float p_val);
    float get_island_spacing() const;
    void set_planet_radius(float p_val);
    float get_planet_radius() const;
    void set_max_noise_octaves(int p_val);
    int get_max_noise_octaves() const;
    void set_max_ridge_octaves(int p_val);
    int get_max_ridge_octaves() const;

    // -- Direct eval mode (Gildea path for all terrain, not just CSG) --
    void set_force_direct_eval(bool p_val);
    bool get_force_direct_eval() const;
    void set_csg_refinement(int p_val);
    int get_csg_refinement() const;

    // -- Generator resource (overrides built-in noise when set) --
    void set_generator(const Ref<DCTerrainGenerator> &p_generator);
    Ref<DCTerrainGenerator> get_generator() const;

    // -- GPU compute pipeline --
    void set_use_gpu(bool p_val);
    bool get_use_gpu() const;

    // -- LOD tuning --
    void set_lod_factor(float p_val);
    float get_lod_factor() const;
    void set_hysteresis(float p_val);
    float get_hysteresis() const;
    void set_fade_speed(float p_val);
    float get_fade_speed() const;
    void set_debug_lod_colors(bool p_val);
    bool get_debug_lod_colors() const;
    void set_debug_wireframe(bool p_val);
    bool get_debug_wireframe() const;
    void set_debug_edits(bool p_val);
    bool get_debug_edits() const;
    void set_normal_perturbation(float p_val);
    float get_normal_perturbation() const;

    // -- Screen-space error LOD --
    void set_screen_split_threshold(float p_val);
    float get_screen_split_threshold() const;
    void set_use_screen_space_error(bool p_val);
    bool get_use_screen_space_error() const;

    // -- LOD mode (Eager Fade vs Blocking Swap) --
    void set_lod_mode(int p_mode);
    int get_lod_mode() const;

    // -- LOD freeze (call during grapple/teleport to prevent thrashing) --
    void set_lod_frozen(bool p_frozen);
    bool get_lod_frozen() const;
    void debug_kick_lod();  // Debug: force one LOD re-traversal (pure invalidate_settled, no freeze/camera move)

private:
    void _upload_cell(CellResult& result);
    void _free_node_rids(OctreeClipmapNode* node);
    void _requeue_affected_chunks(const float center[3], float radius);
    // Shared by both stamp_mesh_* entry points. Was duplicated in each, which is
    // how both missed the hardening edit_terrain_box received. Returns the count
    // re-queued; reports what it had to defer to the post-upload path.
    int _requeue_chunks_for_mesh_stamp(const Vector3 &center, float radius, bool interactive);
    static void _on_node_free(OctreeClipmapNode* node, void* userdata);
    static void _on_mesh_free(OctreeClipmapNode* node, void* userdata);
    static void _on_collision_free(OctreeClipmapNode* node, void* userdata);
    void _propagate_config();
    void _drain_workers();  // poll & discard all in-flight results
    void _update_debug_seams();
    void _generate_seam_for_node(OctreeClipmapNode* node);
    void _upload_seam_mesh(OctreeClipmapNode* node, const DCMeshData& mesh_data);

    // Density engine (pure C++)
    DCDensity density_;
    DensityConfig config_;

    // Generator abstraction — routes evaluate() through DCGenerator interface
    DCGeneratorNoise noise_generator_;      // default noise generator (wraps density_)
    DCGenerator* generator_ptr_ = nullptr;  // active generator pointer
    // Warn-once latch for the built-in-generator substitution below.
    bool warned_generator_fallback_ = false;
    bool using_builtin_generator_ = true;
    // ⚠ HOLDS THE BAKED GLSL ALIVE. DCGpuThread caches the custom_glsl POINTER
    // and compares by pointer, not by string -- so handing it a temporary's
    // c_str() leaves it holding a dangling address, and a later allocation
    // landing there would read as "same shader" and reuse the wrong compiled
    // one. Silently. This member outlives every submit.
    std::string baked_volume_glsl_;
    // mutable + atomic: the query entry points are const and DCCollisionSDF is
    // reachable from more than one thread.
    mutable std::atomic<uint64_t> n_sample_density_{0};
    mutable std::atomic<uint64_t> n_eval_with_edits_{0};
    double scalar_profile_accum_ = 0.0;
    bool scalar_profile_on_ = false;
    void _fallback_to_builtin(const char *where);
    Ref<DCTerrainGenerator> generator_resource_;  // user-assigned Resource (inspector/script)

    // Octree clipmap (replaces DCChunkManager)
    OctreeClipmap octree_clipmap_;

    // Worker threads with dedicated GPU pipelines (replaces DCGpuThread + DCThreadPool)
    std::vector<std::unique_ptr<DCWorkerCPU>> workers_;  // game mode: auto-scaled to CPU cores
    std::unique_ptr<DCGpuThread> gpu_thread_;            // shared GPU density fill thread

    // RT/CLAS removed — re-add after CPU MVP working
    uint32_t frame_counter_ = 0;
    int next_worker_ = 0;  // round-robin dispatch

    // Editor mode: shared single worker across all DCTerrain instances
    // (multiple RenderingDevices crash on some GPUs)
    static std::unique_ptr<DCWorkerCPU> s_editor_worker_;
    static int s_editor_refcount_;
    bool using_editor_worker_ = false;

    // Unified access: returns either per-instance or shared editor workers
    bool _has_workers() const;
    bool _submit_to_worker(const CellRequest& req);
    bool _submit_to_worker_priority(const CellRequest& req);  // edit lane (1b)
    size_t _worker_priority_depth() const;                    // total queued edits across workers (1b)
    void _poll_workers(std::function<void(CellResult&)> handler, double budget_ms, int min_count);
    void _set_worker_generation(uint32_t gen);
    // Spawn the game-mode worker pool. Split out of _notification so the
    // deferred path (waiting on GPU bring-up) can call it from _process.
    void _spawn_workers();
    // Size of the pre-mesh MESH_PENDING backlog as of the last
    // _dispatch_pre_mesh_requests pass. Instrumentation only (FRAME pm=<n>):
    // this backlog drives the dispatch-phase cost, and it has no distance
    // eviction, so it is the number to watch.
    int premesh_pending_last_frame_ = 0;
    bool workers_deferred_ = false;   // workers held back until the GPU is up
    std::chrono::steady_clock::time_point workers_deferred_since_;
    void _drain_all_workers();
    uint32_t generation_ = 0;  // incremented on rebuild — stale results discarded
    float prev_cam_pos_[3] = {0.0f, 0.0f, 0.0f};  // teleport detection

    // Terrain display properties
    Color terrain_color_ = Color(0.3f, 0.5f, 0.25f);
    Ref<Material> custom_material_;  // When set, overrides internal per-LOD materials
    float voxel_size_ = 1.0f;
    int octree_depth_ = 5;
    bool seamless_lod_ = false; // Force all chunks to LOD 0 (no inter-chunk seams)

    // Per-LOD materials (unified ShaderMaterial — same shader for settled + morphing chunks)
    // 16, not 12. This caps the per-LOD material array, and a depth-14 clipmap has
    // LOD levels 0..13; at 12 the coarsest two would share slot 11, breaking the
    // per-LOD fade_t crossfade and the debug LOD colours at exactly the distances
    // a long view exists to show. It does not gate geometry -- octree_depth is
    // unclamped -- so this is about the far LODs looking right, not existing.
    static constexpr int MAX_LOD_MATERIALS = 16;
    Ref<ShaderMaterial> lod_materials_[MAX_LOD_MATERIALS];
    Ref<Texture2DArray> tex_atlas_;      // WAD texture atlas (layer index == material_id)
    Ref<Texture2DArray> glow_atlas_;     // PBR: RGBA — rgb emissive glow, a gloss (→ roughness)
    Ref<Texture2DArray> norm_atlas_;     // PBR: tangent-space normal map
    Ref<Texture2D> proj_table_tex_;      // per-face projection table as an RGBA32F data texture
                                         // (width = 3 texels/entry, height = num entries); sampled
                                         // by the shader via the per-vertex projection index.
    int proj_table_count_ = 0;           // number of projection entries (rows) in proj_table_tex_
    bool use_textures_ = false;          // false = flat debug colors (default), true = atlas
    void _apply_texture_params(const Ref<ShaderMaterial> &mat) const;  // set atlas+toggle on a mat

    // Unified terrain shader (used by ALL chunks at ALL times)
    Ref<Shader> terrain_shader_;
    // Per-chunk ShaderMaterial copies — only for actively-fading chunks (~16-32 at a time).
    // When fade completes, the entry is removed and the chunk returns to the shared lod_materials_.
    std::unordered_map<OctreeClipmapNode*, Ref<ShaderMaterial>> fade_overrides_;

    // Terrain edit list (owned here, snapshot shared via DensityConfig)
    DCEditList edit_list_;

    // Keep ArrayMesh refs alive per-node (prevent GC while RS holds RIDs).
    // Key = OctreeClipmapNode pointer. Cleared when node is freed.
    std::unordered_map<OctreeClipmapNode*, Ref<ArrayMesh>> node_meshes_;
    std::unordered_map<OctreeClipmapNode*, Ref<ArrayMesh>> seam_meshes_;

    // Track freed node pointers to prevent use-after-free when processing
    // worker results. Cleared at start of each frame.
    std::unordered_set<OctreeClipmapNode*> freed_nodes_;

    // Deferred deallocation queue for merged/freed nodes.
    // Stores nodes that need RID cleanup (1 per frame to avoid stutter).
    struct DeallocEntry {
        RID rs_instance;
        RID collision_body;
        RID collision_shape;
        RID seam_mesh_instance;
        Ref<ArrayMesh> mesh;  // prevent GC until RS frees the instance
    };
    std::deque<DeallocEntry> dealloc_queue_;

    // Trimesh collision manager (queued concave polygon shapes)
    DCCollisionTrimesh collision_trimesh_;

    // Collision mode
    CollisionMode collision_mode_ = COLLISION_TRIMESH;

    // Seam mesh generation — nodes waiting for neighbors to become ACTIVE
    struct SeamRequest {
        OctreeClipmapNode* node;
        uint32_t node_generation = 0;
    };
    std::vector<SeamRequest> seam_pending_;

    // LOD tuning
    float lod_factor_ = 3.0f;
    float hysteresis_ = 1.3f;
    float fade_speed_ = 3.0f;
    float normal_perturbation_ = 0.12f;
    bool debug_lod_colors_ = true;
    bool debug_wireframe_ = false;

    // LOD mode
    LodMode lod_mode_ = LOD_BLOCKING_SWAP;

    // BLOCKING_SWAP mode helpers (pre-mesh cache approach)
    void _dispatch_pre_mesh_requests(int regular_dispatched_this_frame);
    void _apply_cached_meshes();

    // LOD freeze — skips octree traversal when true.
    bool lod_frozen_ = false;

    // Mesh quality flags (toggleable for A/B testing)
    bool use_adaptive_qef_ = true;       // Eigenvalue-adaptive QEF regularization
    bool adaptive_arch_lod_ = false;     // experimental crease/architecture sub-LOD (off by default)
    void _update_arch_bounds();          // recompute the architecture AABB from convex edits
    bool use_shortest_diagonal_ = true;  // Aspect-ratio-aware quad splitting
    bool use_mesh_simplify_ = true;      // meshoptimizer post-process on GPU path

    // Debug
    bool debug_logging_ = false;
    bool debug_seams_ = false;
    bool debug_clipmap_ = false;
    bool debug_edits_ = false;
    MeshInstance3D* debug_mesh_node_ = nullptr;
    MeshInstance3D* debug_clipmap_node_ = nullptr;
    MeshInstance3D* debug_edits_node_ = nullptr;
    int debug_frames_ = 0;

    void _update_debug_clipmap();
    void _update_debug_edits();
};

} // namespace godot

#endif // DC_TERRAIN_H