Files
VoxelForge/Source/VoxelForge/Public/VoxelWorld.h
T
Fr0zkaandClaude Opus 5.5 47148e0a0d build: VoxelForge compiles in Unreal 5.7 on Linux (L65); Linux-only Unreal trace timing fields
Linux UE builds failed on the Core wrapper module: the Linux-only PerModuleInline.gen.cpp saw Standalone/Compat
shadow headers instead of the engine's, and WITH_EDITOR was redefined under -Werror. Compat headers now forward
to the engine headers with include_next for generated Linux module TUs (canonical Core TUs opt into the Compat
definitions via CoreNamespacePreamble.h); CoreMinimal.h undefines WITH_EDITOR first. VoxelForgeCore.Build.cs
enables that forwarding and AVX2 on Linux only; VoxelForge.Build.cs disables Unity on Linux only. Portability:
std::int64_t consistently in VoxelContentManager.cpp; grouped &&/|| in VoxelRedefinedDensityField.cpp. Linux-only
trace fields split Core generateTile / mesh copy / RMC stream build. Standalone Clang and GCC whole-view hashes
are unchanged vs 3202193. The Unreal spawn trace is blocked by the owner's new assets (SW route sweep fails).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 19:19:07 +01:00

1611 lines
88 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <atomic>
#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<bool> 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<AActor> 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<RealtimeMesh::FRealtimeMeshStreamSet> 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;
// Optional phase timings recorded only while the bounded startup trace is active.
double CoreGenerateSeconds = 0.0;
double MeshCopySeconds = 0.0;
double StreamBuildSeconds = 0.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<uint8> 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<FVector> 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<UMaterialInterface>, TObjectPtr<UMaterialInstanceDynamic>> 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<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> VoxelForgeCoreTileHashes;
uint64 LastVoxelForgeCoreEditDigest = 0;
uint64 LastVoxelForgeCoreEditSequence = 0;
TArray<FVoxelTileKey> LastVoxelForgeCoreAffectedTiles;
//=========================================================================
// CHUNK STORAGE
//=========================================================================
//=========================================================================
// CLIPMAP TILE STORAGE (chunked-LOD)
//=========================================================================
// A level-L tile spans (CHUNK_SIZE<<L) voxels meshed at step (1<<L) → constant 32³-cell
// mesh, one component, one draw, covering 8^L× the volume. Streaming loads concentric
// shells (level 0 near, coarser far), so total tile count stays low (~1-2k) regardless
// of view distance. That low count is WHY each tile can have its own component without a
// game-thread problem (this supersedes the earlier region-batching). Collision + content
// are level-0 only.
/** Tiles whose result was successfully applied — INCLUDING empty/all-air tiles, so we never
* re-submit them. A non-empty level-0 tile is not collision-ready until its RMC cook callback
* has added it to CollisionReadyTiles. */
TSet<FVoxelTileKey> 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<FVoxelTileKey> 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<FVoxelTileKey> CollisionSuppressedUntilCook;
TMap<FVoxelTileKey, uint64> 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<FVoxelTileKey> 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<FVoxelTileKey> CollisionSolidTiles;
struct FPendingCollisionCook
{
TWeakObjectPtr<URealtimeMesh> 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<FVoxelTileKey, FPendingCollisionCook> PendingCollisionCooks;
uint64 NextCollisionSubmissionId = 0;
TMap<URealtimeMesh*, FDelegateHandle> CollisionBodyUpdatedHandles;
struct FStep1SupportSweepProbe
{
uint64 CollisionRequestCycles = 0;
uint64 BodyUpdatedCycles = 0;
uint64 PhysicsStateReadyCycles = 0;
bool bFirstSweepAttemptLogged = false;
};
TMap<FVoxelTileKey, FStep1SupportSweepProbe> Step1SupportSweepProbes;
struct FStep1VisibleApplyProbe
{
uint64 AppliedCycles = 0;
uint64 AppliedFrame = 0;
float LastRenderTimeBeforeApply = 0.0f;
};
TMap<FVoxelTileKey, FStep1VisibleApplyProbe> Step1PendingVisibleApplyProbes;
TArray<FChunkResult> Step1HeldApplyResults;
TSet<FVoxelTileKey> 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<FVoxelTileKey, URealtimeMeshComponent*> 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<URealtimeMeshComponent*> 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<FVoxelTileKey>& 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<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
FString VoxelForgeCoreWorldAssemblyJson;
FString VoxelForgeCoreWorldAssemblyHash;
FString VoxelForgeCoreResolvedLayoutJson;
FString VoxelForgeCoreResolvedLayoutIdentity;
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
bool bCoreRegionCompilePending = false;
TArray<FVoxelTileKey> PendingCoreRegionTiles;
TSet<FVoxelTileKey> PendingCoreRegionTileSet;
TArray<FVoxelTileKey> PendingCoreRetainedTiles;
TSet<FVoxelTileKey> 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<bool>* 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<FVoxelTileKey>& 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<FChunkResult, EQueueMode::Mpsc> CriticalProcessQueue;
TQueue<FChunkResult, EQueueMode::Mpsc> EditedProcessQueue;
TQueue<FChunkResult, EQueueMode::Mpsc> NearProcessQueue;
TQueue<FChunkResult, EQueueMode::Mpsc> 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<int32> QueuedStreamingResultCount{0};
std::atomic<uint64> QueuedStreamingResultBytes{0};
std::atomic<int32> PeakQueuedStreamingResultCount{0};
std::atomic<uint64> PeakQueuedStreamingResultBytes{0};
std::atomic<int32> ActiveTileTaskCount{0};
std::atomic<uint64> 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<FVoxelTileKey> PendingTiles;
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
TMap<FVoxelTileKey, TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>>
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<FVoxelTileKey> 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<FVoxelTileKey> DirtyRemeshQueue;
// Set to true during EndPlay — async tasks check this before accessing UObjects
std::atomic<bool> 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<bool> 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<int32> 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<int32> ActiveGenerationWorkerCount{0};
FORCEINLINE bool ShouldAbortWork(const std::atomic<bool>* 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<FVoxelTileKey, FVoxelSharedDensityGrid> 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<FVoxelStreamingAnchor> 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<FVoxelTileKey> CollisionOnlyTiles;
TSet<FVoxelTileKey> 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<FVoxelTileKey> CollisionDesiredTiles;
// Exact pawn-radius box used only by the default-off Step-1 cook sidecar measurement.
TSet<FVoxelTileKey> Step1SupportCohortTiles;
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> 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<FVoxelTileKey> 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<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
TArray<FVoxelTileKey> 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<FVoxelTileKey, TArray<FVoxelTileKey>> 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<uint64> 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<uint64> 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<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples;
TArray<FFarFieldSubmitSample> FarFieldSubmitSamples;
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level.
static constexpr int32 TrackedClassifierLODCount = 9;
std::atomic<uint64> TotalWorkerGenerationCycles{0};
std::atomic<uint64> TotalWorkerGenerationTasks{0};
std::atomic<uint64> TotalObsoleteWorkerCycles{0};
std::atomic<uint64> TotalObsoleteWorkerTasks{0};
std::atomic<uint64> TotalValidationDensityCalls{0};
std::atomic<uint64> TotalMesherDensityCalls{0};
std::atomic<uint64> TotalSealedSolidProofCandidates{0};
std::atomic<uint64> TotalSealedSolidProofSkips{0};
std::atomic<uint64> TotalOutOfLayoutAirProofSkips{0};
std::atomic<uint64> OuterClassifierCallsByLOD[TrackedClassifierLODCount]{};
std::atomic<uint64> 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<FVoxelTileKey, FLoadedTileGeometryStats> 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<FModificationRevision> ModificationRevisions;
TMap<FVoxelTileKey, uint32> AppliedModificationEpoch;
struct FStreamingLatencySample
{
double RequestToReadySeconds = 0.0;
double QueueWaitSeconds = 0.0;
double GenerationSeconds = 0.0;
double CollisionCookSeconds = 0.0;
};
TArray<FStreamingLatencySample> 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<ACameraActor> 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<double> 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<FVoxelTileHashDumpRecord> 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<FVector> 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<APawn> PawnTickPrerequisite;
TWeakObjectPtr<UPawnMovementComponent> PawnMovementTickPrerequisite;
TWeakObjectPtr<APawn> CollisionGatedPawn;
TWeakObjectPtr<UPawnMovementComponent> CollisionGatedPawnMovement;
TWeakObjectPtr<UCharacterMovementComponent> 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<FVoxelTileKey, uint32> 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<FVoxelTileKey> TransitionHold;
TArray<FVoxelTileKey> 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<FVoxelTileKey> 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<FIntVector>& DirtyCoords, const FVoxelTileKey* ExcludeTile = nullptr);
};