From 20c6d86f0e0c9360fbfb1aa9daaabe6db25dbdf8 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Fri, 9 Oct 2026 02:12:39 +0200 Subject: [PATCH] wip(assembler): M1 round 4 PARTIAL -- stopped at owner's request (GPU coil whine at night) Unverified mid-round state (cliff plates / horizon slab / routes in progress). Resume from here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- ARCHITECTURE.md | 10 +- CODEMAP.md | 7 + Standalone/Build.ps1 | 15 +- Standalone/Core/API.md | 36 +- .../include/VoxelForgeCore/VoxelForgeCore.h | 60 ++- Standalone/Core/src/FarShellsInternal.h | 33 +- Standalone/Core/src/VoxelForgeCore.cpp | 48 ++- Standalone/Core/src/WorldAssemblerInternal.h | 359 +++++++++++++++++- Standalone/README.md | 20 +- Standalone/Source/GpuFarViewerLinkStubs.cpp | 59 +++ Standalone/Source/StandaloneViewer.cpp | 239 +++++++++++- 11 files changed, 839 insertions(+), 47 deletions(-) create mode 100644 Standalone/Source/GpuFarViewerLinkStubs.cpp diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 855118b..45eb57b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1165,7 +1165,7 @@ choices for the next implementation pass: | TunnelNetwork | Keep the native primitive representation; add missing density operators as measured. | Existing rooms, tunnels, and passage chains work, but the 16-tile probe still changed 2.17% of signs and had a 1.01 m mean sampled surface distance. | | FlatPlain and CrystalChamber | Native slab operator using the already copied slab parameters. | Floor, ceiling, columns, and crystal ceiling are a distinct low-cost structure; generic cave primitives do not express them. | | Maze | Native deterministic cell graph and corridor field using FMazeGenerationParams. | The legacy topology is seed-stable and compact; current generic caves changed 13.34% of signs and had a 5.92 m maximum sampled surface distance. | -| SurfaceWorld | Native Core heightfield long term; retain the exact legacy density bridge temporarily. | The bridge gives exact tested density hashes but runs near owner-field cost and does not add render-side water or biome materials. | +| SurfaceWorld | Keep the exact bridge as the Unreal runtime path until the standalone native evaluator is integrated and owner-reviewed. | The native fixed-scale heightfield, route plan and field sweep now exist in `Standalone/Core`; the plugin season/hash path and in-game review remain open. The bridge still lacks render-side water and biome materials. | | VerticalShafts | Native deterministic shaft/ledge field; use legacy fallback only as a short compatibility step. | Generic cave shapes produced no matching surface in the probe and failed the passage-centerline audit. | | FloatingIslands | Native island SDF with deterministic placement and smooth unions. | The current probe changed 95.75% of signs; an island layout is sparse and does not map well to room/tunnel primitives. | | Underwater | Reuse native TunnelNetwork geometry and add an explicit water render payload. | The density field can reuse cave geometry; water material/plane is separate from signed density and remains absent. | @@ -1178,6 +1178,14 @@ worker path explicit and avoids charging every sample for a second field impleme continues to verify the opt-in Unreal route and its 25 cm Core scale; it does not certify the missing archetypes. +**Standalone SurfaceWorld M1 progress (2026-10-09):** `WorldAssemblyManifest` evaluator 4 now +serializes a finite-strate surface route with stable waypoint IDs, a 0.34 m × 1.76 m player capsule, +a 15° grade limit and underfoot support constraints. The final composed field must pass a capsule +sweep before a route candidate is accepted. Cliff shelves carry a two-Core-voxel minimum thickness. +The route law applies to generation and automatic changes; player carve/fill edits remain unrestricted. +This is standalone Core evidence; it is not yet connected to the season asset/hash path or reviewed in +the owner's game. + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index b92fcfb..80fda75 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -827,6 +827,13 @@ the module and passed `CheckCoreSymbols.ps1` against the actual shim objects. Se [`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) for build and headless evidence and the run ledger. +World assembler field types and `AuditHeightfieldBounds`, `AuditHeightfieldShelfThickness`, and +`AuditHeightfieldRoute` are declared in +[`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`](Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h) +and implemented in [`Standalone/Core/src/WorldAssemblerInternal.h`](Standalone/Core/src/WorldAssemblerInternal.h). +The standalone SurfaceWorld plan composer and capture/audit entry point live in +[`Standalone/Source/StandaloneViewer.cpp`](Standalone/Source/StandaloneViewer.cpp). + `RunStandaloneWorldExport` in `VoxelForgeExploreCommandlet.cpp` serializes owner assets on the game thread to schema-v1 JSON. The adapter parses it through `ResolvedWorldSnapshot::FromJson` and keeps UObjects off Core workers. The runtime switch is `AVoxelWorld::bUseVoxelForgeCore` (default false), diff --git a/Standalone/Build.ps1 b/Standalone/Build.ps1 index 681d979..e029eb3 100644 --- a/Standalone/Build.ps1 +++ b/Standalone/Build.ps1 @@ -1,13 +1,15 @@ [CmdletBinding()] param( [string]$OutputName = 'VoxelForgeCoreStandalone.exe', - [ValidateSet('All','Core')][string]$Target = 'All' + [ValidateSet('All','Core')][string]$Target = 'All', + [ValidateRange(1,16)][int]$CompileWorkers = 16, + [ValidateSet('Normal','BelowNormal')][string]$BuildPriority = 'Normal' ) $ErrorActionPreference = 'Stop' $BuildHostProcess = Get-Process -Id $PID -$BuildHostProcess.PriorityClass = 'BelowNormal' -$BuildHostProcess.ProcessorAffinity = [IntPtr]0xFF00 +$BuildHostProcess.PriorityClass = $BuildPriority +$BuildHostProcess.ProcessorAffinity = [IntPtr]0xFFFF $Root = [IO.Path]::GetFullPath($PSScriptRoot) $BuildDir = Join-Path $Root 'Build\VoxelForgeCore' $BinDir = Join-Path $Root 'bin' @@ -72,7 +74,7 @@ $IncludeArgs = @( ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Public')), ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private')) ) -$CommonFlags = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/MP8','/D_CRT_SECURE_NO_WARNINGS') + $IncludeArgs +$CommonFlags = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc',("/MP{0}" -f $CompileWorkers),'/D_CRT_SECURE_NO_WARNINGS') + $IncludeArgs $CoreRsp = Join-Path $BuildDir 'core-compile.rsp' $CoreRspLines = $CommonFlags + @('/c') + @($CompileSources | ForEach-Object { '"{0}"' -f $_ }) [IO.File]::WriteAllLines($CoreRsp,$CoreRspLines,[Text.UTF8Encoding]::new($false)) @@ -89,6 +91,7 @@ if ($Target -eq 'All') { if ($CoreSourceNames[$Index].Replace('\','/') -eq 'Core/src/VoxelForgeCore.cpp') { continue } $LegacySources += $CoreSources[$Index] } + $LegacySources += (Join-Path $Root 'Source\GpuFarViewerLinkStubs.cpp') $LegacyObjects = @($LegacySources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) }) $LegacyRspLines = $CommonFlags + @('/c') + @($LegacySources | ForEach-Object { '"{0}"' -f $_ }) [IO.File]::WriteAllLines($LegacyRsp,$LegacyRspLines,[Text.UTF8Encoding]::new($false)) @@ -104,8 +107,8 @@ $ViewerRspLines = $CommonFlags + @('/Fe"{0}"' -f $OutputPath) + $LegacyObjects + Push-Location $BuildDir try { - # One cl process with /MP8 bounds compiler work to eight processes; MSVC, lib.exe, and the - # viewer link inherit BelowNormal priority and the 0xFF00 process affinity from this host. + # MSVC's /MP count and the build host's Normal-priority 16-CPU mask are configurable for + # isolated BuildHost work; the viewer process uses its own run-time worker setting. $CommandLine = 'call "{0}" >nul && cl.exe @"{1}" && lib.exe @"{2}"' -f $VcVars,$CoreRsp,$LibRsp & $env:ComSpec /d /s /c $CommandLine if ($LASTEXITCODE -eq -1073741502) { throw 'A build process exited with 0xC0000142; stopping as required.' } diff --git a/Standalone/Core/API.md b/Standalone/Core/API.md index 4f601a6..157c4e6 100644 --- a/Standalone/Core/API.md +++ b/Standalone/Core/API.md @@ -79,14 +79,15 @@ Pass `analyticOnly = false` to retain the reference packed-page compiler. Packed parent-cell coordinates and an 8-voxel apron; each covers a 128-voxel cube or one larger coarse tile. This path remains available for parity checks and workloads that need the packed lattice. -## World assembler manifest v1 (prototype; evaluator v1 and v2) +## World assembler manifest v1 (evaluator v1–v4) `ResolvedWorldSnapshot::FromJson` accepts an optional `world_assembly` object alongside the owner snapshot. The object is the shipped-plan payload shape: a fixed-scale recipe and finite-strate plan -with hashes, no generated mesh or voxel samples. The current evaluator-v2 review payload is -[`round2/world_assembly.json`](../Research/WorldAssemblerM1/round2/world_assembly.json); the -evaluator-v1 sample remains at [`round1-manifest.json`](../Research/WorldAssemblerM1/round1-manifest.json). -Round 4 still needs to connect this payload to the season asset/hash path. +with hashes, no generated mesh or voxel samples. The current review payload is written to +[`round4/world_assembly.json`](../Research/WorldAssemblerM1/round4/world_assembly.json). +Evaluator 1–3 payloads remain readable with their original hashes and field semantics. Evaluator 4 +adds a minimum two-voxel shelf thickness and a versioned route support contract; it requires at +least two route waypoints. Every coordinate and scalar uses signed integer units of 1/256 voxel (`units_per_voxel: 256`), independent of the plugin's 25 cm Core voxel and of tile size. The schema version is 1; evaluator @@ -96,8 +97,10 @@ order and hashed as 4- or 8-byte little-endian words with FNV-1a 64. Hashes are lowercase hexadecimal characters; JSON key order and whitespace do not affect them. The root, recipe, plan and each stratum carry a content hash. The recipe stores its seed, warped biome-map settings and one to eight biome profiles; the plan stores finite-strate extents, cap settings, -stable-ID features, fixed-season water and route points. Bounds, feature count and route count are -validated before a manifest is accepted. +stable-ID features, fixed-season water, route points and (for evaluator 4) player-capsule, grade, +support and sweep-spacing constraints. Route points are fixed-scale capsule-center locations with +stable IDs and a protected automatic-change corridor radius. Bounds, feature count, route count, route +IDs, corridor widths and finite-strate capsule extents are validated before a manifest is accepted. Evaluator 2 adds climate-based biome assignment, output-space blending of both ground and cap profiles, per-biome relief and height warp, beach width, cliff limits, and optional per-biome cap @@ -120,8 +123,11 @@ neighbor biome IDs, Q16 blend weight and signed density. Evaluator 2 also return surface and water-fill sample values. Negative density is solid. Its contract reads `biome` and writes `height`, `rock`, `water` and `waterSurface`; route and `surfaceTags` remain declared channels for later blocks. The detail envelope reports maximum absolute displacement and minimum feature -size. Influence is global in XY and limited to the finite stratum's Z extent, except for the -explicitly bounded evaluator-v2 feature boxes. +size. Evaluator 4 defines the shelf's minimum solid thickness as two Core voxels (0.5 m) and applies +its feather to the horizontal footprint so it cannot thin the roof-to-floor section. +`AuditHeightfieldShelfThickness` samples shelf sections against the pre-feature field and includes +the previous sub-voxel taper as a negative control. Influence is global in XY and limited to the +finite stratum's Z extent, except for the explicitly bounded feature boxes. `interval(minXQ, minYQ, minZQ, maxXQ, maxYQ, maxZQ)` returns a conservative density range or `known == false`; callers must keep evaluating when it is unknown. The current heightfield range @@ -131,9 +137,19 @@ negative-coordinate and overhang boxes, and includes a deliberately false one-si negative control. A pass means no sampled point escaped and the negative control was rejected; it is an audit of this block implementation, not a proof for future blocks. +`AuditHeightfieldRoute` sweeps the recorded route through the final composed field using the +versioned player capsule (0.34 m radius, 1.76 m height), checks a 15-degree maximum grade, samples +capsule clearance at no more than half-voxel intervals, and probes support below five foot locations +at each swept center. Automatic changes must be composed first and pass this audit before the plan is +accepted. Player carve/fill edits remain unrestricted. This validates analytic field collision and +support; it does not cook an Unreal physics mesh. + ## Queries and tiles -`densityAt(position)` returns signed density; negative is solid and nonnegative is air. +`densityAt(position)` returns signed density; negative is solid and nonnegative is air. Server and +authority code can compile/query a `World` and run `AuditHeightfieldRoute` without calling +`generateTile`; generated render meshes are not part of this query path. Unreal physics collision +cooking remains the adapter's separate readiness step. `isSolidAt` wraps that sign test. `isSolidAlongSegment(start, end, spacing)` samples evenly along the segment with no interval wider than `spacing` voxels; it returns true if any sample is solid. `classifyBox(origin, step, cells)` returns `allSolid`, `allAir` or `mixed` for mesher tile diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index ea7a183..db55a7d 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -82,6 +82,8 @@ namespace VoxelForgeCore double meshingMilliseconds = 0.0; bool tileWasClassified = false; bool meshingSkipped = false; + bool featureRefinedResolution = false; + std::uint64_t farCapTrianglesSuppressed = 0; }; struct TileMesh @@ -126,7 +128,7 @@ namespace VoxelForgeCore // L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles. inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1; - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 3; + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 4; enum class FieldChannel : std::uint32_t { @@ -193,11 +195,26 @@ namespace VoxelForgeCore struct WorldAssemblyRoutePointV1 { + // Stable ID and fixed-scale capsule-center waypoint. Radius is the protected + // automatic-change corridor around this centerline point. std::uint64_t stableFeatureId = 0; std::array positionSubvoxels{}; std::int64_t radiusSubvoxels = 0; }; + struct WorldAssemblyRouteConstraintsV1 + { + std::uint32_t version = 0; + std::int64_t capsuleRadiusSubvoxels = 0; + std::int64_t capsuleHeightSubvoxels = 0; + std::int64_t maximumGradeQ16 = 0; + std::int64_t sweepSpacingSubvoxels = 0; + std::int64_t supportProbeRadiusSubvoxels = 0; + std::int64_t supportSearchDepthSubvoxels = 0; + std::int64_t minimumClearanceSubvoxels = 0; + std::uint32_t minimumSupportProbes = 0; + }; + struct WorldAssemblyFeatureV1 { // Stable numeric type IDs are part of the plan contract: 1 is a heightfield cliff plane, @@ -228,6 +245,7 @@ namespace VoxelForgeCore std::int64_t waterSurfaceSubvoxels = 0; bool waterEnabled = false; std::vector features; + WorldAssemblyRouteConstraintsV1 routeConstraints; std::vector route; std::uint64_t contentHash = 0; }; @@ -357,10 +375,50 @@ namespace VoxelForgeCore bool passed = false; }; + struct WorldAssemblyShelfThicknessAuditResult + { + std::uint64_t featuresTested = 0; + std::uint64_t columnsTested = 0; + std::uint64_t activeColumns = 0; + std::uint64_t negativeRuns = 0; + std::uint64_t subVoxelColumns = 0; + std::uint64_t subVoxelRuns = 0; + std::uint64_t legacySubVoxelColumns = 0; + std::uint64_t legacySubVoxelRuns = 0; + std::int64_t minimumObservedThicknessSubvoxels = 0; + std::int64_t minimumThicknessColumnXSubvoxels = 0; + std::int64_t minimumThicknessColumnYSubvoxels = 0; + std::int64_t minimumThicknessGroundSubvoxels = 0; + std::int64_t minimumThicknessBottomSubvoxels = 0; + std::int64_t minimumThicknessTopSubvoxels = 0; + bool negativeControlDetected = false; + bool passed = false; + }; + + struct WorldAssemblyRouteAuditResult + { + std::uint64_t segmentsTested = 0; + std::uint64_t centerSamples = 0; + std::uint64_t capsuleFieldSamples = 0; + std::uint64_t blockedCapsuleSamples = 0; + std::uint64_t supportProbeSamples = 0; + std::uint64_t unsupportedCenterSamples = 0; + std::uint64_t gradeFailures = 0; + std::int64_t minimumClearanceSubvoxels = 0; + std::int64_t maximumSupportDepthSubvoxels = 0; + bool passed = false; + }; + VFCORE_API WorldAssemblyBoundsAuditResult AuditHeightfieldBounds( const Heightfield2D& block, std::uint32_t boxCount = 256, std::uint64_t seed = 0x574f524c44415331ull); + VFCORE_API WorldAssemblyShelfThicknessAuditResult AuditHeightfieldShelfThickness( + const Heightfield2D& block); + + VFCORE_API WorldAssemblyRouteAuditResult AuditHeightfieldRoute( + const Heightfield2D& block); + class VFCORE_API ResolvedWorldSnapshot { public: diff --git a/Standalone/Core/src/FarShellsInternal.h b/Standalone/Core/src/FarShellsInternal.h index 7f7b874..aedfc45 100644 --- a/Standalone/Core/src/FarShellsInternal.h +++ b/Standalone/Core/src/FarShellsInternal.h @@ -967,11 +967,38 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta }; const int32 Base=static_cast(NewVertices.size()); const double Xs[4]={X0,X1,X1,X0},Ys[4]={Y0,Y0,Y1,Y1}; + std::array CornerZ{}; + std::array CornerNormals{}; + bool bValidPatch=true; for(int32 Corner=0;Corner<4;++Corner) { - const double Z=SurfaceZ(Xs[Corner],Ys[Corner]); - NewVertices.emplace_back(Xs[Corner]*25.0,Ys[Corner]*25.0,Z*25.0); - NewNormals.push_back(Normal); + const double PredictedZ=SurfaceZ(Xs[Corner],Ys[Corner]); + const std::vector CornerCrossings=FindFarStratumCrossings( + Shape,Xs[Corner],Ys[Corner],ZMin,ZMax); + if(CornerCrossings.empty()){bValidPatch=false;break;} + CornerZ[Corner]=*std::min_element(CornerCrossings.begin(),CornerCrossings.end(), + [PredictedZ](double A,double B) + {return std::abs(A-PredictedZ)((Xs[Corner]-Origin.X)/256.0), static_cast((Ys[Corner]-Origin.Y)/256.0)); if(bHasColors)NewColors.push_back(Color); diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 512d0db..f75cf88 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -627,12 +627,42 @@ namespace VoxelForgeCore mesher.bGenerateSkirts=world.bGenerateSkirts;mesher.SkirtCells=static_cast(world.SkirtCells); mesher.LODOctaveDrop=world.LODOctaveDrop; const int32 level=key.lod,extent=CHUNK_SIZE< densityValues; const FVoxelTileKey nativeKey=ToNative(key); const FIntVector origin=nativeKey.OriginVoxels(); + int32 cells=levelworldAssembly&&level<=2) + { + constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const std::array tileMin={ + static_cast(origin.X)*Scale, + static_cast(origin.Y)*Scale, + static_cast(origin.Z)*Scale}; + const std::array tileMax={ + static_cast(origin.X+extent)*Scale, + static_cast(origin.Y+extent)*Scale, + static_cast(origin.Z+extent)*Scale}; + for(const auto& stratum:impl_->worldAssembly->plan.strates) + for(const auto& feature:stratum.features) + { + if(feature.type!=4)continue; + bool overlaps=true; + for(std::size_t axis=0;axis<3;++axis) + overlaps=overlaps&&feature.minimumSubvoxels[axis]<=tileMax[axis] + &&feature.maximumSubvoxels[axis]>=tileMin[axis]; + if(overlaps) + { + // A two-voxel shelf is only one sample wide at LOD1's normal step. + // Mesh feature-intersecting tiles at the one-voxel Core lattice so + // the full-resolution block cannot turn into a one-cell plate. + cells=extent;step=1;bFeatureRefinedResolution=true;break; + } + } + } + int64 sampleCount=0;std::vector densityValues; TileMesh out;out.key=key; + out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution; const bool bHasWorldAssembly=impl_->worldAssembly!=nullptr; const bool bNeedsFarShell=bHasWorldAssembly&&key.lod>=7 &&(tileShape.HasFarWorld()||impl_->shape->HasFarWorld()); @@ -691,6 +721,18 @@ namespace VoxelForgeCore AppendFarStratumShells(world,farShape,nativeKey,step,mesh); AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh); } + if(bHasWorldAssembly&&key.lod>=8&&mesh.NumCeilingTriangles>0) + { + // The finite sky cap remains in the composed field, queries, and local meshes. + // At the coarsest clipmap level its large planar triangles read as a detached + // horizon slab, so omit only that far render group after far-shell construction. + const int32 KeptTriangleCount=std::max(0,mesh.Triangles.Num()/3-mesh.NumCeilingTriangles); + const int32 KeptIndexCount=KeptTriangleCount*3; + out.diagnostics.farCapTrianglesSuppressed=static_cast(mesh.NumCeilingTriangles); + mesh.Triangles.SetNum(KeptIndexCount); + mesh.TriangleSkirtFaceMasks.SetNum(KeptTriangleCount); + mesh.NumCeilingTriangles=0; + } out.densitySampleCount=static_cast(std::max(0,sampleCount)); if(bHasWorldAssembly) diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index e35a8d9..49c13f9 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -16,6 +16,7 @@ namespace VoxelForgeCore namespace world_assembly_detail { constexpr std::int64_t Q16 = 1ll << 16; + constexpr std::int64_t MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel; constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll; constexpr std::size_t MaxBiomeCount = 8; constexpr std::size_t MaxStrateCount = 64; @@ -228,6 +229,60 @@ namespace world_assembly_detail || !std::all_of(point.positionSubvoxels.begin(), point.positionSubvoxels.end(), ValidScalar)) { error = "world assembly route point has an invalid feature ID, position or radius"; return false; } } + std::vector routeIds; + routeIds.reserve(stratum.route.size()); + for (const auto& point : stratum.route) + { + if (std::find(routeIds.begin(), routeIds.end(), point.stableFeatureId) != routeIds.end()) + { error = "world assembly route point IDs must be unique"; return false; } + routeIds.push_back(point.stableFeatureId); + } + for (std::size_t i = 1; i < stratum.route.size(); ++i) + { + std::uint64_t squared = 0; + for (std::size_t axis = 0; axis < 3; ++axis) + { + const std::int64_t delta = stratum.route[i].positionSubvoxels[axis] + - stratum.route[i - 1].positionSubvoxels[axis]; + if (std::abs(delta) > 4096 * WorldAssemblySubvoxelsPerVoxel) + { error = "world assembly route segment exceeds its bounded sweep length"; return false; } + squared += static_cast(delta * delta); + } + if (squared == 0) + { error = "world assembly route contains a zero-length segment"; return false; } + } + const auto& constraints = stratum.routeConstraints; + if (constraints.version == 0) + { + if (constraints.capsuleRadiusSubvoxels != 0 || constraints.capsuleHeightSubvoxels != 0 + || constraints.maximumGradeQ16 != 0 || constraints.sweepSpacingSubvoxels != 0 + || constraints.supportProbeRadiusSubvoxels != 0 + || constraints.supportSearchDepthSubvoxels != 0 + || constraints.minimumClearanceSubvoxels != 0 || constraints.minimumSupportProbes != 0) + { error = "world assembly route constraints need a supported version"; return false; } + } + else if (constraints.version != 1 + || !ValidScalar(constraints.capsuleRadiusSubvoxels) + || !ValidScalar(constraints.capsuleHeightSubvoxels) + || !ValidScalar(constraints.maximumGradeQ16) + || !ValidScalar(constraints.sweepSpacingSubvoxels) + || !ValidScalar(constraints.supportProbeRadiusSubvoxels) + || !ValidScalar(constraints.supportSearchDepthSubvoxels) + || !ValidScalar(constraints.minimumClearanceSubvoxels) + || constraints.capsuleRadiusSubvoxels <= 0 + || constraints.capsuleHeightSubvoxels < 2 * constraints.capsuleRadiusSubvoxels + || constraints.maximumGradeQ16 <= 0 || constraints.maximumGradeQ16 > Q16 + || constraints.sweepSpacingSubvoxels <= 0 + || constraints.sweepSpacingSubvoxels > WorldAssemblySubvoxelsPerVoxel / 2 + || constraints.supportProbeRadiusSubvoxels <= 0 + || constraints.supportSearchDepthSubvoxels <= 0 + || constraints.minimumClearanceSubvoxels < 0 + || constraints.minimumSupportProbes == 0 || constraints.minimumSupportProbes > 5) + { error = "world assembly route support constraints are outside the supported range"; return false; } + if (constraints.version == 1) + for (const auto& point : stratum.route) + if (point.radiusSubvoxels < constraints.capsuleRadiusSubvoxels) + { error = "world assembly route corridor is narrower than its player capsule"; return false; } return true; } @@ -310,6 +365,20 @@ namespace world_assembly_detail for (const auto value : feature.maximumSubvoxels) HashWord(hash, static_cast(value), 8); for (const auto value : feature.parametersSubvoxels) HashWord(hash, static_cast(value), 8); } + if (stratum.routeConstraints.version != 0) + { + const auto& constraints = stratum.routeConstraints; + HashWord(hash, 0x524f5554455631ull, 8); + HashWord(hash, constraints.version, 4); + HashWord(hash, static_cast(constraints.capsuleRadiusSubvoxels), 8); + HashWord(hash, static_cast(constraints.capsuleHeightSubvoxels), 8); + HashWord(hash, static_cast(constraints.maximumGradeQ16), 8); + HashWord(hash, static_cast(constraints.sweepSpacingSubvoxels), 8); + HashWord(hash, static_cast(constraints.supportProbeRadiusSubvoxels), 8); + HashWord(hash, static_cast(constraints.supportSearchDepthSubvoxels), 8); + HashWord(hash, static_cast(constraints.minimumClearanceSubvoxels), 8); + HashWord(hash, constraints.minimumSupportProbes, 4); + } HashWord(hash, stratum.route.size(), 4); for (const auto& point : stratum.route) { @@ -440,6 +509,24 @@ namespace world_assembly_detail { error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; } if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false); if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0; + const VFJson::Value& routeConstraintsValue = item.Get("route_constraints"); + if (!std::holds_alternative(routeConstraintsValue.Data)) + { + auto& constraints = stratum.routeConstraints; + std::uint32_t minimumSupportProbes = 0; + if (!routeConstraintsValue.IsObject() + || !ReadU32(routeConstraintsValue.Get("version"), constraints.version) + || !ReadInteger(routeConstraintsValue.Get("capsule_radius_q"), constraints.capsuleRadiusSubvoxels) + || !ReadInteger(routeConstraintsValue.Get("capsule_height_q"), constraints.capsuleHeightSubvoxels) + || !ReadInteger(routeConstraintsValue.Get("maximum_grade_q16"), constraints.maximumGradeQ16) + || !ReadInteger(routeConstraintsValue.Get("sweep_spacing_q"), constraints.sweepSpacingSubvoxels) + || !ReadInteger(routeConstraintsValue.Get("support_probe_radius_q"), constraints.supportProbeRadiusSubvoxels) + || !ReadInteger(routeConstraintsValue.Get("support_search_depth_q"), constraints.supportSearchDepthSubvoxels) + || !ReadInteger(routeConstraintsValue.Get("minimum_clearance_q"), constraints.minimumClearanceSubvoxels) + || !ReadU32(routeConstraintsValue.Get("minimum_support_probes"), minimumSupportProbes)) + { error = "world assembly route constraints are missing a fixed-scale field"; return false; } + constraints.minimumSupportProbes = minimumSupportProbes; + } for (const auto& featureValue : item.Get("features").AsArray()) { WorldAssemblyFeatureV1 feature; @@ -1109,7 +1196,7 @@ namespace world_assembly_detail inline std::int64_t CliffShelfDensity(const WorldAssemblyFeatureV1& feature, const WorldAssemblyRecipeV1& recipe,const WorldAssemblyStratumPlanV1& plan, std::int64_t x,std::int64_t y,std::int64_t z, - const HeightfieldPointSample& localColumn) + const HeightfieldPointSample& localColumn,bool enforceMinimumThickness) { const auto& lo=feature.minimumSubvoxels;const auto& hi=feature.maximumSubvoxels; if(xhi[0]||yhi[1]||zhi[2]) @@ -1134,17 +1221,36 @@ namespace world_assembly_detail return std::numeric_limits::max()/4; const std::uint32_t vertical=SmoothQ16(static_cast( std::clamp(aboveGround*Q16/height,0,Q16))); - const std::int64_t sourceOffset=MulQ16(reach,vertical); + // Evaluator v4 samples the source column on the shelf's root line. The offset + // therefore follows horizontal progress through the block and stays fixed for + // every z slice. Advancing it with z can make the shelf switch on at its top; + // sampling the far end everywhere instead can pull a distant cliff cap across + // the whole shelf. Earlier evaluators retain their height-dependent taper. + const std::int64_t sourceOffset=enforceMinimumThickness + ?std::clamp(along,0,reach):MulQ16(reach,vertical); const std::int64_t sourceX=x-dirX*sourceOffset; const std::int64_t sourceY=y-dirY*sourceOffset; const HeightfieldPointSample source=CachedColumn(recipe,plan,sourceX,sourceY); if(source.groundHeightSubvoxels<=localColumn.groundHeightSubvoxels) return std::numeric_limits::max()/4; - std::int64_t candidate=std::max({localColumn.groundHeightSubvoxels-z, - z-source.groundHeightSubvoxels,-along,along-reach, - std::abs(lateral)-halfWidth,-aboveGround,aboveGround-height}); const std::uint32_t edgeFade=FeatureFadeQ16(feature,x,y,Feather); - candidate+=static_cast(Q16-edgeFade)*Feather/Q16; + const std::int64_t edgeInset=static_cast(Q16-edgeFade)*Feather/Q16; + const std::int64_t sourceTop=enforceMinimumThickness + ?std::max(source.groundHeightSubvoxels, + localColumn.groundHeightSubvoxels+MinimumCliffShelfThicknessSubvoxels) + :source.groundHeightSubvoxels; + std::int64_t candidate=std::max({localColumn.groundHeightSubvoxels-z, + z-sourceTop,-aboveGround,aboveGround-height}); + if(enforceMinimumThickness) + { + // The feather narrows the shelf footprint. It must not offset the top and + // bottom surfaces, or their vertical separation collapses into MC sheets. + candidate=std::max({candidate,-along+edgeInset,along-reach+edgeInset, + std::abs(lateral)-halfWidth+edgeInset}); + } + else + candidate=std::max({candidate,-along,along-reach, + std::abs(lateral)-halfWidth})+edgeInset; return candidate; } @@ -1192,6 +1298,26 @@ bool WorldAssemblyManifest::finalize(std::string& error) for (auto& stratum : plan.strates) { if (!world_assembly_detail::ValidateStratum(stratum, error)) return false; + if (recipe.evaluatorVersion >= 4) + { + if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2) + { error = "world assembly evaluator 4 requires a versioned entrance-to-exit route"; return false; } + const std::int64_t bottomZ = static_cast(stratum.bottomChunkZ) + * 32 * WorldAssemblySubvoxelsPerVoxel; + const std::int64_t topZ = (static_cast(stratum.topChunkZ) + 1) + * 32 * WorldAssemblySubvoxelsPerVoxel; + const std::int64_t halfHeight = stratum.routeConstraints.capsuleHeightSubvoxels / 2; + for (const auto& point : stratum.route) + { + if (point.positionSubvoxels[2] - halfHeight < bottomZ + || point.positionSubvoxels[2] + halfHeight > topZ) + { error = "world assembly route capsule escapes its finite stratum Z bounds"; return false; } + } + for (const auto& feature : stratum.features) + if (feature.type == 4 && feature.parametersSubvoxels[2] + < 2 * WorldAssemblySubvoxelsPerVoxel) + { error = "world assembly evaluator 4 cliff shelf is thinner than its two-voxel minimum"; return false; } + } if (std::find(strateIds.begin(), strateIds.end(), stratum.strateIndex) != strateIds.end()) { error = "world assembly plan contains a duplicate strate index"; return false; } strateIds.push_back(stratum.strateIndex); @@ -1322,7 +1448,21 @@ std::string WorldAssemblyManifest::ToJson() const << feature.parametersSubvoxels[0] << ',' << feature.parametersSubvoxels[1] << ',' << feature.parametersSubvoxels[2] << ',' << feature.parametersSubvoxels[3] << "]}"; } - out << "],\"route\":["; + out << ']'; + if (stratum.routeConstraints.version != 0) + { + const auto& constraints = stratum.routeConstraints; + out << ",\"route_constraints\":{\"version\":" << constraints.version + << ",\"capsule_radius_q\":" << constraints.capsuleRadiusSubvoxels + << ",\"capsule_height_q\":" << constraints.capsuleHeightSubvoxels + << ",\"maximum_grade_q16\":" << constraints.maximumGradeQ16 + << ",\"sweep_spacing_q\":" << constraints.sweepSpacingSubvoxels + << ",\"support_probe_radius_q\":" << constraints.supportProbeRadiusSubvoxels + << ",\"support_search_depth_q\":" << constraints.supportSearchDepthSubvoxels + << ",\"minimum_clearance_q\":" << constraints.minimumClearanceSubvoxels + << ",\"minimum_support_probes\":" << constraints.minimumSupportProbes << '}'; + } + out << ",\"route\":["; for (std::size_t p = 0; p < stratum.route.size(); ++p) { const auto& point = stratum.route[p]; @@ -1391,7 +1531,8 @@ HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y, } else if(feature.type==4&&recipe_->evaluatorVersion>=3) { - const std::int64_t shelf=CliffShelfDensity(feature,*recipe_,*plan_,x,y,z,out); + const std::int64_t shelf=CliffShelfDensity(feature,*recipe_,*plan_,x,y,z,out, + recipe_->evaluatorVersion>=4); out.densitySubvoxels=SmoothMinSubvoxels(out.densitySubvoxels,shelf, 4*WorldAssemblySubvoxelsPerVoxel); } @@ -1592,6 +1733,9 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept (feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2})); if(feature.type==4)maximum=std::max(maximum,feature.parametersSubvoxels[2] +WorldAssemblySubvoxelsPerVoxel); + if(feature.type==4&&recipe_->evaluatorVersion>=4) + minimumFeature=std::min(minimumFeature, + world_assembly_detail::MinimumCliffShelfThicknessSubvoxels); if(feature.type==3)maximum=std::max(maximum,std::abs(feature.parametersSubvoxels[0])); } out.maximumAbsoluteDisplacementSubvoxels = maximum; @@ -1746,4 +1890,203 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block && result.unknownBounds == 0 && result.negativeControlDetected; return result; } + +WorldAssemblyShelfThicknessAuditResult AuditHeightfieldShelfThickness(const Heightfield2D& block) +{ + WorldAssemblyShelfThicknessAuditResult result; + const auto* recipe=block.recipe(); + const auto* plan=block.plan(); + if(!block.isValid()||!recipe||!plan||recipe->evaluatorVersion<4)return result; + constexpr std::int64_t sampleStep=WorldAssemblySubvoxelsPerVoxel/4; + constexpr std::int64_t xyStep=WorldAssemblySubvoxelsPerVoxel/2; + std::int64_t minimumObserved=std::numeric_limits::max(); + for(std::size_t featureIndex=0;featureIndexfeatures.size();++featureIndex) + { + const auto& feature=plan->features[featureIndex]; + if(feature.type!=4)continue; + ++result.featuresTested; + auto baselinePlan=*plan; + baselinePlan.features.erase(baselinePlan.features.begin()+featureIndex); + const Heightfield2D baseline(*recipe,baselinePlan); + const std::int64_t firstX=feature.minimumSubvoxels[0]+WorldAssemblySubvoxelsPerVoxel; + const std::int64_t lastX=feature.maximumSubvoxels[0]-WorldAssemblySubvoxelsPerVoxel; + const std::int64_t firstY=feature.minimumSubvoxels[1]+WorldAssemblySubvoxelsPerVoxel; + const std::int64_t lastY=feature.maximumSubvoxels[1]-WorldAssemblySubvoxelsPerVoxel; + for(std::int64_t y=firstY;y<=lastY;y+=xyStep) + for(std::int64_t x=firstX;x<=lastX;x+=xyStep) + { + ++result.columnsTested; + const std::int64_t ground=baseline.point(x,y,0).groundHeightSubvoxels; + const std::int64_t range=std::max(feature.parametersSubvoxels[2], + world_assembly_detail::MinimumCliffShelfThicknessSubvoxels)+sampleStep; + std::int64_t currentRun=0,legacyCurrentRun=0; + std::int64_t currentStart=0,legacyCurrentStart=0; + bool columnActive=false,columnSubVoxel=false,legacyColumnSubVoxel=false; + const auto recordRun=[&](std::int64_t run,std::int64_t start,bool legacy) + { + if(run<=0)return; + if(legacy) + { + if(run::max()) + result.minimumObservedThicknessSubvoxels=minimumObserved; + result.negativeControlDetected=result.legacySubVoxelColumns>0; + result.passed=result.featuresTested>0&&result.activeColumns>0 + &&result.minimumObservedThicknessSubvoxels>=world_assembly_detail::MinimumCliffShelfThicknessSubvoxels + &&result.subVoxelRuns==0&&result.negativeControlDetected; + return result; +} + +WorldAssemblyRouteAuditResult AuditHeightfieldRoute(const Heightfield2D& block) +{ + WorldAssemblyRouteAuditResult result; + const auto* plan=block.plan(); + if(!block.isValid()||!plan||plan->routeConstraints.version!=1||plan->route.size()<2) + return result; + using namespace world_assembly_detail; + const auto& constraints=plan->routeConstraints; + const std::int64_t radius=constraints.capsuleRadiusSubvoxels; + const std::int64_t halfHeight=constraints.capsuleHeightSubvoxels/2; + const std::int64_t axisHalf=std::max(0,halfHeight-radius); + const std::int64_t spacing=std::min(constraints.sweepSpacingSubvoxels, + WorldAssemblySubvoxelsPerVoxel/2); + const std::int64_t supportSpacing=std::min(spacing, + WorldAssemblySubvoxelsPerVoxel/4); + std::int64_t minimumClearance=std::numeric_limits::max(); + std::int64_t maximumSupportDepth=0; + const std::array,5> supportOffsets{{ + {{0,0}},{{constraints.supportProbeRadiusSubvoxels,0}}, + {{-constraints.supportProbeRadiusSubvoxels,0}}, + {{0,constraints.supportProbeRadiusSubvoxels}}, + {{0,-constraints.supportProbeRadiusSubvoxels}}}}; + + const auto sweepCapsule=[&](const std::array& center) + { + ++result.centerSamples; + const std::int64_t footZ=center[2]-halfHeight; + const std::int64_t firstDz=-halfHeight+constraints.minimumClearanceSubvoxels; + for(std::int64_t dz=firstDz;dz<=halfHeight;dz+=spacing) + { + const std::int64_t capOffset=std::max(0,std::abs(dz)-axisHalf); + const std::int64_t radialSquared=radius*radius-capOffset*capOffset; + if(radialSquared<0)continue; + for(std::int64_t oy=-radius;oy<=radius;oy+=spacing) + for(std::int64_t ox=-radius;ox<=radius;ox+=spacing) + { + if(ox*ox+oy*oy>radialSquared)continue; + const auto sample=block.point(center[0]+ox,center[1]+oy,center[2]+dz); + ++result.capsuleFieldSamples; + minimumClearance=std::min(minimumClearance,sample.densitySubvoxels); + if(sample.densitySubvoxelsroute.size();++segment) + { + ++result.segmentsTested; + const auto& from=plan->route[segment-1].positionSubvoxels; + const auto& to=plan->route[segment].positionSubvoxels; + const std::int64_t dx=to[0]-from[0],dy=to[1]-from[1],dz=to[2]-from[2]; + const std::uint64_t horizontalSquared=static_cast(dx*dx+dy*dy); + const std::uint64_t horizontalLength=IntegerSqrt(horizontalSquared); + const std::uint64_t rise=static_cast(std::abs(dz)); + if(horizontalLength==0||rise*static_cast(Q16) + >horizontalLength*static_cast(constraints.maximumGradeQ16)) + ++result.gradeFailures; + const std::uint64_t length=IntegerSqrt(horizontalSquared + +static_cast(dz*dz)); + const std::uint64_t sampleCount=std::max(1, + (length+static_cast(spacing)-1)/static_cast(spacing)); + const std::uint64_t first=segment==1?0:1; + for(std::uint64_t i=first;i<=sampleCount;++i) + { + const auto interpolate=[&](std::size_t axis) + { + const std::int64_t delta=to[axis]-from[axis]; + return from[axis]+delta*static_cast(i) + /static_cast(sampleCount); + }; + sweepCapsule({interpolate(0),interpolate(1),interpolate(2)}); + } + } + result.minimumClearanceSubvoxels=minimumClearance==std::numeric_limits::max() + ?0:minimumClearance; + result.maximumSupportDepthSubvoxels=maximumSupportDepth; + result.passed=result.segmentsTested>0&&result.centerSamples>0 + &&result.blockedCapsuleSamples==0&&result.unsupportedCenterSamples==0 + &&result.gradeFailures==0; + return result; +} } diff --git a/Standalone/README.md b/Standalone/README.md index 22c595f..4f4de3f 100644 --- a/Standalone/README.md +++ b/Standalone/README.md @@ -1,7 +1,7 @@ # VoxelForge standalone world This C++20 program loads the owner snapshot from `data/owner_world.json`, runs the VoxelForge CPU -pipeline, streams the clipmap with up to eight host worker threads, and displays it through Direct3D +pipeline, streams the clipmap with up to sixteen host worker threads, and displays it through Direct3D 11. `Core/` contains the Unreal-free `VoxelForgeCore` API and static-library target used by the standalone shape-world path. Unreal is not linked or needed at run time. @@ -16,18 +16,18 @@ From the plugin root, run: `Build.ps1 -Target Core` builds only `Build\VoxelForgeCore\VoxelForgeCore.lib`; `-Target All` builds that library and the viewer. `Core\core_sources.txt` is the shared translation-unit -manifest. The build uses the configured VS 2022 `vcvars64.bat`, MSVC `/std:c++20`, `/O2`, -`/arch:AVX2`, `/fp:precise` and `/MP8`, BelowNormal priority and affinity `0xFF00`. Pass another -executable name to build beside any viewer that may be open by setting `-OutputName`, for example +manifest. The defaults use MSVC `/std:c++20`, `/O2`, `/arch:AVX2`, `/fp:precise`, `/MP16`, Normal +priority and affinity `0xFFFF`. Use `-CompileWorkers 8 -BuildPriority BelowNormal` for a lower-impact +build. Pass another executable name to build beside any viewer that may be open by setting `-OutputName`, for example `VoxelForgeCoreStandalone_test.exe`. The executable finds the JSON under `Standalone\data\`. +The isolated viewer links `Source\GpuFarViewerLinkStubs.cpp`; GPU Far rendering is not part of this +CPU viewer target. -### Daytime resource limit +### BuildHost resource controls -Run audits and benchmarks at BelowNormal priority and use no more than 8 of this PC's 16 logical -CPUs. The viewer clamps worker requests to eight. `Build.ps1` bounds MSVC with `/MP8`, sets the -build process to BelowNormal and affinity `0xFF00`, and retains `/fp:precise`. The verification -runner in `Core\verification\RunMachine.ps1` applies the same limits to viewer runs and refuses to -overwrite an existing log stem. +The standalone viewer accepts `--workers=1..16`, `--process-priority=Normal` and +`--affinity-all-cpu`. The verification runner in `Core\verification\RunMachine.ps1` retains its +separate BelowNormal/8-worker limits and refuses to overwrite an existing log stem. ## Controls diff --git a/Standalone/Source/GpuFarViewerLinkStubs.cpp b/Standalone/Source/GpuFarViewerLinkStubs.cpp new file mode 100644 index 0000000..62fa455 --- /dev/null +++ b/Standalone/Source/GpuFarViewerLinkStubs.cpp @@ -0,0 +1,59 @@ +#include "../Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h" + +// The isolated CPU viewer does not compile the GPU Far implementation. These link stubs keep +// that optional path unavailable while preserving the Core and CPU mesh/capture paths. +namespace VoxelForgeGpuFarViewer +{ + std::filesystem::path ExecutablePrecompiledShaderDirectory(const std::filesystem::path& path) + { return path.parent_path(); } + bool InitializeFarPipeline(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&,std::string& error) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool QueueFarShaderWarmup(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&, + bool,const std::string&,bool,std::string& error) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool CompleteFarShaderWarmup(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&,std::string& error) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool ReserveFarOutputPool(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&, + std::uint32_t,std::uint32_t,std::string& error,std::uint32_t) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool ReadFarTileRequiredVertexCounts(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&, + const std::vector&,std::vector&,std::string& error) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool ReadFarTileOutputStatuses(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&, + const std::vector&,std::vector&,std::vector&, + std::string& error) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool QueryFarGpuMemory(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&, + std::uint64_t& usage,std::uint64_t& budget,std::string& error) + { usage=budget=0;error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool BeginFarTileBatch(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&,std::string& error) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool BuildFarTile(ID3D11Device*,ID3D11DeviceContext*,VoxelForgeCore::World&, + const VoxelForgeCore::TileKey&,const VoxelForgeCore::FarFieldTileInput*, + const std::filesystem::path&,ID3D11Buffer** vertices,ID3D11Buffer** indices, + ID3D11Buffer** args,ID3D11Buffer** connectorVertices,ID3D11Buffer** connectorIndices, + std::uint32_t,std::uint32_t,std::uint32_t* argsOffset,ID3D11Query** completion, + GpuTimingQueries*,CpuMesh*,double* enqueueMs,double* dispatchMs,std::string& error, + bool,std::uint32_t,std::uint32_t) + { + if(vertices)*vertices=nullptr; + if(indices)*indices=nullptr; + if(args)*args=nullptr; + if(connectorVertices)*connectorVertices=nullptr; + if(connectorIndices)*connectorIndices=nullptr; + if(argsOffset)*argsOffset=0; + if(completion)*completion=nullptr; + if(enqueueMs)*enqueueMs=0.0; + if(dispatchMs)*dispatchMs=0.0; + error="GPU far rendering is excluded from the isolated CPU viewer"; + return false; + } + bool SubmitFarTileBatch(ID3D11Device*,ID3D11DeviceContext*,const std::filesystem::path&, + ID3D11Query**,GpuTimingQueries*,std::string& error) + { error="GPU far rendering is excluded from the isolated CPU viewer";return false; } + bool ResolveGpuTiming(ID3D11DeviceContext*,GpuTimingQueries&,double& dispatchMs, + std::uint64_t*,std::uint64_t*,std::uint64_t*) + { dispatchMs=0.0;return false; } + bool ResolveGpuTimestampPair(ID3D11DeviceContext*,GpuTimingQueries&,std::uint64_t,double& dispatchMs) + { dispatchMs=0.0;return false; } +} diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index ad0720a..5cb13f3 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -7878,6 +7878,152 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; return hash?hash:1; } + bool BuildFiniteStrateRoute(VoxelForgeCore::WorldAssemblyRecipeV1& recipe, + VoxelForgeCore::WorldAssemblyStratumPlanV1& stratum,std::uint64_t& tested, + std::uint64_t& gradeRejected,std::uint64_t& sweepRejected,std::string& error) + { + using namespace VoxelForgeCore; + constexpr std::int64_t PlayerRadiusQ=348; // 0.34 m in Core's 0.25 m voxel scale. + constexpr std::int64_t PlayerHeightQ=1802; // 1.76 m in Core's 0.25 m voxel scale. + constexpr std::int64_t HalfHeightQ=PlayerHeightQ/2; + constexpr std::int64_t StepQ=128; // 0.5 Core voxel, finer than the density lattice. + constexpr std::int64_t ProbeRadiusQ=174; + constexpr std::int64_t SupportDepthQ=256; + constexpr std::int64_t GradeQ16=17560; // tan(15 degrees), rounded upward. + constexpr std::int64_t RouteRadiusQ=4*WorldAssemblySubvoxelsPerVoxel; + struct Direction {std::int32_t x,y;}; + constexpr std::array directions{{ + {65536,0},{46341,46341},{0,65536},{-46341,46341}, + {-65536,0},{-46341,-46341},{0,-65536},{46341,-46341}}}; + struct Candidate + { + std::int64_t score=0; + std::int32_t centerX=0,centerY=0,direction=0; + std::vector points; + }; + + auto& constraints=stratum.routeConstraints; + constraints.version=1; + constraints.capsuleRadiusSubvoxels=PlayerRadiusQ; + constraints.capsuleHeightSubvoxels=PlayerHeightQ; + constraints.maximumGradeQ16=GradeQ16; + constraints.sweepSpacingSubvoxels=StepQ; + constraints.supportProbeRadiusSubvoxels=ProbeRadiusQ; + constraints.supportSearchDepthSubvoxels=SupportDepthQ; + constraints.minimumClearanceSubvoxels=8; + constraints.minimumSupportProbes=3; + + const Heightfield2D finalField(recipe,stratum); + if(!finalField.isValid()) + {error="route planner received an invalid final composed field";return false;} + std::vector candidates; + constexpr std::int32_t halfLength=128; + constexpr std::int32_t waypointSpacing=8; + for(std::int32_t centerY=-256;centerY<=256;centerY+=64) + for(std::int32_t centerX=-256;centerX<=256;centerX+=64) + for(std::int32_t directionIndex=0;directionIndex(directions.size());++directionIndex) + { + Candidate candidate; + candidate.centerX=centerX;candidate.centerY=centerY;candidate.direction=directionIndex; + candidate.points.reserve(33); + std::int64_t previousGround=0,previousX=0,previousY=0; + bool gradePass=true; + for(std::int32_t index=0;index<=32;++index) + { + const std::int64_t along=(index-16)*waypointSpacing; + const std::int64_t x=centerX+std::llround( + static_cast(along)*directions[directionIndex].x/65536.0); + const std::int64_t y=centerY+std::llround( + static_cast(along)*directions[directionIndex].y/65536.0); + const auto column=finalField.point(WorldAssemblyFixed(static_cast(x)), + WorldAssemblyFixed(static_cast(y)),0); + const std::int64_t horizontalQ=static_cast(std::llround( + std::hypot(static_cast(x-previousX),static_cast(y-previousY)) + *WorldAssemblySubvoxelsPerVoxel)); + if(index>0) + { + const std::int64_t riseQ=std::abs(column.groundHeightSubvoxels-previousGround); + if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16) + {gradePass=false;break;} + candidate.score+=riseQ*65536/std::max(1,horizontalQ); + } + WorldAssemblyRoutePointV1 point; + point.positionSubvoxels={WorldAssemblyFixed(static_cast(x)), + WorldAssemblyFixed(static_cast(y)),column.groundHeightSubvoxels+HalfHeightQ}; + point.radiusSubvoxels=RouteRadiusQ; + point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed, + static_cast(x),static_cast(y), + 0x52540000u+static_cast(index)); + candidate.points.push_back(point); + previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y; + } + ++tested; + if(gradePass)candidates.push_back(std::move(candidate)); + else ++gradeRejected; + } + std::stable_sort(candidates.begin(),candidates.end(),[](const Candidate& a,const Candidate& b) + { + if(a.score!=b.score)return a.score0) + for(std::size_t pointIndex=0;pointIndex0) + std::printf("[WorldAssemblerRouteRepaired] uniform_lift_q=%lld support_limit_q=%lld\n", + static_cast(liftQ),static_cast(SupportDepthQ)); + return true; + } + if(audit.blockedCapsuleSamples==0||liftQ>=MaximumRepairLiftQ)break; + const std::int64_t needed=std::max(1, + constraints.minimumClearanceSubvoxels-audit.minimumClearanceSubvoxels); + const std::int64_t steps=(needed+RepairStepQ-1)/RepairStepQ; + const std::int64_t nextLift=std::max(liftQ+RepairStepQ,liftQ+steps*RepairStepQ); + if(nextLift>MaximumRepairLiftQ)break; + liftQ=nextLift; + ++repairAttempt; + } + if(sweepAttempt<3) + std::printf("[WorldAssemblerRouteCandidateRejected] ordinal=%llu blocked=%llu unsupported=%llu grade_failures=%llu minimum_clearance_q=%lld\n", + static_cast(sweepAttempt), + static_cast(lastAudit.blockedCapsuleSamples), + static_cast(lastAudit.unsupportedCenterSamples), + static_cast(lastAudit.gradeFailures), + static_cast(lastAudit.minimumClearanceSubvoxels)); + ++sweepAttempt; + ++sweepRejected; + } + std::printf("[WorldAssemblerRouteSearchFailed] tested=%llu grade_rejected=%llu sweepable_candidates=%llu sweep_rejected=%llu last_blocked=%llu last_unsupported=%llu last_grade_failures=%llu\n", + static_cast(tested),static_cast(gradeRejected), + static_cast(candidates.size()),static_cast(sweepRejected), + static_cast(lastAudit.blockedCapsuleSamples), + static_cast(lastAudit.unsupportedCenterSamples), + static_cast(lastAudit.gradeFailures)); + std::printf("[WorldAssemblerRouteRepairSummary] repair_attempts=%llu\n", + static_cast(repairAttempt)); + stratum.route.clear(); + error="finite-strate route candidates failed the composed-field capsule and support sweep"; + return false; + } + bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root, std::string& error,const std::string& manifestOutputPath) { @@ -8067,9 +8213,25 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; stratum.features.push_back(shelf); } } + std::uint64_t routeCandidates=0,routeGradeRejected=0,routeSweepRejected=0; + if(!BuildFiniteStrateRoute(recipe,stratum,routeCandidates,routeGradeRejected, + routeSweepRejected,error))return false; + std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u status=PASS\n", + static_cast(routeCandidates), + static_cast(routeGradeRejected), + static_cast(routeSweepRejected),stratum.route.size(), + stratum.routeConstraints.minimumSupportProbes); manifest.plan.strates.push_back(std::move(stratum)); if(!manifest.finalize(error))return false; const std::string manifestText=manifest.ToJson(); + const auto roundTrip=VoxelForgeCore::WorldAssemblyManifest::FromJson(manifestText,error); + if(!roundTrip||!roundTrip->validate(error)||roundTrip->ToJson()!=manifestText + ||roundTrip->contentHash!=manifest.contentHash) + {if(error.empty())error="world assembly canonical plan serialization did not round-trip";return false;} + std::printf("[WorldAssemblerPlanSerialization] bytes=%zu canonical_round_trip=true route_points=%zu plan_hash=%016llx manifest_hash=%016llx status=PASS\n", + manifestText.size(),manifest.plan.strates.front().route.size(), + static_cast(manifest.plan.contentHash), + static_cast(manifest.contentHash)); if(!manifestOutputPath.empty()) { const std::filesystem::path outputPath(manifestOutputPath); @@ -8134,6 +8296,55 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; static_cast(bounds.exceptionPointSamples), bounds.negativeControlDetected?"true":"false",bounds.passed?"PASS":"FAIL"); + const auto shelfThickness=VoxelForgeCore::AuditHeightfieldShelfThickness(block); + std::printf("[WorldAssemblerShelfThicknessAudit] features=%llu columns=%llu active_columns=%llu negative_runs=%llu minimum_observed_vox=%.3f sub_voxel_runs=%llu sub_voxel_columns=%llu legacy_sub_voxel_runs=%llu legacy_sub_voxel_control_columns=%llu negative_control_detected=%s status=%s\n", + static_cast(shelfThickness.featuresTested), + static_cast(shelfThickness.columnsTested), + static_cast(shelfThickness.activeColumns), + static_cast(shelfThickness.negativeRuns), + static_cast(shelfThickness.minimumObservedThicknessSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, + static_cast(shelfThickness.subVoxelRuns), + static_cast(shelfThickness.subVoxelColumns), + static_cast(shelfThickness.legacySubVoxelRuns), + static_cast(shelfThickness.legacySubVoxelColumns), + shelfThickness.negativeControlDetected?"true":"false", + shelfThickness.passed?"PASS":"FAIL"); + std::printf("[WorldAssemblerShelfMinimumColumn] x_vox=%.3f y_vox=%.3f ground_z_vox=%.3f run_bottom_z_vox=%.3f run_top_z_vox=%.3f\n", + static_cast(shelfThickness.minimumThicknessColumnXSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, + static_cast(shelfThickness.minimumThicknessColumnYSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, + static_cast(shelfThickness.minimumThicknessGroundSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, + static_cast(shelfThickness.minimumThicknessBottomSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, + static_cast(shelfThickness.minimumThicknessTopSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + + const auto routeAudit=VoxelForgeCore::AuditHeightfieldRoute(block); + auto raisedRoutePlan=plan; + for(auto& point:raisedRoutePlan.route) + point.positionSubvoxels[2]+=16*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const VoxelForgeCore::Heightfield2D raisedRouteBlock(manifest->recipe,raisedRoutePlan); + const auto raisedRouteAudit=VoxelForgeCore::AuditHeightfieldRoute(raisedRouteBlock); + const bool routeSupportNegativeControl=!raisedRouteAudit.passed + &&raisedRouteAudit.unsupportedCenterSamples>0; + std::printf("[WorldAssemblerRouteSweep] points=%zu segments=%llu swept_centers=%llu capsule_field_samples=%llu blocked_samples=%llu support_probes=%llu unsupported_centers=%llu grade_failures=%llu minimum_clearance_q=%lld maximum_support_depth_q=%lld unsupported_control_centers=%llu negative_control_detected=%s collision_readiness=%s status=%s\n", + plan.route.size(),static_cast(routeAudit.segmentsTested), + static_cast(routeAudit.centerSamples), + static_cast(routeAudit.capsuleFieldSamples), + static_cast(routeAudit.blockedCapsuleSamples), + static_cast(routeAudit.supportProbeSamples), + static_cast(routeAudit.unsupportedCenterSamples), + static_cast(routeAudit.gradeFailures), + static_cast(routeAudit.minimumClearanceSubvoxels), + static_cast(routeAudit.maximumSupportDepthSubvoxels), + static_cast(raisedRouteAudit.unsupportedCenterSamples), + routeSupportNegativeControl?"true":"false", + routeAudit.passed?"analytic_field_sweep_ready":"blocked", + routeAudit.passed&&routeSupportNegativeControl?"PASS":"FAIL"); + bool cliffRegressionPassing=false; const auto cliffIt=std::find_if(plan.features.begin(),plan.features.end(), [](const auto& feature){return feature.type==4;}); @@ -8496,12 +8707,26 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; coreQueryMaximumError=std::max(coreQueryMaximumError,std::abs(expected-actual)); ++coreQuerySamples; } + for(const auto& routePoint:plan.route) + { + const auto& position=routePoint.positionSubvoxels; + const double x=static_cast(position[0])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const double y=static_cast(position[1])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const double z=static_cast(position[2])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const double expected=static_cast(block.point(position[0],position[1],position[2]) + .densitySubvoxels)/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const double actual=coreWorld.densityAt({x,y,z}); + coreQueryMaximumError=std::max(coreQueryMaximumError,std::abs(expected-actual)); + ++coreQuerySamples; + } } - const bool coreQueryPass=coreCompiled&&coreQuerySamples==45&&coreQueryMaximumError<=1.0/256.0; - std::printf("[WorldAssemblerCoreQuery] compiled=%s compile_ms=%.3f samples=%llu max_abs_error=%.8f render_meshes_generated=0 readiness=%s status=%s error=%s\n", + const std::uint64_t expectedCoreQuerySamples=45+plan.route.size(); + const bool coreQueryPass=coreCompiled&&coreQuerySamples==expectedCoreQuerySamples + &&coreQueryMaximumError<=1.0/256.0; + std::printf("[WorldAssemblerServerFieldQuery] role=server_api_path=World::densityAt compiled=%s compile_ms=%.3f samples=%llu route_waypoints=%zu max_abs_error=%.8f render_meshes_generated=0 local_field_query=%s collision_mesh_cooking=not_in_standalone_core status=%s error=%s\n", coreCompiled?"true":"false",coreCompileMs, - static_cast(coreQuerySamples),coreQueryMaximumError, - coreQueryPass?"field_ready_without_meshes":"unavailable",coreQueryPass?"PASS":"FAIL", + static_cast(coreQuerySamples),plan.route.size(),coreQueryMaximumError, + coreQueryPass?"ready_without_meshes":"unavailable",coreQueryPass?"PASS":"FAIL", coreError.empty()?"none":coreError.c_str()); if(!captureDirectory.empty()) { @@ -8582,7 +8807,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; ownerSaved&&nativeSaved?"PASS":"FAIL"); } std::printf("[WorldAssemblerFullViewTiming] status=NOT_MEASURED_IN_AUDIT_MODE; capture_viewer_runs_report_separately\n"); - return bounds.passed&&cliffRegressionPassing&&seamClassificationPassing + std::printf("[WorldAssemblerReadiness] local_field_query=%s analytic_collision_validation=%s physics_collision_cooking=deferred_to_Unreal_adapter render_mesh_readiness=reported_by_matched_view_runs\n", + coreQueryPass?"ready_without_render_meshes":"not_ready", + routeAudit.passed?"route_sweep_ready":"not_ready"); + return bounds.passed&&shelfThickness.passed&&routeAudit.passed + &&routeSupportNegativeControl&&cliffRegressionPassing&&seamClassificationPassing &&deterministic&&coreQueryPass?0:11; } }