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
@@ -11,7 +11,6 @@
|
||||
#include "IImageWrapper.h"
|
||||
#include "IImageWrapperModule.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Parse.h"
|
||||
#include "Misc/Paths.h"
|
||||
@@ -32,6 +31,7 @@
|
||||
#include "VoxelDensityAblation.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelGeometryHash.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
@@ -339,6 +339,9 @@ struct FExploreArguments
|
||||
// density_grid_reuse=false to produce a matched wall-clock control.
|
||||
bool bReuseDensityGrid = true;
|
||||
bool bBlockEarlyOut = false;
|
||||
// Opt-in parity mode: copy the runtime mesher presentation/LOD settings from the authored
|
||||
// settings object. The default remains the historical bounded export configuration.
|
||||
bool bGameRuntimeConfig = false;
|
||||
int32 MeshMinBatchSize = 1;
|
||||
int32 MaxWalkCells = DefaultMaxWalkCells;
|
||||
float BudgetMinutes = DefaultBudgetMinutes;
|
||||
@@ -470,6 +473,10 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
int32 BlockEarlyOut = OutArguments.bBlockEarlyOut ? 1 : 0;
|
||||
FParse::Value(*Params, TEXT("blockearlyout="), BlockEarlyOut);
|
||||
OutArguments.bBlockEarlyOut = BlockEarlyOut != 0;
|
||||
int32 GameRuntimeConfig = OutArguments.bGameRuntimeConfig ? 1 : 0;
|
||||
FParse::Value(*Params, TEXT("gameconfig="), GameRuntimeConfig);
|
||||
FParse::Value(*Params, TEXT("paritygame="), GameRuntimeConfig);
|
||||
OutArguments.bGameRuntimeConfig = GameRuntimeConfig != 0;
|
||||
FParse::Value(*Params, TEXT("meshminbatch="), OutArguments.MeshMinBatchSize);
|
||||
OutArguments.MeshMinBatchSize = FMath::Clamp(OutArguments.MeshMinBatchSize, 1, 64);
|
||||
int32 Lod = 0;
|
||||
@@ -686,11 +693,27 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OutArguments.Slot < 1 || OutArguments.Slot > MaxSyntheticStrates - 2)
|
||||
// The historical explorer contract needs two adjacent mouths around an interior slot. A
|
||||
// parity export can instead target the game's authored single-strate layout, which has no
|
||||
// such mouths and is valid for export-only mode. Build() still validates the slot against
|
||||
// the loaded settings layout.
|
||||
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1;
|
||||
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
|
||||
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
|
||||
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."),
|
||||
MaxSyntheticStrates - 2);
|
||||
if (OutArguments.bGameRuntimeConfig)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("slot must be in [0,%d] for game-runtime configuration parity."),
|
||||
MaxSyntheticStrates - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."),
|
||||
MaxSyntheticStrates - 2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.RenderWidth < 16 || OutArguments.RenderHeight < 16
|
||||
@@ -1127,7 +1150,9 @@ struct FExploreWorld
|
||||
}
|
||||
|
||||
const int32 NumStrates = bUseAuthoredSettings
|
||||
? FMath::Max(Settings->TotalStrates, Arguments.Slot + 2)
|
||||
? (Arguments.bGameRuntimeConfig
|
||||
? FMath::Max(Settings->TotalStrates, 1)
|
||||
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
|
||||
: Arguments.Slot + 2;
|
||||
if (NumStrates > MaxSyntheticStrates)
|
||||
{
|
||||
@@ -1397,6 +1422,12 @@ struct FExploreWorld
|
||||
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
Mesher->SetGenerator(Generator.Get());
|
||||
Mesher->bUseBlockEarlyOut = Arguments.bBlockEarlyOut;
|
||||
if (Arguments.bGameRuntimeConfig)
|
||||
{
|
||||
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
|
||||
Mesher->SkirtCells = Settings->SkirtCells;
|
||||
Mesher->LODOctaveDrop = Settings->LODOctaveDrop;
|
||||
}
|
||||
|
||||
if (Settings->WorldRadiusVoxels != 0.0f
|
||||
|| Generator->WorldRadiusVoxels != 0.0f
|
||||
@@ -2048,47 +2079,7 @@ bool AppendCanonicalMesh(
|
||||
|
||||
FString ComputeGeometryHash(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);
|
||||
return VoxelForgeGeometryHash::Compute(MeshData);
|
||||
}
|
||||
|
||||
bool EnsureExploreMesh(
|
||||
@@ -2153,9 +2144,14 @@ bool EnsureExploreMesh(
|
||||
}
|
||||
|
||||
// This is the same canonical UVoxelMarchingCubesMesher used by export and runtime chunk
|
||||
// generation. Skirts are presentation geometry for LOD seams and would extend outside the
|
||||
// bounded explorer region, so the shared mesh uses the export setting.
|
||||
World.Mesher->bGenerateSkirts = false;
|
||||
// generation. Historical export deliberately omitted presentation skirts; parity opts into
|
||||
// the authored runtime settings so the requested comparison has the same mesher inputs.
|
||||
World.Mesher->bGenerateSkirts = Arguments.bGameRuntimeConfig
|
||||
? World.Settings->bGenerateSkirts : false;
|
||||
World.Mesher->SkirtCells = Arguments.bGameRuntimeConfig
|
||||
? World.Settings->SkirtCells : 2.0f;
|
||||
World.Mesher->LODOctaveDrop = Arguments.bGameRuntimeConfig
|
||||
? World.Settings->LODOctaveDrop : 0;
|
||||
|
||||
const double MeshStartSeconds = FPlatformTime::Seconds();
|
||||
if (Budget.ShouldStop(TEXT("mesh")))
|
||||
@@ -6320,6 +6316,7 @@ FString BuildExploreJson(
|
||||
Writer->WriteValue(TEXT("export_step"), Arguments.ExportStep);
|
||||
Writer->WriteValue(TEXT("density_grid_reuse"), Arguments.bReuseDensityGrid);
|
||||
Writer->WriteValue(TEXT("block_early_out"), Arguments.bBlockEarlyOut);
|
||||
Writer->WriteValue(TEXT("game_runtime_config"), Arguments.bGameRuntimeConfig);
|
||||
Writer->WriteValue(TEXT("mesh_min_batch_size"), Arguments.MeshMinBatchSize);
|
||||
Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity);
|
||||
Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull);
|
||||
@@ -6532,6 +6529,18 @@ FString BuildExploreJson(
|
||||
Writer->WriteValue(TEXT("x"), TileX);
|
||||
Writer->WriteValue(TEXT("y"), TileY);
|
||||
Writer->WriteValue(TEXT("z"), TileZ);
|
||||
Writer->WriteValue(TEXT("absolute_tile_x"),
|
||||
World.ExploreMeshOrigin.X / CHUNK_SIZE + TileX);
|
||||
Writer->WriteValue(TEXT("absolute_tile_y"),
|
||||
World.ExploreMeshOrigin.Y / CHUNK_SIZE + TileY);
|
||||
Writer->WriteValue(TEXT("absolute_tile_z"),
|
||||
World.ExploreMeshOrigin.Z / CHUNK_SIZE + TileZ);
|
||||
Writer->WriteValue(TEXT("origin_x_voxels"),
|
||||
World.ExploreMeshOrigin.X + TileX * CHUNK_SIZE);
|
||||
Writer->WriteValue(TEXT("origin_y_voxels"),
|
||||
World.ExploreMeshOrigin.Y + TileY * CHUNK_SIZE);
|
||||
Writer->WriteValue(TEXT("origin_z_voxels"),
|
||||
World.ExploreMeshOrigin.Z + TileZ * CHUNK_SIZE);
|
||||
Writer->WriteValue(TEXT("sample_step"), Arguments.ExportStep);
|
||||
Writer->WriteValue(TEXT("lod"), TileLod);
|
||||
Writer->WriteValue(TEXT("triangle_count"), World.ExploreTileTriangleCounts[TileIndex]);
|
||||
|
||||
Reference in new issue
Block a user