tooling(test): one command to test VoxelForge, with a commandlet-vs-game parity check
Tools/VoxelForgeTest.ps1 runs scenarios (canonical, owner, probe, perf, tests, parity) with the known-good flag sets, isolated user/config dirs, serialized launches, an owner-process guard, and a compact result.json + summary.txt instead of 300 MB logs that Codex has to stream. Answers the owner's question 'can the commandlet reflect in-game behaviour': -gameconfig=1 makes the commandlet use the authored settings, and the parity scenario compares per-tile geometry hashes between the commandlet export and the game's streaming path. Parity PASS: 64 commandlet tiles vs 810 game records, no differing tile. First run of the tests scenario: 44 tests, 28 pass, 16 fail (8 composer, 6 op-stack equivalence, PassageLandsInOpenSpace, StrateParamBlendCoverage). Triage is the next round: stale expectations from the floor removal and the degrees change, versus real defects, especially the legacy-vs-op-stack equivalence ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
1c642fd182
commit
a7b8d10de6
10 files changed
+1484
-52
No files matched your search
@@ -14,6 +14,7 @@
|
||||
#include "VoxelDensityVolume.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelGeometryHash.h"
|
||||
#include "VoxelStartupTrace.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "VoxelStats.h"
|
||||
@@ -45,8 +46,11 @@
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Misc/ScopeLock.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "HAL/PlatformMisc.h"
|
||||
#include "Policies/CondensedJsonPrintPolicy.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -1133,6 +1137,7 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
}
|
||||
VoxelCaveMorphology::LogPlayerFitMemoStats();
|
||||
LogStreamingLatencySummary();
|
||||
WriteTileHashDump();
|
||||
const uint64 TilePostReachDifferences =
|
||||
VoxelGenLOD::GSkippedPostDifferences.load(std::memory_order_relaxed);
|
||||
UE_LOG(LogTemp, Display,
|
||||
@@ -1693,6 +1698,9 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
HeadlessStreamingTestLastActualPosition = FVector::ZeroVector;
|
||||
HeadlessStreamingTestBeginSeconds = 0.0;
|
||||
HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||
bTileHashDumpEnabled = false;
|
||||
TileHashDumpPath.Reset();
|
||||
TileHashDumpRecords.Reset();
|
||||
|
||||
bHeadlessStrateCrossingTest = false;
|
||||
bHeadlessStrateCrossingTestStartPlaced = false;
|
||||
@@ -1720,6 +1728,25 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
HeadlessStrateCrossingTestRoute.Reset();
|
||||
|
||||
const TCHAR* CommandLine = FCommandLine::Get();
|
||||
FString RequestedTileHashDumpPath;
|
||||
if (FParse::Value(CommandLine, TEXT("voxel.TileHashDump="), RequestedTileHashDumpPath))
|
||||
{
|
||||
RequestedTileHashDumpPath.TrimQuotesInline();
|
||||
if (FPaths::IsRelative(RequestedTileHashDumpPath))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeParity] voxel.TileHashDump must be an absolute path: '%s'."),
|
||||
*RequestedTileHashDumpPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
TileHashDumpPath = FPaths::ConvertRelativePathToFull(RequestedTileHashDumpPath);
|
||||
FPaths::NormalizeFilename(TileHashDumpPath);
|
||||
bTileHashDumpEnabled = true;
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeParity] tile_hash_dump_enabled=1 path=%s"),
|
||||
*TileHashDumpPath);
|
||||
}
|
||||
}
|
||||
int32 MoveValue = 0;
|
||||
const bool bMoveRequested =
|
||||
(FParse::Value(CommandLine, TEXT("voxel.TestMove="), MoveValue) && MoveValue != 0)
|
||||
@@ -1849,6 +1876,109 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
}
|
||||
}
|
||||
|
||||
void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||
bool bSheetTile, const FVoxelMeshData& MeshData)
|
||||
{
|
||||
if (!bTileHashDumpEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FVoxelTileHashDumpRecord Record;
|
||||
Record.Tile = Tile;
|
||||
Record.OriginVoxels = OriginVoxels;
|
||||
Record.Step = Step;
|
||||
Record.Cells = Cells;
|
||||
Record.BandChunkLo = BandChunkLo;
|
||||
Record.BandChunkHi = BandChunkHi;
|
||||
Record.bSheetTile = bSheetTile;
|
||||
Record.bEmpty = MeshData.IsEmpty();
|
||||
Record.NumTriangles = MeshData.Triangles.Num() / 3;
|
||||
Record.GeometryHash = VoxelForgeGeometryHash::Compute(MeshData);
|
||||
|
||||
FScopeLock Lock(&TileHashDumpMutex);
|
||||
TileHashDumpRecords.Add(MoveTemp(Record));
|
||||
}
|
||||
|
||||
void AVoxelWorld::WriteTileHashDump()
|
||||
{
|
||||
if (!bTileHashDumpEnabled || TileHashDumpPath.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FVoxelTileHashDumpRecord> Records;
|
||||
{
|
||||
FScopeLock Lock(&TileHashDumpMutex);
|
||||
Records = TileHashDumpRecords;
|
||||
}
|
||||
Records.Sort([](const FVoxelTileHashDumpRecord& A, const FVoxelTileHashDumpRecord& B)
|
||||
{
|
||||
if (A.Tile.Level != B.Tile.Level) return A.Tile.Level < B.Tile.Level;
|
||||
if (A.Tile.Coord.Z != B.Tile.Coord.Z) return A.Tile.Coord.Z < B.Tile.Coord.Z;
|
||||
if (A.Tile.Coord.Y != B.Tile.Coord.Y) return A.Tile.Coord.Y < B.Tile.Coord.Y;
|
||||
if (A.Tile.Coord.X != B.Tile.Coord.X) return A.Tile.Coord.X < B.Tile.Coord.X;
|
||||
return A.GeometryHash < B.GeometryHash;
|
||||
});
|
||||
|
||||
IFileManager::Get().MakeDirectory(*FPaths::GetPath(TileHashDumpPath), true);
|
||||
FString Json;
|
||||
TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> Writer =
|
||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("schema_version"), 1);
|
||||
Writer->WriteValue(TEXT("source"), TEXT("game_streaming_path"));
|
||||
Writer->WriteValue(TEXT("settings_asset"), Settings ? Settings->GetPathName() : FString());
|
||||
if (Settings != nullptr)
|
||||
{
|
||||
Writer->WriteObjectStart(TEXT("runtime_config"));
|
||||
Writer->WriteValue(TEXT("generate_skirts"), Settings->bGenerateSkirts);
|
||||
Writer->WriteValue(TEXT("skirt_cells"), static_cast<double>(Settings->SkirtCells));
|
||||
Writer->WriteValue(TEXT("lod_octave_drop"), Settings->LODOctaveDrop);
|
||||
Writer->WriteValue(TEXT("full_res_clip_levels"), Settings->FullResClipLevels);
|
||||
Writer->WriteValue(TEXT("coarse_tile_cells"), Settings->CoarseTileCells);
|
||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"),
|
||||
Settings->GetEffectiveStrateContentCutMinLevel());
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteValue(TEXT("record_count"), Records.Num());
|
||||
Writer->WriteArrayStart(TEXT("records"));
|
||||
for (const FVoxelTileHashDumpRecord& Record : Records)
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("tile_x"), Record.Tile.Coord.X);
|
||||
Writer->WriteValue(TEXT("tile_y"), Record.Tile.Coord.Y);
|
||||
Writer->WriteValue(TEXT("tile_z"), Record.Tile.Coord.Z);
|
||||
Writer->WriteValue(TEXT("level"), Record.Tile.Level);
|
||||
Writer->WriteValue(TEXT("origin_x_voxels"), Record.OriginVoxels.X);
|
||||
Writer->WriteValue(TEXT("origin_y_voxels"), Record.OriginVoxels.Y);
|
||||
Writer->WriteValue(TEXT("origin_z_voxels"), Record.OriginVoxels.Z);
|
||||
Writer->WriteValue(TEXT("step"), Record.Step);
|
||||
Writer->WriteValue(TEXT("cells"), Record.Cells);
|
||||
Writer->WriteValue(TEXT("band_chunk_lo"), Record.BandChunkLo);
|
||||
Writer->WriteValue(TEXT("band_chunk_hi"), Record.BandChunkHi);
|
||||
Writer->WriteValue(TEXT("sheet_tile"), Record.bSheetTile);
|
||||
Writer->WriteValue(TEXT("empty"), Record.bEmpty);
|
||||
Writer->WriteValue(TEXT("triangle_count"), Record.NumTriangles);
|
||||
Writer->WriteValue(TEXT("geometry_hash"), Record.GeometryHash);
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
Writer->WriteObjectEnd();
|
||||
if (!Writer->Close() || !FFileHelper::SaveStringToFile(Json, *TileHashDumpPath))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeParity] Could not write tile hash dump '%s'."),
|
||||
*TileHashDumpPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeParity] tile_hash_dump=%s records=%d"),
|
||||
*TileHashDumpPath, Records.Num());
|
||||
}
|
||||
}
|
||||
|
||||
void AVoxelWorld::InitializeHeadlessStrateCrossingTest()
|
||||
{
|
||||
if (!bHeadlessStrateCrossingTest)
|
||||
@@ -5308,6 +5438,11 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
Result.bHasGroundTris = NumTris > MeshData.NumCeilingTriangles;
|
||||
}
|
||||
|
||||
// Parity instrumentation observes the same FVoxelMeshData that the runtime stream builder
|
||||
// consumes. It is intentionally after the abort fence, so a canceled worker cannot publish a
|
||||
// partial identity, and it records empty/proof tiles as well as visible meshes.
|
||||
RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, bSheetTile, MeshData);
|
||||
|
||||
Result.ClassifySeconds = ClassifySeconds;
|
||||
Result.MeshSeconds = MeshSeconds;
|
||||
Result.StreamSeconds = StreamSeconds;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// VoxelGeometryHash.h
|
||||
// The compact geometry identity shared by offline export and runtime streaming diagnostics.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
namespace VoxelForgeGeometryHash
|
||||
{
|
||||
// Keep this deliberately identical to the historical explorer hash. It is a cheap,
|
||||
// deterministic identity for a tile's complete mesh payload; changing it would invalidate
|
||||
// the commandlet's existing canonical evidence.
|
||||
inline FString Compute(const FVoxelMeshData& MeshData)
|
||||
{
|
||||
uint32 Crc = 0;
|
||||
const int32 Counts[] = {
|
||||
MeshData.Vertices.Num(),
|
||||
MeshData.Normals.Num(),
|
||||
MeshData.UVs.Num(),
|
||||
MeshData.Colors.Num(),
|
||||
MeshData.Triangles.Num(),
|
||||
MeshData.NumCeilingTriangles,
|
||||
};
|
||||
Crc = FCrc::MemCrc32(Counts, sizeof(Counts), Crc);
|
||||
if (MeshData.Vertices.Num() > 0)
|
||||
{
|
||||
Crc = FCrc::MemCrc32(
|
||||
MeshData.Vertices.GetData(),
|
||||
MeshData.Vertices.Num() * sizeof(FVector), Crc);
|
||||
}
|
||||
if (MeshData.Normals.Num() > 0)
|
||||
{
|
||||
Crc = FCrc::MemCrc32(
|
||||
MeshData.Normals.GetData(),
|
||||
MeshData.Normals.Num() * sizeof(FVector), Crc);
|
||||
}
|
||||
if (MeshData.UVs.Num() > 0)
|
||||
{
|
||||
Crc = FCrc::MemCrc32(
|
||||
MeshData.UVs.GetData(),
|
||||
MeshData.UVs.Num() * sizeof(FVector2D), Crc);
|
||||
}
|
||||
if (MeshData.Colors.Num() > 0)
|
||||
{
|
||||
Crc = FCrc::MemCrc32(
|
||||
MeshData.Colors.GetData(),
|
||||
MeshData.Colors.Num() * sizeof(FColor), Crc);
|
||||
}
|
||||
if (MeshData.Triangles.Num() > 0)
|
||||
{
|
||||
Crc = FCrc::MemCrc32(
|
||||
MeshData.Triangles.GetData(),
|
||||
MeshData.Triangles.Num() * sizeof(int32), Crc);
|
||||
}
|
||||
return FString::Printf(TEXT("%08X"), Crc);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "HAL/CriticalSection.h"
|
||||
#include <atomic>
|
||||
#include "VoxelTypes.h"
|
||||
#include "VoxelGenerator.h"
|
||||
@@ -137,6 +138,21 @@ struct FChunkResult
|
||||
TArray<uint8> CaptureGrid;
|
||||
};
|
||||
|
||||
/** 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 bSheetTile = false;
|
||||
bool bEmpty = true;
|
||||
int32 NumTriangles = 0;
|
||||
FString GeometryHash;
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class VOXELFORGE_API AVoxelWorld : public AActor
|
||||
{
|
||||
@@ -512,6 +528,10 @@ private:
|
||||
void ObserveHeadlessStrateCrossingTest(const FVector& PlayerPosition, APawn* PlayerPawn);
|
||||
void MaybeFinishHeadlessStrateCrossingTest();
|
||||
void MaybeFinishHeadlessStreamingTest();
|
||||
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||
bool bSheetTile, 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.
|
||||
@@ -977,6 +997,13 @@ public:
|
||||
double HeadlessStreamingTestBeginSeconds = 0.0;
|
||||
double HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||
|
||||
// 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
|
||||
|
||||
Reference in new issue
Block a user