// VoxelWorld.h // The main world manager - orchestrates chunk loading, generation, meshing, and rendering #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "HAL/CriticalSection.h" #include #include "VoxelTypes.h" #include "VoxelGenerator.h" #include "VoxelMarchingCubesMesher.h" #include "VoxelSettings.h" #include "VoxelStrateManager.h" #include "VoxelDiffLayer.h" #include "VoxelWorld.generated.h" // Forward declaration class URealtimeMeshComponent; class URealtimeMesh; class URealtimeMeshSimple; class UBodySetup; class APawn; class UPawnMovementComponent; class UCharacterMovementComponent; class UVoxelDiffLayer; class UVoxelContentManager; class UVoxelAtmosphereManager; class UVoxelDensityVolume; class UMaterialParameterCollection; class UVolumeTexture; class UMaterialInterface; class UMaterialInstanceDynamic; class FScopedGenerationPause; class ACameraActor; namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers class FVoxelForgeCoreRuntimeState; struct FVoxelForgeCoreTileHashRecord { uint64 Density = 0; uint64 Mesh = 0; uint64 Payload = 0; uint64 Topology = 0; int32 Triangles = 0; bool bEmpty = true; bool bHasHashes = false; }; /** Per-request cancellation state. The game thread flips the flag when a pending tile leaves the * desired set; workers only read it and return an aborted result through the normal MPSC queue. */ struct FVoxelTileCancellationState { std::atomic bObsolete{false}; }; /** * How a streaming anchor wants its tiles built (ARCHITECTURE §9.3). The local player is an implicit * FullVisual anchor (the clipmap). Extra anchors — AI, and later remote players — are registered so * their surroundings stream too (so they physically exist away from the local camera). * * `CollisionOnly` tiles cook collision but are hidden (`SetVisibility(false)`) — no draw / VSM cost — * UNLESS the player clipmap also wants that exact level-0 tile (then it renders normally). This is the * §9.4 render-skip: it kills the cost of terrain around AI/remote players far from the local camera. * (Geometry streams are still built on the worker — off the frame; a deeper "no stream build at all" * collision-only path is a later optimization.) */ UENUM(BlueprintType) enum class EVoxelAnchorPolicy : uint8 { CollisionOnly UMETA(DisplayName = "Collision Only"), // enough to stand / pathfind / hit FullVisual UMETA(DisplayName = "Full Visual") // rendered too (a second local viewpoint) }; /** Versioned Core mesh recipe. Hybrid requires tagged sharp plan features and affects only Core meshes. * Recette de maillage Core versionnee. Hybrid exige des fonctions planaires balisees et ne touche qu'au Core. */ UENUM(BlueprintType) enum class EVoxelForgeCoreMesherRecipe : uint8 { MarchingCubesV1 UMETA(DisplayName = "Marching Cubes V1"), WorldAssemblerHybridV1 UMETA(DisplayName = "World Assembler Hybrid V1"), WorldAssemblerHybridV2 UMETA(DisplayName = "World Assembler Hybrid V2") }; /** A non-player streaming center: keep a small box of level-0 tiles loaded around Actor so it has * collision wherever it is. Plain internal struct (weak ptr is GC-safe without reflection). */ struct FVoxelStreamingAnchor { TWeakObjectPtr Actor; EVoxelAnchorPolicy Policy = EVoxelAnchorPolicy::CollisionOnly; // Thin box around the anchor (level-0 chunks). Default = the chunk it's in + 1 on each horizontal // edge (3×3) + 1 chunk BELOW (ground safety when its capsule sits near a chunk's bottom); nothing // above (a grounded NPC doesn't need it — raise ZAbove for flyers). Empty tiles in the box are ~free. int32 XYRadiusChunks = 1; // horizontal Chebyshev radius (0 = its own column) int32 ZBelowChunks = 1; // chunks below the anchor's chunk int32 ZAboveChunks = 0; // chunks above FIntVector LastChunk = FIntVector(MAX_int32, MAX_int32, MAX_int32); // move detection (rebuild trigger) }; /** * AVoxelWorld - The main voxel terrain actor * * RESPONSIBILITIES: * - Track which chunks should be loaded based on player position * - Create/destroy chunks as player moves * - Coordinate generation and meshing * - Manage mesh components for rendering * * LIFECYCLE: * - BeginPlay: Initialize generator, mesher * - Tick: Update chunks around player position */ enum class EVoxelTileWorkPriority : uint8 { Horizon = 0, LOD0 = 1, Edited = 2, CollisionCritical = 3 }; struct FChunkResult { FVoxelTileKey Tile; // which clipmap tile this mesh is for (carries coord + level) // T1.f: the RMC geometry buffers are BUILT ON THE WORKER (BuildTileStreamSet in the gen task) // so the game thread only uploads them — the per-vertex builder loop was the dominant // game-thread cost while moving (the apply drain). TSharedPtr (not a by-value StreamSet) so // FChunkResult stays movable through the MPSC queue with the type only FORWARD-DECLARED here. // Null ⇒ no-surface tile (uniform all-air/all-solid or an empty content band; no component). TSharedPtr Streams; uint32 Epoch = 0; // Generation epoch — discard if stale // Monotonic request timestamp used for the low-overhead streaming latency summary and the // opt-in profile. It is carried through the worker queue so the report can separate scheduler // wait from generation time. uint64 RequestStartCycles = 0; // Full background-task boundaries include stream conversion and result packaging around the // generator call; they let the trace reconcile per-tile worker-seconds with worker occupancy. // Les bornes de tâche incluent la conversion et la préparation du résultat autour du générateur. uint64 WorkerTaskStartCycles = 0; uint64 WorkerTaskEndCycles = 0; // Operating-system thread id identifies the physical worker slot used by this tile. // L'identifiant du thread systeme repere le slot worker utilise par cette tuile. uint32 WorkerThreadId = 0; // Worker timing boundaries for separating scheduler wait from generation and result/apply wait. // These are captured with low overhead for the clean streaming-latency summary; expensive // per-tile profiling remains opt-in. uint64 GenerationStartCycles = 0; uint64 GenerationEndCycles = 0; uint64 ApplyStartCycles = 0; EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon; // Ordinals make admitted order and actual worker start order auditable across MPSC results. // Les numeros d'ordre rendent auditables l'admission et le demarrage des workers via MPSC. uint64 RequestOrdinal = 0; uint64 GenerationOrdinal = 0; bool bResidentAtRequest = false; // Step-1-only tag captured on the game thread for the pawn's bounded support cohort. bool bStep1SupportCohort = false; int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0; int32 NumSkirtTriangles = 0; int32 NumVertices = 0; uint64 GeometryBytes = 0; bool bSealedSolidProof = false; // true when the cheap gap/seal proof skipped meshing bool bOutOfLayoutAirProof = false; // true when the exact constant-air guard skipped meshing // Clean, low-overhead work counters. These are carried with the result so the worker can // count exact validation and mesher density calls locally, then publish one aggregate update. int64 ValidationDensityCalls = 0; int64 MesherDensityCalls = 0; bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded bool bObsolete = false; // Worker observed per-tile desired-set cancellation uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time uint32 ModificationEpoch = 0; // Diff-layer revision captured at request time bool bRecordStreamingLatency = true; bool bEmpty = true; // true ⇒ no surface (uniform all-air/all-solid or empty band); still loaded // F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the // column's CeilSurf; overhangs/cave roofs stay ground) and packs them as two contiguous runs // (ground then cap) in the index buffer → polygroups 0/1 → two RMC sections with their own // material + shadow flag. These tell the apply path which sections exist (RMC only creates a // section for a non-empty polygroup — configuring a missing one is invalid). bool bHasGroundTris = false; bool bHasCeilingTris = false; uint64 CoreDensityHash = 0; uint64 CoreMeshHash = 0; uint64 CorePayloadHash = 0; uint64 CoreTopologyHash = 0; bool bHasCoreHashes = false; // STRATE CONTENT CUT — the chunk-Z band this tile was MESHED with (MIN/MAX = uncut). The // apply path clamps its strate/material lookups into it (a coarse tile's raw min corner can // sit in a strate whose content was cut out of the mesh entirely). int32 BandChunkLo = MIN_int32; int32 BandChunkHi = MAX_int32; // CAPTURE-DURING-MESHING: the tile's CHUNK_SIZE³ R8 density grid, captured by the mesher (no extra // GetDensityAt). Non-empty only for capture-eligible tiles (level 0, full-res). The game thread hands // it to UVoxelDensityVolume::IngestTileCapture so the density clipmap reuses the mesher's samples // instead of re-sampling. Moved (not copied) through the MPSC queue. See UVoxelDensityVolume. TArray CaptureGrid; // Exact pre-edit level-0 lattice for the bounded local-player dig cache. Sampled on the worker // with the tile's normal generator context and moved through the result queue. FVoxelSharedDensityGrid PreEditDensityGrid; // Sparse points on the prior surface, used only by the explicit rendered-coverage audit. // Captured from the mesher output and kept out of normal streaming results. bool bCaptureSurfaceWitnesses = false; TArray SurfaceWitnesses; }; /** One worker-produced mesh identity emitted only by the explicit parity harness. */ struct FVoxelTileHashDumpRecord { FVoxelTileKey Tile; FIntVector OriginVoxels = FIntVector::ZeroValue; int32 Step = 1; int32 Cells = 0; int32 BandChunkLo = MIN_int32; int32 BandChunkHi = MAX_int32; bool bEmpty = true; int32 NumTriangles = 0; FString GeometryHash; }; /** Resident CPU geometry payload associated with one currently loaded tile. */ struct FLoadedTileGeometryStats { uint64 Vertices = 0; uint64 Triangles = 0; uint64 GeometryBytes = 0; }; UCLASS() class VOXELFORGE_API AVoxelWorld : public AActor { GENERATED_BODY() public: AVoxelWorld(); //========================================================================= // SETTINGS //========================================================================= UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World") UVoxelSettings* Settings; //========================================================================= // COMPONENTS & REFERENCES //========================================================================= /** The terrain generator */ UPROPERTY() UVoxelGenerator* Generator; /** The mesh builder */ UPROPERTY() UVoxelMarchingCubesMesher* Mesher; /** The strate manager — maps depth to strate definitions */ UPROPERTY() UVoxelStrateManager* StrateManager; /** Player terrain modifications (carving & filling). * Stores density diffs on top of procedural terrain. * Created in BeginPlay, passed to Generator for density evaluation. */ UPROPERTY() UVoxelDiffLayer* DiffLayer; /** Spawns per-chunk decorations/actors from the strate content pools and the * aesthetic water surfaces. Created in BeginPlay. */ UPROPERTY() UVoxelContentManager* ContentManager; /** Drives per-strate fog + ambient + persistent ceiling/floor layer actors * (e.g. seas of clouds) based on the strate the player is in. Created in BeginPlay * when bManageAtmosphere is true. */ UPROPERTY() UVoxelAtmosphereManager* AtmosphereManager; /** Optional player-centred density CLIPMAP for mini-sun raymarched shadows. It is absent when * Settings->bEnableDensityVolume is false, so no fill thread or texture uploads run. */ UPROPERTY() UVoxelDensityVolume* DensityVolume; /** Shared Material Instance Dynamics that bind the density-volume textures + per-frame shadow params * (clipmap transform + nearest orb) onto the terrain material(s). Keyed by BASE material so every * tile of a given base shares ONE MID (no batching cost). Created lazily in ApplyMeshToTile; * refreshed each Tick only while DensityVolume exists. When disabled, each MID receives neutral * VolP* values and null VolTex* bindings at creation. */ UPROPERTY() TMap, TObjectPtr> TerrainMIDs; /** When true, VoxelForge spawns & drives its own height fog + skylight + ceiling/floor * layer actors from each strate's settings. Turn OFF if you manage fog/lighting * yourself in the level (avoids a duplicate ExponentialHeightFog). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World") bool bManageAtmosphere = true; /** Route terrain generation, streaming and edits through Standalone Core (opt-in). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Use VoxelForge Core")) bool bUseVoxelForgeCore = false; /** Core-only, versioned mesher selection. Default keeps the existing Core hashes and geometry. * Recette reservee au Core; la valeur par defaut conserve les hachages et la geometrie actuels. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Core Mesher Recipe")) EVoxelForgeCoreMesherRecipe CoreMesherRecipe = EVoxelForgeCoreMesherRecipe::MarchingCubesV1; /** Physical Core voxel size in centimeters. Tile coordinates remain 32 voxels per tile. * Taille physique d'un voxel Core en centimetres; chaque tuile contient 32 voxels. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Core Voxel Size (cm)", ClampMin = "12.5", ClampMax = "25.0")) float CoreVoxelSizeCm = 25.0f; /** Core-only clipmap radius, in level-0 Core tiles. Does not read Settings->ClipRadius. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "1", ClampMax = "64")) int32 CoreClipRadius = 3; /** Core-only maximum clip level. Does not read Settings->MaxClipLevel. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "0", ClampMax = "8")) int32 CoreMaxClipLevel = 8; /** Commandlet-only identity capture; normal Core streaming leaves Core's hash path disabled. */ bool bCaptureVoxelForgeCoreHashesForValidation = false; TMap VoxelForgeCoreTileHashes; uint64 LastVoxelForgeCoreEditDigest = 0; uint64 LastVoxelForgeCoreEditSequence = 0; TArray LastVoxelForgeCoreAffectedTiles; //========================================================================= // CHUNK STORAGE //========================================================================= //========================================================================= // CLIPMAP TILE STORAGE (chunked-LOD) //========================================================================= // A level-L tile spans (CHUNK_SIZE< LoadedTiles; /** Level-0 tiles whose latest submitted RMC collision body has finished cooking and was * installed. This is the authoritative collision-readiness set used by the pawn gate; mesh * submission and LoadedTiles are intentionally not sufficient. */ TSet CollisionReadyTiles; /** A carve intersected the pawn's current support tile. Its previous body is withdrawn * immediately and readiness returns only after the replacement cook is installed. */ TSet CollisionSuppressedUntilCook; TMap CollisionSuppressedAtCycles; /** Level-0 tiles whose applied result is known to contain no surface (uniform empty/solid). * They need no BodySetup themselves, but the pawn gate may walk past them while looking for * the first resolved collidable support tile. */ TSet CollisionNotRequiredTiles; /** Subset of CollisionNotRequiredTiles proved all-solid. A support probe inside one of these * tiles searches upward for the boundary tile; all-air/empty tiles search downward. */ TSet CollisionSolidTiles; struct FPendingCollisionCook { TWeakObjectPtr Mesh; uint64 SubmissionId = 0; uint64 MeshSubmittedCycles = 0; uint64 CollisionSubmittedCycles = 0; uint64 CollisionSubmittedFrame = 0; bool bBodyUpdatedEventSeen = false; bool bPooledComponent = false; uint64 BodyUpdatedEventCycles = 0; uint64 BodyUpdatedEventFrame = 0; bool bPhysicsStateReadyAtBodyUpdate = false; uint64 PhysicsStateReadyCycles = 0; uint64 FutureCallbackCycles = 0; uint64 CompletionGameThreadCycles = 0; // Game-thread-only timing copied from the generation result for the clean LOD0 summary. uint64 RequestStartCycles = 0; uint64 GenerationStartCycles = 0; uint64 GenerationEndCycles = 0; uint64 ApplyStartCycles = 0; EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon; bool bStep1SupportCohort = false; int32 NumVertices = 0; int32 NumTriangles = 0; int32 NumSkirtTriangles = 0; int32 PendingCooksAtSubmit = 0; bool bUseAsyncCook = true; bool bUseComplexAsSimpleCollision = true; bool bShouldFastCookMeshes = false; bool bMergeAllMeshes = false; uint64 SuppressedAtCycles = 0; bool bRecordStreamingLatency = false; }; /** One serialised completion record per tile. The serial prevents a late RMC future from * making a remeshed tile, or a pooled component reused by another tile, appear ready. */ TMap PendingCollisionCooks; uint64 NextCollisionSubmissionId = 0; TMap CollisionBodyUpdatedHandles; struct FStep1SupportSweepProbe { uint64 CollisionRequestCycles = 0; uint64 BodyUpdatedCycles = 0; uint64 PhysicsStateReadyCycles = 0; bool bFirstSweepAttemptLogged = false; }; TMap Step1SupportSweepProbes; struct FStep1VisibleApplyProbe { uint64 AppliedCycles = 0; uint64 AppliedFrame = 0; float LastRenderTimeBeforeApply = 0.0f; }; TMap Step1PendingVisibleApplyProbes; TArray Step1HeldApplyResults; TSet Step1HeldApplyTiles; int32 Step1HeldVisibleLOD0Count = 0; int32 Step1LastLoggedPrequeueCount = -1; int32 Step1HeldApplyCursor = 0; bool bStep1UploadCorpusReleased = false; /** Render component per NON-empty loaded tile (GC-safe via actor ownership; not UPROPERTY * because FVoxelTileKey isn't a USTRUCT key). */ TMap TileComponents; /** T2.c — COMPONENT POOL. Unloading a tile parks its component here (geometry + collision * stripped via RemoveSectionGroup, hidden, still registered) instead of DestroyComponent; * ApplyMeshToTile pops from here instead of NewObject + RegisterComponent. Kills the * create/register/GC churn of fast travel and regen bursts. Same GC-safety rationale as * TileComponents (registered components are owned by the actor). Bounded — overflow is * destroyed for real. */ TArray TileComponentPool; static constexpr int32 MaxPooledTileComponents = 256; /** Pop a pooled tile component (made visible again) or create + register a fresh one. */ URealtimeMeshComponent* AcquireTileComponent( double* OutCreationSeconds = nullptr, double* OutRegistrationSeconds = nullptr, bool* bOutCreated = nullptr); /** Park a tile component in the pool (strip geometry/collision, hide) — or destroy it * for real when the pool is full. */ void ReleaseTileComponent(URealtimeMeshComponent* Comp); /** T2.d — the effective concurrent gen-task budget: the asset's MaxConcurrentTasks, * capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background * priority stops frame starvation, not the context-switch overhead). */ int32 GetMaxConcurrentTasks() const; int32 GetMaxConcurrentStreamingTasks() const; /** Submitted generation tasks still queued or running; completed MPSC results do not occupy admission. * Taches de generation soumises, en file ou en cours ; les resultats MPSC termines ne bloquent pas l'admission. */ int32 GetActiveGenerationTaskCount() const; /** Count queued/running work for admission; the legacy CVar can include finished results awaiting apply. * Compter le travail en file/en cours ; le CVar de compatibilite peut inclure les resultats finis en attente. */ int32 GetGenerationAdmissionCount() const; /** Reserve task slots for collision-critical/edit/LOD0 work; far work uses the remainder. */ int32 GetNearTaskReserve(int32 MaxTasks) const; //========================================================================= // TERRAIN MODIFICATION (player carving & filling) //========================================================================= /** * Carve terrain at a world position (remove rock, create air). * Applies a spherical brush that subtracts density. * * @param Position - World-space center of the carve brush * @param Radius - Brush radius in legacy Runtime voxels; Core converts it using its configured * voxel size to preserve the physical brush size. * @param Strength - Legacy Runtime density offset; Core scales it to its configured size. * Offset de densite Runtime historique; Core le convertit selon sa resolution. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void CarveAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f); /** * Fill terrain at a world position (add rock, seal holes). * Applies a spherical brush that adds density. * * @param Position - World-space center of the fill brush * @param Radius - Brush radius in legacy Runtime voxels; Core converts it using its configured * voxel size to preserve the physical brush size. * @param Strength - Legacy Runtime density offset; Core scales it to its configured size. * Offset de densite Runtime historique; Core le convertit selon sa resolution. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void FillAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f); /** * Box brush carve/fill. Position is world-space (Unreal units); ExtentVoxels is the * box half-size in legacy Runtime voxels. Core converts it using its configured voxel size. * Strength is a legacy-voxel density offset; Core scales it and forces its sign. * La force est un decalage de densite historique; Core la convertit selon sa resolution et impose le signe. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void CarveBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f); UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void FillBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f); /** * Capsule brush carve/fill between two world-space points (Unreal units). Tube radius and density offset * use legacy Runtime voxels; Core converts both using its configured voxel size. * Creuse/remplit une capsule entre deux points monde; rayon et decalage historiques suivent la resolution Core. * Great for boring tunnels or laying solid pillars/walls. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f); UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f); /** * Apply a fully-specified modification (any shape). Centers/endpoints and brush dimensions * are expected in active voxel coordinates here (legacy or configured Core size); high-level * helpers convert legacy brush sizes to preserve physical dimensions. * Returns nothing; re-meshes affected chunks. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void ApplyModification(const FVoxelModification& Modification); /** * Clear all player modifications (e.g., on season reset). * Regenerates all currently loaded chunks to restore procedural terrain. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") void ClearAllModifications(); //========================================================================= // SEED / SEASON MANAGEMENT //========================================================================= /** * Change the world seed and regenerate everything. * * This is the "season reset" operation: * 1. Updates the seed in Settings, Generator, and StrateManager * 2. Clears ALL player modifications (carvings are meaningless in a new world) * 3. Resets the elevator depth (player must re-discover strates) * 4. Increments the season counter * 5. Unloads all chunks and lets Tick reload them with the new seed * * The game layer is responsible for calling this at season boundaries, * saving/loading the season counter, and any pre-reset cleanup (inventory, etc.) * * @param NewSeed - The new world seed */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Season") void ChangeSeed(int32 NewSeed); /** * Get the current world seed. */ UFUNCTION(BlueprintPure, Category = "Voxel World|Season") int32 GetCurrentSeed() const; /** * Get the current season number (incremented each time ChangeSeed is called). */ UFUNCTION(BlueprintPure, Category = "Voxel World|Season") int32 GetCurrentSeason() const; /** Reviewed manifest digest to replicate/compare at join; empty on the legacy authored path. */ UFUNCTION(BlueprintPure, Category = "Voxel World|Season") FString GetCurrentSeasonContentHash() const; //========================================================================= // STRATE QUERIES (gameplay integration) //========================================================================= /** * Get which strate index a world position is in. * * @param WorldPosition - Position in world space (Unreal units) * @return Strate index (0 = topmost), or -1 if outside all strates */ //========================================================================= // ESPACE MONDE <-> ESPACE ACTEUR / WORLD <-> ACTOR SPACE //========================================================================= // Le champ de densite est defini en espace ACTEUR : (0,0) est l origine de CET acteur, pas // celle du monde Unreal. Tout point venant d Unreal (position du pion, hit de trace, ancre de // streaming) DOIT passer par ces helpers avant de toucher quoi que ce soit cote voxel. // // The density field is authored in ACTOR space: (0,0) is THIS actor s origin, not Unreal s // world origin. Any point arriving from Unreal (pawn position, trace hit, streaming anchor) // MUST go through these helpers before touching anything voxel-side. // // INVARIANT VERIFIABLE PAR GREP / GREP-CHECKABLE INVARIANT: // aucun `/ VOXEL_SIZE` applique a un parametre nomme `World*` hors de ces fonctions. // no `/ VOXEL_SIZE` applied to a parameter named `World*` outside these functions. // C est la vraie garantie : le risque n est pas un site rate parmi ceux deja trouves, c est // le SITE SUIVANT que personne ne verra. The risk is not a missed site among those already // found - it is the NEXT one nobody notices. // // La racine est Static (voir BeginPlay) => la transform ne bouge pas en jeu, donc la relire a // chaque appel est gratuit et ne demande aucune invalidation. // The root is Static (see BeginPlay), so the transform cannot change during play: re-reading // it per call is free and needs no invalidation. /** World cm -> actor-local cm. */ UFUNCTION(BlueprintPure, Category = "Voxel World|Space") FVector WorldToLocalCm(FVector WorldPos) const; /** World cm -> actor-local VOXEL coords (what every density / diff-layer API expects). */ UFUNCTION(BlueprintPure, Category = "Voxel World|Space") FVector WorldToLocalVoxel(FVector WorldPos) const; /** Actor-local VOXEL coords -> world cm (placing actors, debug draws, UI markers). */ UFUNCTION(BlueprintPure, Category = "Voxel World|Space") FVector LocalVoxelToWorld(FVector VoxelPos) const; /** Core-route state accessors used by the isolated BuildHost commandlet. */ bool IsVoxelForgeCoreReady() const; FVector GetVoxelForgeCoreDefaultCenterWorld() const; bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const; bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const; bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const; UFUNCTION(BlueprintPure, Category = "Voxel World|Strate") int32 GetStrateAtPosition(FVector WorldPosition) const; // Convert actor-local physical Z to/from UVoxelStrateManager's legacy coordinate space. // Convertit Z local physique vers/depuis l'espace historique de UVoxelStrateManager. float ToStrateManagerLocalZ(float LocalZCm) const; float FromStrateManagerLocalZ(float ManagerLocalZCm) const; /** * Probe the biome field at a world location (e.g. a mouse line-trace hit). Returns the dominant + * neighbour biome, the climate fields, the border blend weight, and the dominant biome's decoration * count — the SAME resolution the decoration scatter uses per column. Use it to debug placement: * a returned DominantDecorationCount of 0 means that biome has no decorations (an empty region), * NOT a bug. WorldLocation is full world space (the actor transform is undone internally). */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Biome") FVoxelBiomeQuery GetBiomeAtWorldLocation(FVector WorldLocation) const; /** * SurfaceWorld ground finder for self-arranging prefabs (ruins/set-pieces authored as Blueprints): * the terrain surface + sky-cap ceiling world-Z under WorldLocation's XY, WITHOUT any line trace or * streamed collision. Deterministic and available before the area meshes. Returns false (outs = the * input Z) when the point isn't a SurfaceWorld heightfield (cave strates) — fall back to a trace there. * Does NOT account for passage/spine carving; re-check with a trace if the spot might be carved. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Query") bool GetVoxelSurfaceHeightAt(FVector WorldLocation, float& OutSurfaceWorldZ, float& OutCeilingWorldZ) const; //========================================================================= // STREAMING ANCHORS — keep terrain (collision) loaded around actors that aren't the local player // (AI now; remote players later). ARCHITECTURE §9.3. Registering an actor folds a small box of // level-0 tiles around it into the desired set, so it has ground to stand on / pathfind / be hit // even far from the camera. Idempotent per actor (re-register updates the policy/radius). //========================================================================= /** Start streaming terrain around Actor. Default box = its chunk + 1 horizontal ring + 1 chunk below * (ground safety), nothing above. Empty tiles are ~free (trivial-tile reject); with the CollisionOnly * policy the SOLID tiles cook collision but don't render (§9.4). Raise ZAbove for flyers/tall NPCs. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Streaming") void RegisterStreamingAnchor(AActor* Actor, EVoxelAnchorPolicy Policy = EVoxelAnchorPolicy::CollisionOnly, int32 XYRadiusChunks = 1, int32 ZBelowChunks = 1, int32 ZAboveChunks = 0); /** Stop streaming terrain around Actor. Its tiles are released by the normal delta cull. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Streaming") void UnregisterStreamingAnchor(AActor* Actor); //========================================================================= // LIGHTING — DENSITY VOLUME (debug / material wiring) //========================================================================= /** The GPU R8 density volume texture for a clip level (0 = finest, near the player). Null until the * volume has streamed in / if GPU upload is off. STEP 1b-i validation: in a debug BP, create a * dynamic material instance of a Volume-Texture-sampling material and SetTextureParameterValue from * this — you should see the density field, centred on the player, updating as you move & carve. */ UFUNCTION(BlueprintCallable, Category = "Voxel World|Lighting") UVolumeTexture* GetDensityVolumeTexture(int32 Level = 0) const; private: friend class FScopedGenerationPause; friend class UVoxelContentManager; friend class UVoxelAtmosphereManager; #if WITH_EDITOR /** Last editor research-field selection applied to loaded tiles. */ int32 AppliedResearchFieldEnabled = INDEX_NONE; int32 AppliedResearchFieldProfile = INDEX_NONE; #endif /** Get/create the shared MID wrapping a base terrain material (binds volume textures + shadow params). * Returns Base unchanged-wrapped, or nullptr if Base is null. */ UMaterialInstanceDynamic* GetOrCreateTerrainMID(UMaterialInterface* Base); /** Recompute the packed volume/orb shader params (TVP0..4) from the density volume + nearest orb, and * push them (and the volume textures) onto every terrain MID. Called each Tick. */ void UpdateTerrainMaterialParams(); /** Apply the current TVP0..4 + level-0 volume texture to one MID (also used on MID creation). */ void SetVolumeParamsOnMID(UMaterialInstanceDynamic* MID) const; /** Put all terrain MIDs into the material's neutral, volume-disabled state. */ void SetDensityVolumeMaterialParamsDisabled(); /** Runtime Core snapshot, world lifetime, and worker-side streaming bridge. */ bool InitializeVoxelForgeCoreWorld(FString& OutFailureReason); void FallBackToLegacyAfterCoreInitializationFailure(const FString& FailureReason); void BuildCoreDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition, APawn* PlayerPawn, const FVector& PlayerHeading, TArray& OutLeavers); FIntVector GetStreamingHeadingStep(const FVector& PlayerHeading, const APawn* PlayerPawn) const; int32 GetStreamingCollisionLookaheadTiles(const FVector& PlayerHeading, const FIntVector& HeadingStep) const; void StartPendingCoreRegionCompile(); void LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority); void EnqueueStreamingResult(FChunkResult&& Result); void RecordStreamingResultDequeued(const FChunkResult& Result); uint64 GetStreamingResultPayloadBytes(const FChunkResult& Result) const; int32 GetStreamingResultBacklogCount() const; bool IsStreamingResultBacklogCapped() const; void ApplyCoreModification(const FVoxelModification& Modification); void BuildDesiredTransitionIndex(); float GetActiveVoxelSizeCm() const; float GetLegacyVoxelToActiveVoxelScale() const; FIntVector GetTileCoordAtLocalCm(const FVector& LocalPositionCm) const; FIntVector GetTileCoordAtVoxelPosition(const FVector& VoxelPosition) const; /** Explicitly opt-in headless streaming validation. This is a command-line-only harness path; * normal gameplay never moves the pawn here. It supplies a repeatable moving session and lets * -nullrhi request a graceful EndPlay so the clean latency summary is flushed. */ void ConfigureHeadlessStreamingTest(); void AdvanceHeadlessDigMotionTest(FVector& InOutPlayerPosition, APawn* Pawn); void RecordHeadlessDigMotionFrame(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime); void AdvanceHeadlessStreamingTest(FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn); void AdvanceHeadlessCollisionGateStressTest(FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn); void ObserveHeadlessCollisionGateStressTest(const FVector& PlayerPosition, APawn* PlayerPawn); bool MeasureHeadlessCollisionGateStressTerrain( const FVector& WorldPosition, float CapsuleHalfHeightCm, float& OutFeetDensity, float& OutCenterDensity, float& OutSurfaceLocalVoxelZ, float& OutClearanceCm); void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn); void InitializeHeadlessStrateCrossingTest(); void AdvanceHeadlessStrateCrossingTest(FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn); void ObserveHeadlessStrateCrossingTest(const FVector& PlayerPosition, APawn* PlayerPawn); void MaybeFinishHeadlessStrateCrossingTest(); void MaybeFinishHeadlessSurfaceFallTest(); void MaybeFinishHeadlessCollisionGateStressTest(); void MaybeFinishHeadlessStreamingTest(); void UpdateVisualStalenessAudit(); void LogVisualStalenessAudit(); void LogReadyTransitionVisibilityMeasure(); void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi, const FVoxelMeshData& MeshData); void WriteTileHashDump(); // Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector // Parameter only delivers float3 (RGB) into a Custom node (the alpha is dropped), so we never use .w. // TVP0 = L0 WindowOrigin.xyz (world cm) TVP1 = L0 OriginMod.xyz (cells) // TVP2 = OrbPos.xyz (world cm) TVP3 = OrbColor.rgb * OrbIntensity (premultiplied) // TVP4 = (OrbMaxDist, OrbFalloff, MarchSteps) TVP5 = (Res, L0 CellWorldSize, OrbEnable) // TVP6 = L1 WindowOrigin.xyz TVP7 = L1 OriginMod.xyz // TVP8 = L2 WindowOrigin.xyz TVP9 = L2 OriginMod.xyz // Coarser levels' cell size is derived in-shader (cell_L = L0Cell * 2^L); Res is shared. FLinearColor TVP0 = FLinearColor::Black, TVP1 = FLinearColor::Black, TVP2 = FLinearColor::Black, TVP3 = FLinearColor::Black, TVP4 = FLinearColor::Black, TVP5 = FLinearColor::Black, TVP6 = FLinearColor::Black, TVP7 = FLinearColor::Black, TVP8 = FLinearColor::Black, TVP9 = FLinearColor::Black; // Change-detection for the per-Tick MID pushes (MIDs OWN their param values, so skip-if-identical // is safe there — unlike the MPC, whose world instance can reset behind our back; see // UpdateOrbLightMPC, which deliberately rewrites every frame). TWeakObjectPtr LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated TSharedPtr VoxelForgeCoreState; FString VoxelForgeCoreWorldAssemblyJson; FString VoxelForgeCoreWorldAssemblyHash; FString VoxelForgeCoreResolvedLayoutJson; FString VoxelForgeCoreResolvedLayoutIdentity; FVector CoreFallbackCenterLocalCm = FVector::ZeroVector; bool bCoreRegionCompilePending = false; TArray PendingCoreRegionTiles; TSet PendingCoreRegionTileSet; TArray PendingCoreRetainedTiles; TSet CoreRegionCompileTilesInFlight; FIntVector LastCoreRegionRequestCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); FIntVector PendingCoreRegionRequestCenter = FIntVector::ZeroValue; FIntVector ActiveCoreRegionCompileCenter = FIntVector::ZeroValue; bool bHasLastCoreRegionRequestCenter = false; double CoreRegionPendingSinceSeconds = 0.0; double ActiveCoreRegionCompileStartSeconds = 0.0; uint64 ActiveCoreRegionCompileStartCycles = 0; uint64 CoreRegionIndependentSubmissions = 0; uint64 CoreRegionDependentDeclines = 0; int32 CoreRegionCompileRequestSerial = 0; int32 ActiveCoreRegionCompileRequestSerial = 0; public: //========================================================================= // LIVE EDIT (debug tuning in PIE) //========================================================================= /** When true, editing your Strate Definition data assets during PIE * will automatically regenerate all chunks so you see the result live. * Also adds a "Regenerate" button in Details for manual refresh. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit") bool bLiveEditStrates = false; #if WITH_EDITORONLY_DATA /** Seed passed to the deterministic offline composer roll. This does not change Settings->Seed. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer") int32 ComposerSeed = 0; /** Candidate row passed to VF_RollStrateCandidate (the offline tests use the same index). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer", meta = (ClampMin = "0")) int32 ComposerCandidateIndex = 0; /** Existing layout slot to replace; 0 is the topmost strate. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer", meta = (ClampMin = "0")) int32 ComposerTargetStrateIndex = 0; /** False rolls one native parameter vector; true rolls the structure recipe and its six blocks. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer") bool bComposerRollStructure = false; #endif #if WITH_EDITOR /** Roll and apply the selected offline composer candidate to one live PIE slot. */ UFUNCTION(CallInEditor, Category = "Live Edit|Composer") void ApplyComposerCandidate(); #endif /** Click to force-regenerate all chunks right now (useful during PIE). */ UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit") void RegenerateAllChunks(); /** Re-read ALL of VoxelSettings (strate layout, inter-strate gap, passages, spine) * and rebuild from scratch, then regenerate every chunk. Use this after changing * passage / gap / spine settings so they apply live without restarting PIE — * RegenerateAllChunks alone keeps the existing layout & passages. */ UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit") void RebuildStrates(); /** F2 — DETERMINISM VALIDATOR (run during PIE, takes ~a second). Samples a band of * densities at a chunk boundary near the player through TWO cache-window alignments * (thread_local chunk caches warmed from the left chunk, then from the right one, same * points re-sampled) plus a same-alignment repeat. Every delta MUST be exactly 0 — * anything else is a window-invariance regression (ARCHITECTURE §8.4). Logs the verdict * and the first offending voxel. Run it after any "bit-identical" hot-path refactor. */ UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit") void ValidateDeterminism(); //========================================================================= // EDITOR BRUSH (manual carve/fill from the Details panel, works in PIE) //========================================================================= // The diff layer only exists while playing, so these buttons act during PIE. /** World-space center (Unreal units) for the editor carve/fill buttons below. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush") FVector EditorBrushCenter = FVector::ZeroVector; /** Brush radius in legacy Runtime voxels; Core helpers preserve the same physical radius. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.5")) float EditorBrushRadius = 1.5f * 100.0f / VOXEL_SIZE; /** Brush strength for the editor buttons (sign forced by the button). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.1")) float EditorBrushStrength = 12.0f; /** Carve a sphere at EditorBrushCenter using the brush settings above. */ UFUNCTION(CallInEditor, BlueprintCallable, Category = "Editor Brush") void EditorCarveSphere(); /** Fill a sphere at EditorBrushCenter using the brush settings above. */ UFUNCTION(CallInEditor, BlueprintCallable, Category = "Editor Brush") void EditorFillSphere(); /** Generation counter — incremented each time we regenerate. * Async tasks carry this value; stale results are discarded. */ uint32 GenerationEpoch = 0; /** Draw the inter-strate passages each frame (lines along the path + endpoint * spheres) so you can see where they spawned and verify they carve. PIE only. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Debug") bool bDebugDrawPassages = false; //========================================================================= // BIOME MAP PREVIEW (bake the XY biome field to a PNG — works without PIE) //========================================================================= // Tune biome layout (cell size, warp, climate boxes) without flying around: set // the strate + window, click Bake, open Saved/BiomePreview.png. Uses a transient // generator seeded from VoxelSettings, so it works in the editor with no PIE. /** The strate whose Biomes[] + BiomeMapParams to preview. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview") UVoxelStrateDefinition* BiomePreviewStrate = nullptr; /** Width/height of the sampled window in VOXELS (centred on BiomePreviewCenter). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview", meta = (ClampMin = "1.0")) float BiomePreviewWorldSize = 16000.0f; /** Centre of the preview window in voxel coords (X,Y). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview") FVector2D BiomePreviewCenter = FVector2D::ZeroVector; /** Output image resolution (pixels per side). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview", meta = (ClampMin = "64", ClampMax = "2048")) int32 BiomePreviewResolution = 512; /** Which field to visualise: biome debug colours, relief, or moisture. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview") EBiomePreviewChannel BiomePreviewChannel = EBiomePreviewChannel::Biome; /** Bake the selected channel to Saved/BiomePreview.png. */ UFUNCTION(CallInEditor, BlueprintCallable, Category = "Biome Preview") void BakeBiomePreview(); #if WITH_EDITOR virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; /** Called by FCoreUObjectDelegates::OnObjectModified when ANY UObject is edited. * We filter for UVoxelStrateDefinition changes and regenerate if live edit is on. */ void OnObjectModifiedInEditor(UObject* ModifiedObject); /** Handle to unbind the delegate when EndPlay is called */ FDelegateHandle OnObjectModifiedHandle; #endif //========================================================================= // ACTOR LIFECYCLE //========================================================================= virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; virtual void Tick(float DeltaTime) override; //========================================================================= // CHUNK MANAGEMENT - YOU IMPLEMENT THESE //========================================================================= /** * Update which chunks are loaded based on a world position. * * CONCEPT: * - Figure out which chunk the position is in (the "center") * - Determine which chunks SHOULD exist (within view distance) * - Load any chunks that should exist but don't * - Unload any chunks that exist but shouldn't * * @param CenterPosition - Usually the player's position */ void UpdateChunksAroundPosition(const FVector& CenterPosition, APawn* PlayerPawn, const FVector& PlayerHeading); /** * Load a single chunk at the given coordinate. * * CONCEPT: * - Create chunk data * - Generate terrain * - Build mesh * - Create visual component * * @param ChunkCoord - Which chunk to load */ void LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon); /** * Worker-side gen for one tile: classify → GenerateMesh → BuildTileStreamSet. * Fills Result (no enqueue, no bookkeeping). Called from the async ChunkGen task AND from the * synchronous carve path (SyncRemeshTile) — reads Generator/Mesher only, so it's safe on either * thread. See LoadTile for how the parameters are derived. */ void GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture, int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi, FChunkResult& Result, const std::atomic* ObsoleteFlag = nullptr, bool bWantPreEditDensityGrid = false, const FVoxelSharedDensityGrid* SharedDensityOverride = nullptr); /** * Game-thread apply for one gen result (shared by ProcessPendingChunks + SyncRemeshTile): * epoch check, ingest capture, then either release the tile's component (empty) or submit a mesh * through ApplyMeshToTile. Returns true iff a VISIBLE mesh was submitted (counts against the * apply budget); collision readiness is a later RMC completion. * Does NOT touch PendingTiles — the caller owns that. A non-empty result counts as applied only * after mesh submission succeeds; collision readiness arrives asynchronously afterwards. */ bool ApplyTileResult(FChunkResult& Result); /** * Same-frame level-0 re-mesh on the game thread: gen + apply INLINE so a player carve is visible * THIS frame (no async round-trip). Used for the tile under the brush centre; neighbours re-mesh * async (prioritised) via RemeshDirtyChunks. One full-res tile gen on the game thread — bounded. */ void SyncRemeshTile(const FVoxelTileKey& Tile); /** * Unload a single chunk. * * CONCEPT: * - Remove and destroy the mesh component * - Remove chunk data from storage * * @param ChunkCoord - Which chunk to unload */ void UnloadTile(const FVoxelTileKey& Tile); /** * Upload a tile's geometry to its RealtimeMesh component (game thread). * * The vertex/index buffers (Streams) are already BUILT on the worker (T1.f — see * BuildTileStreamSet / FChunkResult), so this only does the game-thread-only work: * material resolution, get-or-create the component, CreateSectionGroup(MoveTemp), * and per-section config (collision/shadow). Never called for empty tiles. * * F17 — the streams carry TWO polygroups (0 = ground, 1 = sky-cap ceiling, classified * semantically per triangle on the worker): RMC auto-creates one section per non-empty * group, so a coarse tile spanning both the terrain and the cap gets BOTH materials * (the old whole-tile vote painted the loser with the winner's material). * * Takes the whole FChunkResult (tile key, streams — consumed/moved —, per-group flags and * the strate content band the mesh was cut to; material lookups clamp into that band). * Never called for empty results. */ bool ApplyMeshToTile(FChunkResult& Result); /** Mini-sun lighting (bounded directional). Each frame writes the nearest 4 active orbs' WORLD * positions (+ reach radius in .w) into OrbLightMPC's Orb0..3 vector params; the Directional * Light's Light Function material reads them to mask its contribution into a pool around each * orb. No-op until OrbLightMPC is assigned. Replaces the density-volume raymarch. */ void UpdateOrbLightMPC(); /** The Material Parameter Collection (MPC_VoxelOrbs) the orb Light Function reads. Assign in the * AVoxelWorld details. Params expected: Vector Orb0,Orb1,Orb2,Orb3 = (x,y,z, reachRadiusCm). */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting") UMaterialParameterCollection* OrbLightMPC = nullptr; /** Build the clipmap desired-tile set (concentric shells) around the player tile. * OutLeavers = les tuiles désirées au crossing PRÉCÉDENT qui ne le sont plus — les seuls * candidats au cull (delta), au lieu de re-scanner TOUTES les tuiles chargées par crossing. */ void BuildDesiredTiles(const FIntVector& CenterChunkCoord, const FVector& PlayerPosition, APawn* PlayerPawn, const FVector& PlayerHeading, TArray& OutLeavers); void CancelObsoleteTileWork(); /** True if a tile's world footprint is still within the outermost clip shell (so a * not-desired loaded tile there is mid-LOD-transition and must wait for its replacement, * vs. one that has left the view entirely and can be culled immediately). */ bool IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector& CenterChunkCoord) const; //========================================================================= // HELPERS //========================================================================= /** Return the current player position and distinguish "no pawn" from a valid origin position. */ bool TryGetPlayerPosition(FVector& OutPosition, APawn** OutPawn = nullptr) const; /** Compatibility wrapper for callers that only need the location. A real pawn at the origin * therefore returns FVector::ZeroVector just like any other valid position. */ FVector GetPlayerPosition() const; /** Level-0 tile under the pawn's feet. This is deliberately separate from the actor-center tile: * the floor that catches a falling pawn is what must be cooked before movement is released. */ bool GetPlayerSupportTile(APawn* Pawn, const FVector& PlayerPosition, FVoxelTileKey& OutSupportTile) const; bool IsTileCollisionReady(const FVoxelTileKey& Tile) const; bool IsTileCollisionDesired(const FVoxelTileKey& Tile) const; bool IsCollisionTileUnavailable(const FVoxelTileKey& Tile, const TCHAR*& OutReason) const; bool IsCollisionReadyFromSupportTile(const FVoxelTileKey& CandidateTile, FVoxelTileKey* OutSupportTile = nullptr, const TCHAR** OutUnavailableReason = nullptr) const; bool IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition, FVoxelTileKey* OutSupportTile = nullptr, const TCHAR** OutUnavailableReason = nullptr) const; bool IsPawnCapsuleOverlappingPendingFloor(APawn* Pawn, const FVector& PlayerPosition, const FVector& NextPlayerPosition, FVoxelTileKey* OutPendingTile = nullptr) const; void RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile); /** Hold pawn movement while a swept pending support tile intersects the capsule. Restore the * previous mode, gravity, and velocity on release. The world tick is a prerequisite of the pawn * and movement component so the gate check runs before movement physics. */ void UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime); void EngagePawnCollisionGate(APawn* Pawn); void ReleasePawnCollisionGate(); /** Called by the RMC collision future after async cook + BodySetup installation. */ void HandleTileCollisionCookComplete(const FVoxelTileKey& Tile, uint64 SubmissionId, URealtimeMesh* Mesh, uint8 Result, uint64 FutureCallbackCycles); void BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh); void UnbindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh); void HandleRealtimeMeshCollisionBodyUpdated(URealtimeMesh* Mesh, UBodySetup* BodySetup); void PollStep1SupportSweepProbes(); void PollStep1VisibleApplyProbes(); void RecordLOD0ReadySample(uint64 RequestStartCycles, uint64 GenerationStartCycles, uint64 GenerationEndCycles, uint64 ApplyStartCycles, uint64 ReadyCycles, uint64 CollisionSubmittedCycles); void RecordLOD0RingReadyIfComplete(); void LogStreamingLatencySummary() const; // (GetLODForChunk / LODToStep / IsChunkInRange removed — dead since the clipmap // streaming replaced the distance-LOD scheme; the level lives in FVoxelTileKey.) //========================================================================= // ASYNC //========================================================================= // MPSC: up to MaxConcurrentTasks ChunkGen worker threads Enqueue concurrently, // the game thread (ProcessPendingChunks) is the only consumer. The default Spsc // mode is NOT safe for multiple producers — concurrent Enqueues race on the tail // link and silently drop results, which leaks PendingChunkCoord slots until the // budget is exhausted and streaming stalls permanently. Mpsc guards the producer side. // Result queues mirror the scheduling order: collision support first, then edited tiles, // then ordinary LOD0, then the horizon. This keeps a support result from waiting behind a // completed near-field backlog while preserving one consumer on the game thread. TQueue CriticalProcessQueue; TQueue EditedProcessQueue; TQueue NearProcessQueue; TQueue ProcessQueue; // The cap counts completed MPSC results plus admitted tile tasks, so a slow game-thread apply // cannot accumulate an unbounded payload queue while completed work never consumes worker slots. // Le plafond compte les resultats MPSC et les taches admises; un apply lent ne peut donc pas // accumuler sans limite, et les resultats termines ne reprennent pas de slot worker. std::atomic QueuedStreamingResultCount{0}; std::atomic QueuedStreamingResultBytes{0}; std::atomic PeakQueuedStreamingResultCount{0}; std::atomic PeakQueuedStreamingResultBytes{0}; std::atomic ActiveTileTaskCount{0}; std::atomic StreamingResultBacklogCapStops{0}; // Keys stay pending from task submission until the game thread applies the result; completed results can wait here. // Les cles restent en attente de la soumission jusqu'a l'application sur le thread de jeu ; les resultats termines y attendent. TSet PendingTiles; // Game-thread-owned map; workers retain the shared token and only read its atomic flag. TMap> PendingTileCancellation; // STRATE CONTENT CUT (see UVoxelSettings::StrateContentCutMinLevel) — the player-strate // chunk-Z band coarse tiles are meshed to (MIN/MAX sentinels = no cut, e.g. in the gap). // Updated each crossing in UpdateChunksAroundPosition; on change (strate transition) the // loaded coarse tiles whose content depends on it are re-queued through BandRemeshQueue // (drained by the budgeted submit loop — LoadTile re-gens in place, no visual pop). int32 MeshBandChunkLo = MIN_int32; int32 MeshBandChunkHi = MAX_int32; TSet BandRemeshQueue; // DIG RESPONSIVENESS — tiles at every LOD touched by a player carve/fill that still need an // async re-mesh (the synchronously-remeshed centre tile is excluded). Drained FIRST in the submit // loop (ahead of streaming + band) and launched at BackgroundHigh so a dig never waits behind // streaming. A tile that's in flight is marked obsolete and KEPT queued so a stale pre-carve // result cannot publish and the post-carve result is admitted immediately. TSet DirtyRemeshQueue; // Set to true during EndPlay — async tasks check this before accessing UObjects std::atomic bShuttingDown{false}; // Set during editor-driven generation mutations; distinct from teardown/shutdown semantics. // Active pendant les mutations de génération lancées par l'éditeur, sans signifier la destruction. std::atomic bGenerationPaused{false}; // Submitted async tasks (including UE-queued work) — EndPlay waits for this to reach 0. // Taches asynchrones soumises (y compris la file UE) — EndPlay attend que le compteur revienne a 0. std::atomic ActiveTaskCount{0}; // Active workers are counted inside the launched tile task, distinct from tasks queued by UE. // Les travailleurs actifs sont comptes dans la tache de tuile, distincts des taches en attente UE. std::atomic ActiveGenerationWorkerCount{0}; FORCEINLINE bool ShouldAbortWork(const std::atomic* ObsoleteFlag = nullptr) const { return bShuttingDown.load(std::memory_order_relaxed) || bGenerationPaused.load(std::memory_order_relaxed) || (ObsoleteFlag != nullptr && ObsoleteFlag->load(std::memory_order_relaxed)); } // Player's level-0 tile coord (= chunk coord). The desired set is rebuilt when this changes. FIntVector CurrentCenterChunk = FIntVector::ZeroValue; // Exact unedited L0 lattices retained around the player for same-frame carve remeshes. // The radius helper bounds this to 27 × 35³ floats (4.42 MiB). TMap PreEditDensityCache; // Non-player streaming anchors (AI now, remote players later — ARCHITECTURE §9.3). BuildDesiredTiles // folds each anchor's small level-0 box into the desired set (same DesiredStamped machinery → the // delta cull releases an anchor's tiles automatically when it moves away / is unregistered). The // desired-set rebuild also fires when any anchor crosses a chunk boundary (UpdateChunksAroundPosition). TArray StreamingAnchors; // Adds each anchor's level-0 tiles to DesiredSorted/DesiredStamped (dedup vs the player clipmap) // and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles. void AddAnchorDesiredTiles(); void ReconcileCollisionDesiredTiles(); // Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile // boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled). bool bForceDesiredRebuild = false; // §9.4 RENDER-SKIP — level-0 tiles wanted ONLY by CollisionOnly anchors (no player-clipmap / no // FullVisual desirer this crossing): they cook collision but are hidden. Rebuilt each crossing in // BuildDesiredTiles. `Prev` lets ReconcileAnchorTileVisibility toggle just the DELTA on already- // loaded tiles when a tile flips render↔collision-only (player walks toward/away from a cluster), // without an O(loaded) scan. Both empty when there are no CollisionOnly anchors → zero cost. TSet CollisionOnlyTiles; TSet PrevCollisionOnlyTiles; // Independent from DesiredSorted: rendered LOD0 tiles outside the pawn radius remain visible // without a Chaos body. Pawn radius, anchor boxes, and collision-gate support are unioned here. TSet CollisionDesiredTiles; // Exact pawn-radius box used only by the default-off Step-1 cook sidecar measurement. TSet Step1SupportCohortTiles; TSharedPtr Step1PauseReplayStreams; FVoxelTileKey Step1PauseReplayTile; int32 Step1PauseReplayTriangles = 0; bool bStep1PauseReplayQueued = false; // A short collision-only column below an unresolved player support tile. Keeping these keys // stamped prevents a critical request outside the render clipmap from being canceled on rebuild. TSet CollisionGateDesiredTiles; void ReconcileAnchorTileVisibility(); // --- Streaming work-avoidance (perf) --- // The desired tile set only changes when the player/anchor crosses a level-0 tile boundary or // a rebuild is forced. We cache it and only rebuild/cull/sort then, and go idle once every // desired tile is streamed in — so a stationary player costs ~nothing per frame. FIntVector LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); bool bAllChunksLoaded = false; bool bStartupTraceDesiredRecorded = false; bool bSampleStacksStarted = false; bool bStartupApplyBoostStarted = false; bool bStartupApplyBoostEnded = false; // Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde. bool bStartupApplyConfigTraced = false; uint64 LastApplyFrameCycles = 0; float StartupTraceSpeedCmPerSecond = 0.0f; TArray DesiredSorted; // critical floor prefix, then true-pawn distance-first TArray CriticalDesiredTiles; // occupied/support tile, then heading tile // Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this // index to test transition coverage without rescanning the full desired set per old tile. TMap> DesiredTransitionDescendants; int32 DesiredTransitionMaxLevel = 0; uint32 DesiredEpoch = 0; // increments whenever DesiredStamped is rebuilt int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count float PeakObservedPawnSpeedCmPerSecond = 0.0f; int32 EffectiveNearTaskReserve = -1; bool bStreamingPlayerMoving = false; // GT snapshot consumed by admission helpers / instantane GT pour les admissions uint64 NextStreamingRequestOrdinal = 0; std::atomic NextStreamingGenerationOrdinal{0}; // Cumulative worker-task wall cycles avoid aliasing the instantaneous per-frame counter. // Les cycles cumules des taches evitent l'aliasing du compteur instantane echantillonne par frame. std::atomic TotalGenerationWorkerTaskCycles{0}; double LastStreamingSchedulerSampleSeconds = 0.0; uint64 LastStreamingSchedulerWorkerCycles = 0; struct FReadyTransitionVisibilitySample { uint64 Cycles = 0; int32 TileComponentCount = 0; int32 DesiredTileCount = 0; }; struct FFarFieldSubmitSample { uint64 Cycles = 0; int32 DesiredTileCount = 0; int32 MaxDesiredLevel = 0; }; bool bMeasureReadyTransitionVisibility = false; TArray ReadyTransitionVisibilitySamples; TArray FarFieldSubmitSamples; // Clean performance accounting. LOD 0..8 covers every marching-cubes clip level. static constexpr int32 TrackedClassifierLODCount = 9; std::atomic TotalWorkerGenerationCycles{0}; std::atomic TotalWorkerGenerationTasks{0}; std::atomic TotalObsoleteWorkerCycles{0}; std::atomic TotalObsoleteWorkerTasks{0}; std::atomic TotalValidationDensityCalls{0}; std::atomic TotalMesherDensityCalls{0}; std::atomic TotalSealedSolidProofCandidates{0}; std::atomic TotalSealedSolidProofSkips{0}; std::atomic TotalOutOfLayoutAirProofSkips{0}; std::atomic OuterClassifierCallsByLOD[TrackedClassifierLODCount]{}; std::atomic OuterClassifierVerdictsByLOD[TrackedClassifierLODCount][3]{}; // Low-overhead game-thread geometry totals used to prove that sampler toggling does not alter // the applied tile/triangle stream. These are measurement counters, not generation inputs. uint64 AppliedTileCount = 0; uint64 AppliedVisibleTileCount = 0; uint64 AppliedTriangleCount = 0; uint64 AppliedVisibleTileCountByLevel[TrackedClassifierLODCount]{}; uint64 AppliedTriangleCountByLevel[TrackedClassifierLODCount]{}; uint64 AppliedBandTileCountByLevel[TrackedClassifierLODCount]{}; TMap LoadedTileGeometry; // Diff revision captured by tile requests. A result from before an edit is never allowed to // replace a tile whose footprint intersects that edit; the old visual/collision state remains // until a post-edit result is applied. struct FModificationRevision { uint32 Epoch = 0; FVector MinVoxel = FVector::ZeroVector; FVector MaxVoxel = FVector::ZeroVector; }; uint32 ModificationEpoch = 0; FVector LastModificationMinVoxel = FVector::ZeroVector; FVector LastModificationMaxVoxel = FVector::ZeroVector; TArray ModificationRevisions; TMap AppliedModificationEpoch; struct FStreamingLatencySample { double RequestToReadySeconds = 0.0; double QueueWaitSeconds = 0.0; double GenerationSeconds = 0.0; double CollisionCookSeconds = 0.0; }; TArray LOD0ReadySamples; // Trace-only ring timing: each desired-set rebuild starts a new epoch. A ring is complete // when every current desired level-0 key has reached LoadedTiles and no level-0 request for // that epoch remains pending. This measures visible near-field readiness independently of the // much larger horizon settle point. uint64 DesiredEpochStartCycles = 0; FIntVector DesiredEpochCenterChunk = FIntVector::ZeroValue; uint32 LastLOD0RingReadyEpoch = MAX_uint32; // Headless validation hook. These are intentionally non-UPROPERTY state and are enabled only // by explicit command-line switches; they do not alter ordinary player movement or streaming. bool bHeadlessStreamingTestMovement = false; bool bHeadlessStreamingTestExitOnSteady = false; bool bHeadlessStreamingTestExitRequested = false; bool bStartupTraceThroughCrossing = false; bool bStartupTraceUntilEndPlay = false; bool bHasLastWalkableGateSample = false; bool bLastWalkableGateReady = false; FIntVector LastWalkableGateCenter = FIntVector::ZeroValue; double LastStreamingTraceSampleSeconds = 0.0; bool bHeadlessStreamingTestCenterOverride = false; FVector HeadlessStreamingTestCenterVoxel = FVector::ZeroVector; int32 HeadlessStreamingTestMoveAttempts = 0; double HeadlessStreamingTestDistanceCm = 0.0; float HeadlessStreamingTestSpeedCmPerSecond = 0.0f; float HeadlessStreamingTestStartSeconds = 0.0f; float HeadlessStreamingTestExitSeconds = 0.0f; float HeadlessStreamingTestTeleportAtSeconds = 0.0f; float HeadlessStreamingTestTeleportDistanceCm = 0.0f; bool bHeadlessStreamingTestTeleportDone = false; FVector HeadlessStreamingTestDirection = FVector::XAxisVector; FVector HeadlessStreamingTestLastActualPosition = FVector::ZeroVector; double HeadlessStreamingTestBeginSeconds = 0.0; double HeadlessStreamingTestLastElapsedSeconds = 0.0; // Transient owner-view render hook. Enabled only by explicit voxel.TestCeilingView arguments; // the camera and smaller test radius are held in memory and never saved to a package. bool bTestCeilingViewEnabled = false; bool bTestCeilingCaptureRequested = false; bool bTestCeilingSecondCaptureRequested = false; TWeakObjectPtr TestCeilingCamera; FVector TestCeilingCameraWorldPosition = FVector::ZeroVector; FRotator TestCeilingCameraRotation = FRotator::ZeroRotator; double TestCeilingViewBeginSeconds = 0.0; float TestCeilingCaptureDelaySeconds = 18.0f; float TestCeilingSecondCaptureDelaySeconds = -1.0f; float TestCeilingCameraMoveSpeedCmPerSecond = 0.0f; float TestCeilingCameraMoveStartSeconds = 0.0f; float TestCeilingCameraMoveDurationSeconds = 0.0f; FVector TestCeilingCameraMoveDirection = FVector::XAxisVector; double TestCeilingCameraLastMoveSeconds = 0.0; bool bResearchGamePerfEnabled = false; double ResearchGamePerfLastFrameSeconds = 0.0; double ResearchGamePerfMovingSeconds = 0.0; TArray ResearchGamePerfMovingFrameTimesMs; bool bTestResearchProfileSwitchConfigured = false; bool bTestResearchProfileSwitchApplied = false; float TestResearchProfileSwitchAfterSeconds = -1.0f; int32 TestResearchProfileSwitchTo = -1; double TestResearchProfileSwitchBeginSeconds = 0.0; // Explicit high-speed collision-gate repro. The streamer stays centred at the launch tile while // the character uses ordinary swept movement toward pending support tiles. The test checks // that an unresolved collision holds the pawn in place without letting it enter the tile. bool bHeadlessCollisionGateStressTest = false; bool bHeadlessCollisionGateStressTestStartPlaced = false; bool bHeadlessCollisionGateStressTestStarted = false; bool bHeadlessCollisionGateStressTestPassed = false; bool bHeadlessCollisionGateStressTestFailed = false; bool bHeadlessCollisionGateStressTestExitRequested = false; bool bHeadlessCollisionGateStressTestGateWasEngaged = false; bool bHeadlessCollisionGateStressTestFullStopProbeApplied = false; bool bHeadlessCollisionGateStressTestFullStopProbeObserved = false; bool bHeadlessCollisionGateStressTestFullStop = false; bool bHeadlessCollisionGateStressTestStartHeightCrossed = false; bool bHeadlessCollisionGateStressTestTerrainViolation = false; bool bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false; bool bHeadlessCollisionGateStressTestLanded = false; bool bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false; bool bHeadlessCollisionGateStressTestSettingsReported = false; bool bHeadlessCollisionGateStressDeepDrop = false; bool bHeadlessCollisionGateStressDeepDropPrepared = false; bool bHeadlessCollisionGateStressDeepDropPrewarmReported = false; bool bHeadlessCollisionGateStressDeepDropSurfaceCached = false; float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f; float HeadlessCollisionGateStressTestSpawnHeightVoxels = 2.5f * 100.0f / VOXEL_SIZE; float HeadlessCollisionGateStressTestDurationSeconds = 2.0f; float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f; double HeadlessCollisionGateStressTestBeginSeconds = 0.0; double HeadlessCollisionGateStressTestStartSeconds = 0.0; double HeadlessCollisionGateStressTestLandedSeconds = 0.0; double HeadlessCollisionGateStressTestGateStartSeconds = 0.0; double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; double HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0; double HeadlessCollisionGateStressTestDistanceCm = 0.0; double HeadlessCollisionGateStressTestGateDistanceCm = 0.0; double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0; double HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0; double HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0; double HeadlessCollisionGateStressTestLaunchSupportCheckSeconds = 0.0; double HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = 0.0; double HeadlessCollisionGateStressTestLaunchSupportReadySeconds = 0.0; double HeadlessCollisionGateStressTestMinClearanceCm = 1.0e30; float HeadlessCollisionGateStressTestMinFeetDensity = 0.0f; float HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = 0.0f; int32 HeadlessCollisionGateStressTestTerrainSampleCount = 0; int32 HeadlessCollisionGateStressTestTraceFrame = 0; int32 HeadlessCollisionGateStressTestGateHoldCount = 0; int32 HeadlessCollisionGateStressTestFullStopCount = 0; FVector HeadlessCollisionGateStressTestDirection = FVector::XAxisVector; FVector HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector; FVector HeadlessCollisionGateStressTestStartPosition = FVector::ZeroVector; FVector HeadlessCollisionGateStressTestMinClearancePosition = FVector::ZeroVector; FVector2D HeadlessCollisionGateStressDeepDropSurfaceXYVox = FVector2D::ZeroVector; float HeadlessCollisionGateStressDeepDropSurfaceZVox = 0.0f; FVoxelTileKey HeadlessCollisionGateStressTestLaunchSupportTile; FString HeadlessCollisionGateStressTestFailureReason; // Explicit owner-layout collision-gate repro. This is a command-line-only harness path: it // places the possessed character above the authored single-strate surface and lets ordinary // CharacterMovement gravity carry it to the generated ground. bool bHeadlessSurfaceFallTest = false; bool bHeadlessSurfaceFallTestStartPlaced = false; bool bHeadlessSurfaceFallTestPassed = false; bool bHeadlessSurfaceFallTestFailed = false; bool bHeadlessSurfaceFallTestExitRequested = false; bool bHeadlessSurfaceFallTestPredictionProbeInjected = false; bool bHeadlessSurfaceFallTestPredictionProbeCleared = false; float HeadlessSurfaceFallTestSpawnHeightVoxels = 5.0f * 100.0f / VOXEL_SIZE; float HeadlessSurfaceFallTestGroundToleranceVoxels = 1.0f * 100.0f / VOXEL_SIZE; float HeadlessSurfaceFallTestTimeoutSeconds = 8.0f; float HeadlessSurfaceFallTestPlayerHalfHeightVoxels = 88.0f / VOXEL_SIZE; float HeadlessSurfaceFallTestTerrainZ = 0.0f; double HeadlessSurfaceFallTestBeginSeconds = 0.0; double HeadlessSurfaceFallTestStartSeconds = 0.0; double HeadlessSurfaceFallTestDistanceCm = 0.0; FVector HeadlessSurfaceFallTestStartPosition = FVector::ZeroVector; FVector HeadlessSurfaceFallTestLastActualPosition = FVector::ZeroVector; FVector HeadlessSurfaceFallTestPredictionProbePosition = FVector::ZeroVector; FString HeadlessSurfaceFallTestFailureReason; // Explicit parity evidence. Workers append under the lock; EndPlay writes the compact JSON // after ActiveTaskCount has reached zero, so the dump is a complete worker-side snapshot. bool bTileHashDumpEnabled = false; FString TileHashDumpPath; FCriticalSection TileHashDumpMutex; TArray TileHashDumpRecords; // Real inter-strate crossing validation. This is enabled only by -voxel.TestStrateCrossing=1; // the route is copied from UVoxelStrateManager's generated passage and movement is supplied to // the ordinary pawn movement component. The initial placement onto the upper landing is test // setup; every point after that is reached through swept character movement and the collision gate. bool bHeadlessStrateCrossingTest = false; bool bHeadlessStrateCrossingTestStartPlaced = false; bool bHeadlessStrateCrossingTestStarted = false; bool bHeadlessStrateCrossingTestPassed = false; bool bHeadlessStrateCrossingTestFailed = false; bool bHeadlessStrateCrossingTestExitRequested = false; bool bHeadlessStrateCrossingTestGateWasEngaged = false; bool bHeadlessStrateCrossingTestInitialSupportReported = false; bool bHeadlessStrateCrossingTestPendingFloorOverlap = false; int32 HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE; int32 HeadlessStrateCrossingTestPassageIndex = INDEX_NONE; int32 HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE; int32 HeadlessStrateCrossingTestLowerStrateIndex = INDEX_NONE; int32 HeadlessStrateCrossingTestRouteTargetIndex = 1; int32 HeadlessStrateCrossingTestLastReportedStrate = INDEX_NONE; float HeadlessStrateCrossingTestSpeedCmPerSecond = 800.0f; float HeadlessStrateCrossingTestStartDelaySeconds = 1.0f; float HeadlessStrateCrossingTestTimeoutSeconds = 120.0f; float HeadlessStrateCrossingTestMaxGateSeconds = 10.0f; float HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 0.5f * 100.0f / VOXEL_SIZE; float HeadlessStrateCrossingTestPlayerHalfHeightVoxels = 88.0f / VOXEL_SIZE; float HeadlessStrateCrossingTestMinFloorClearanceVoxels = 1000000.0f; double HeadlessStrateCrossingTestBeginSeconds = 0.0; double HeadlessStrateCrossingTestStartSeconds = 0.0; double HeadlessStrateCrossingTestGateStartSeconds = 0.0; double HeadlessStrateCrossingTestMaxGateDurationSeconds = 0.0; double HeadlessStrateCrossingTestTotalGateDurationSeconds = 0.0; FVector HeadlessStrateCrossingTestLastActualPosition = FVector::ZeroVector; FString HeadlessStrateCrossingTestFailureReason; TArray HeadlessStrateCrossingTestRoute; // Explicit visual-staleness evidence. Mode 1 audits an edit; mode 2 audits an LOD transition. // These fields are command-line-only diagnostics and are inert in ordinary gameplay. bool bVisualStalenessAudit = false; int32 VisualStalenessAuditMode = 0; bool bVisualStalenessAuditExitLogged = false; uint32 VisualAuditEditEpoch = 0; FVector VisualAuditEditMinVoxel = FVector::ZeroVector; FVector VisualAuditEditMaxVoxel = FVector::ZeroVector; double VisualAuditEditBeginSeconds = 0.0; double VisualAuditEditStaleDurationSeconds = -1.0; int32 VisualAuditEditMaxStaleTiles = 0; bool bVisualAuditEditSawStale = false; bool bVisualAuditEditResolved = false; double VisualAuditLODOverlapStartSeconds = 0.0; double VisualAuditLODOverlapMaxSeconds = 0.0; int32 VisualAuditLODOverlapEvents = 0; int32 VisualAuditLODOverlapMaxPairs = 0; // PLAYER COLLISION GATE — all movement is held while level-0 support collision is unresolved. TWeakObjectPtr PawnTickPrerequisite; TWeakObjectPtr PawnMovementTickPrerequisite; TWeakObjectPtr CollisionGatedPawn; TWeakObjectPtr CollisionGatedPawnMovement; TWeakObjectPtr CollisionGatedCharacterMovement; bool bCollisionGateEnabled = true; float SavedCharacterGravityScale = 1.0f; bool bSavedCharacterGravityScale = false; uint8 SavedCharacterMovementMode = 0; uint8 SavedCharacterCustomMovementMode = 0; bool bSavedCharacterMovementMode = false; FVector SavedGateVelocity = FVector::ZeroVector; bool bSavedGateVelocity = false; bool bSavedPawnMovementWasActive = false; bool bPawnCollisionGateEngaged = false; double PawnGateEngagedAtSeconds = 0.0; bool bPawnGateUnavailableReported = false; bool bPawnGateUnsupportedReported = false; FVoxelTileKey PawnGateRequestedSupportTile; bool bPawnGateSupportRequestReported = false; // Keep the first unresolved support request's timing for the startup gate repro. This is // intentionally retained after release so the stress-test result can report the bootstrap path. FVoxelTileKey StartupCollisionGateSupportTile; bool bStartupCollisionGateTimingCaptured = false; bool bStartupCollisionGateAdoptionReported = false; double StartupCollisionGateFirstCheckSeconds = 0.0; double StartupCollisionGateSubmittedSeconds = 0.0; double StartupCollisionGateReadySeconds = 0.0; // Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée. // BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp // périmé sont les "leavers" (retirées + renvoyées). Le cull ne considère que ces leavers + la // TransitionHold — pas de scan O(toutes-les-tuiles-chargées) à chaque crossing (un tel scan // coûtait ~1.6 ms/crossing dans CullTiles). TMap DesiredStamped; uint32 DesiredStamp = 0; bool IsDesired(const FVoxelTileKey& T) const { const uint32* S = DesiredStamped.Find(T); return S && *S == DesiredStamp; } // Tuiles chargées qui ont quitté le desired set mais sont RETENUES (load-before-unload : leur // remplacement n'est pas encore complet, ou le backlog a sauté le test de recouvrement). // Re-considérées par BUDGET tournant (curseur sur la queue, ~256/crossing) — re-scanner TOUTE // la hold par crossing redevient le vieux scan O(loaded) dès que le streaming ne "settle" // jamais (mesuré 2.47 ms/crossing en packagé). Garder une tuile plus longtemps est toujours // hole-safe. Le set est la MEMBERSHIP autoritaire ; la queue peut contenir des clés périmées // (retirées paresseusement au scan). Le "settled cull" reste le filet de sécurité plein-scan. TSet TransitionHold; TArray TransitionHoldQueue; int32 TransitionHoldCursor = 0; void AddToTransitionHold(const FVoxelTileKey& T) { bool bAlready = false; TransitionHold.Add(T, &bAlready); if (!bAlready) { TransitionHoldQueue.Add(T); } } // Tiles approved for removal but whose teardown (component destroy + content actor Destroy()) // is spread across frames. Unbudgeted, a fast traversal culls a whole shell's worth of tiles in // ONE frame → a game-thread teardown spike. Drained by ProcessUnloadQueue (catch-up scaled). TSet PendingUnload; void ProcessPendingChunks(); void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload) void QueuePendingUnload(const FVoxelTileKey& Tile); void HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile); void ReconcileReadyTransitionVisibility(); /** * Re-queue loaded chunks for async re-generation + re-meshing. * Used after terrain modifications: the old mesh stays visible until * the new async result comes back, so there's no visual pop. * * Chunks that aren't currently loaded are ignored (they'll generate * fresh with the diff layer when loaded normally). * * @param DirtyCoords - Chunk coordinates that need re-meshing * @param ExcludeTile - optional level-0 tile already handled synchronously this frame * (SyncRemeshTile): skipped for the async re-queue, but still marked dirty * for the density volume. */ void RemeshDirtyChunks(const TArray& DirtyCoords, const FVoxelTileKey* ExcludeTile = nullptr); };