diff --git a/Standalone/Build.ps1 b/Standalone/Build.ps1 index cb41a9b..681d979 100644 --- a/Standalone/Build.ps1 +++ b/Standalone/Build.ps1 @@ -38,6 +38,32 @@ foreach ($Source in $CoreSources) { if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Required core source is missing: $Source" } } +# Keep the canonical source manifest shared with UBT, but compile every copied implementation +# source through a generated one-source shim that places its Unreal-shaped internals in a private +# namespace. The public API implementation owns its boundary and is compiled directly. +$WrapperDir = Join-Path $BuildDir 'wrappers' +[IO.Directory]::CreateDirectory($WrapperDir) | Out-Null +$CompileSources = [Collections.Generic.List[string]]::new() +foreach ($Index in 0..($CoreSources.Count-1)) { + $ManifestName = $CoreSourceNames[$Index].Replace('\','/') + if ($ManifestName -eq 'Core/src/VoxelForgeCore.cpp') { + $CompileSources.Add($CoreSources[$Index]) + continue + } + $WrapperName = 'vfcore_' + ($ManifestName -replace '[^A-Za-z0-9]+','_') + $WrapperPath = Join-Path $WrapperDir ($WrapperName + '.cpp') + $SourceInclude = $CoreSources[$Index].Replace('\','/') + $WrapperText = @" +#include "CoreNamespacePreamble.h" +namespace vfcore::detail +{ +#include "$SourceInclude" +} +"@ + [IO.File]::WriteAllText($WrapperPath,$WrapperText,[Text.UTF8Encoding]::new($false)) + $CompileSources.Add($WrapperPath) +} + $IncludeArgs = @( ('/I"{0}"' -f (Join-Path $Root 'Core\include')), ('/I"{0}"' -f (Join-Path $Root 'Core\src')), @@ -48,16 +74,28 @@ $IncludeArgs = @( ) $CommonFlags = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/MP8','/D_CRT_SECURE_NO_WARNINGS') + $IncludeArgs $CoreRsp = Join-Path $BuildDir 'core-compile.rsp' -$CoreRspLines = $CommonFlags + @('/c') + @($CoreSources | ForEach-Object { '"{0}"' -f $_ }) +$CoreRspLines = $CommonFlags + @('/c') + @($CompileSources | ForEach-Object { '"{0}"' -f $_ }) [IO.File]::WriteAllLines($CoreRsp,$CoreRspLines,[Text.UTF8Encoding]::new($false)) -$Objects = @($CoreSources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) }) +$Objects = @($CompileSources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) }) $LibRsp = Join-Path $BuildDir 'core-library.rsp' [IO.File]::WriteAllLines($LibRsp,@('/nologo',('/OUT:"{0}"' -f $CoreLibrary)) + $Objects,[Text.UTF8Encoding]::new($false)) +$LegacySources = @() +$LegacyObjects = @() +$LegacyRsp = Join-Path $BuildDir 'viewer-reference-compile.rsp' +if ($Target -eq 'All') { + for ($Index=0; $Index -lt $CoreSources.Count; ++$Index) { + if ($CoreSourceNames[$Index].Replace('\','/') -eq 'Core/src/VoxelForgeCore.cpp') { continue } + $LegacySources += $CoreSources[$Index] + } + $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)) +} + $ViewerRsp = Join-Path $BuildDir 'viewer-link.rsp' -$ViewerRspLines = $CommonFlags + @( - ('/Fe"{0}"' -f $OutputPath), +$ViewerRspLines = $CommonFlags + @('/Fe"{0}"' -f $OutputPath) + $LegacyObjects + @( ('"{0}"' -f (Join-Path $Root 'Source\StandaloneViewer.cpp')), ('"{0}"' -f $CoreLibrary), '/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib' @@ -69,11 +107,20 @@ 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. $CommandLine = 'call "{0}" >nul && cl.exe @"{1}" && lib.exe @"{2}"' -f $VcVars,$CoreRsp,$LibRsp - if ($Target -eq 'All') { $CommandLine += ' && cl.exe @"{0}"' -f $ViewerRsp } & $env:ComSpec /d /s /c $CommandLine if ($LASTEXITCODE -eq -1073741502) { throw 'A build process exited with 0xC0000142; stopping as required.' } if ($LASTEXITCODE -ne 0) { throw "VoxelForgeCore build failed with exit code $LASTEXITCODE" } if (-not (Test-Path -LiteralPath $CoreLibrary -PathType Leaf)) { throw "Core library was not produced: $CoreLibrary" } + $SymbolCheck = Join-Path $Root 'Core\CheckCoreSymbols.ps1' + & $SymbolCheck -LibraryPath $CoreLibrary + if ($Target -eq 'All') { + # The standalone viewer retains a global compatibility build only for legacy comparison + # and audits. Those objects are linked into the viewer executable, never into Core.lib. + $CommandLine = 'call "{0}" >nul && cl.exe @"{1}" && cl.exe @"{2}"' -f $VcVars,$LegacyRsp,$ViewerRsp + & $env:ComSpec /d /s /c $CommandLine + if ($LASTEXITCODE -eq -1073741502) { throw 'A build process exited with 0xC0000142; stopping as required.' } + if ($LASTEXITCODE -ne 0) { throw "Standalone viewer build failed with exit code $LASTEXITCODE" } + } if ($Target -eq 'All' -and -not (Test-Path -LiteralPath $OutputPath -PathType Leaf)) { throw "Viewer was not produced: $OutputPath" } } finally { Pop-Location diff --git a/Standalone/Compat/Json.h b/Standalone/Compat/Json.h index 8c62911..46ceee1 100644 --- a/Standalone/Compat/Json.h +++ b/Standalone/Compat/Json.h @@ -1,8 +1,18 @@ #pragma once #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include namespace VFJson { @@ -27,7 +37,7 @@ namespace VFJson return It == O.end() ? Null : It->second; } double Number(double Fallback = 0.0) const { return IsNumber() ? std::get(Data) : Fallback; } - int64 Integer(int64 Fallback = 0) const { return IsNumber() ? static_cast(std::get(Data)) : Fallback; } + std::int64_t Integer(std::int64_t Fallback = 0) const { return IsNumber() ? static_cast(std::get(Data)) : Fallback; } bool Boolean(bool Fallback = false) const { return std::holds_alternative(Data) ? std::get(Data) : Fallback; } const std::string& String() const { static const std::string Empty; return IsString() ? std::get(Data) : Empty; } }; @@ -49,23 +59,23 @@ namespace VFJson size_t Position = 0; void SkipSpace() { while (Position < Text.size() && static_cast(Text[Position]) <= 0x20) ++Position; } bool Consume(char C) { SkipSpace(); if (Position < Text.size() && Text[Position] == C) { ++Position; return true; } return false; } - static void AppendUtf8(std::string& Out, uint32 Code) + static void AppendUtf8(std::string& Out, std::uint32_t Code) { if (Code <= 0x7f) Out.push_back(static_cast(Code)); else if (Code <= 0x7ff) { Out.push_back(static_cast(0xc0 | (Code >> 6))); Out.push_back(static_cast(0x80 | (Code & 0x3f))); } else if (Code <= 0xffff) { Out.push_back(static_cast(0xe0 | (Code >> 12))); Out.push_back(static_cast(0x80 | ((Code >> 6) & 0x3f))); Out.push_back(static_cast(0x80 | (Code & 0x3f))); } else { Out.push_back(static_cast(0xf0 | (Code >> 18))); Out.push_back(static_cast(0x80 | ((Code >> 12) & 0x3f))); Out.push_back(static_cast(0x80 | ((Code >> 6) & 0x3f))); Out.push_back(static_cast(0x80 | (Code & 0x3f))); } } - bool Hex4(uint32& Out) + bool Hex4(std::uint32_t& Out) { if (Position + 4 > Text.size()) return false; Out = 0; - for (int32 I = 0; I < 4; ++I) + for (std::int32_t I = 0; I < 4; ++I) { const char C = Text[Position++]; - const int32 N = (C >= '0' && C <= '9') ? C - '0' : (C >= 'a' && C <= 'f') ? C - 'a' + 10 : (C >= 'A' && C <= 'F') ? C - 'A' + 10 : -1; + const std::int32_t N = (C >= '0' && C <= '9') ? C - '0' : (C >= 'a' && C <= 'f') ? C - 'a' + 10 : (C >= 'A' && C <= 'F') ? C - 'A' + 10 : -1; if (N < 0) return false; - Out = (Out << 4) | static_cast(N); + Out = (Out << 4) | static_cast(N); } return true; } @@ -89,11 +99,11 @@ namespace VFJson case 'r': Out.push_back('\r'); break; case 't': Out.push_back('\t'); break; case 'u': { - uint32 Code = 0; if (!Hex4(Code)) { Error = "invalid JSON unicode escape"; return false; } + std::uint32_t Code = 0; if (!Hex4(Code)) { Error = "invalid JSON unicode escape"; return false; } if (Code >= 0xd800 && Code <= 0xdbff) { if (Position + 2 > Text.size() || Text[Position++] != '\\' || Text[Position++] != 'u') { Error = "missing JSON low surrogate"; return false; } - uint32 Low = 0; if (!Hex4(Low) || Low < 0xdc00 || Low > 0xdfff) { Error = "invalid JSON low surrogate"; return false; } + std::uint32_t Low = 0; if (!Hex4(Low) || Low < 0xdc00 || Low > 0xdfff) { Error = "invalid JSON low surrogate"; return false; } Code = 0x10000 + ((Code - 0xd800) << 10) + (Low - 0xdc00); } AppendUtf8(Out, Code); break; diff --git a/Standalone/Core/API.md b/Standalone/Core/API.md index 0e497a6..330e59f 100644 --- a/Standalone/Core/API.md +++ b/Standalone/Core/API.md @@ -4,6 +4,11 @@ Public header: `Core/include/VoxelForgeCore/VoxelForgeCore.h` (C++20, standard l Build the static library and viewer with `Build.ps1 -Target All`, or only the library with `Build.ps1 -Target Core`. +The public header uses only standard C++ types and can be included after Unreal's `CoreMinimal.h`. +`Core/verification/UnrealHeaderCoexistence.cpp` is the compile smoke check against the installed +Unreal 5.7 headers. Copied generator and compatibility declarations stay in the private +`vfcore::detail` implementation and do not appear in this API. + ## Units and ownership - `Vec3d` query positions and `Vec3i` voxel coordinates are in **voxels**. diff --git a/Standalone/Core/CheckCoreSymbols.ps1 b/Standalone/Core/CheckCoreSymbols.ps1 new file mode 100644 index 0000000..665ef58 --- /dev/null +++ b/Standalone/Core/CheckCoreSymbols.ps1 @@ -0,0 +1,82 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)][string]$LibraryPath, + [string]$ResultPath +) + +$ErrorActionPreference = 'Stop' +$script:Root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$LibraryPath = [IO.Path]::GetFullPath($LibraryPath) +if (-not (Test-Path -LiteralPath $LibraryPath -PathType Leaf)) { + throw "Core library not found: $LibraryPath" +} + +$VsBinRoots = @(Get-ChildItem -LiteralPath 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC' -Directory -ErrorAction Stop | + ForEach-Object { Join-Path $_.FullName 'bin\Hostx64\x64' } | + Where-Object { Test-Path -LiteralPath (Join-Path $_ 'dumpbin.exe') }) +if ($VsBinRoots.Count -eq 0) { throw 'dumpbin.exe was not found in the Visual Studio toolchain.' } +$Dumpbin = Join-Path $VsBinRoots[-1] 'dumpbin.exe' + +# Build the forbidden set from the compatibility and copied Unreal-facing declarations so the +# check covers the complete private type surface instead of a short hand-maintained example list. +$ScanRoots = @( + (Join-Path $script:Root 'Compat'), + (Join-Path $script:Root 'Source'), + (Join-Path $script:Root 'Core\src') +) +$TypeNames = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) +foreach ($ScanRoot in $ScanRoots) { + foreach ($File in Get-ChildItem -LiteralPath $ScanRoot -Recurse -File -Include *.h,*.hpp,*.cpp) { + foreach ($Match in [regex]::Matches([IO.File]::ReadAllText($File.FullName), + '(?m)^\s*(?:class|struct|enum(?:\s+class)?|using)\s+([FTUE][A-Z][A-Za-z0-9_]*)\b')) { + [void]$TypeNames.Add($Match.Groups[1].Value) + } + } +} +if ($TypeNames.Count -eq 0) { throw 'Could not derive Unreal-style type names from the copied implementation.' } + +$Dump = @(& $Dumpbin /SYMBOLS $LibraryPath 2>&1) +if ($LASTEXITCODE -eq -1073741502) { throw 'dumpbin.exe exited with 0xC0000142; stopping as required.' } +if ($LASTEXITCODE -ne 0) { throw "dumpbin.exe failed with exit code $LASTEXITCODE" } +if ($Dump.Count -lt 1000) { + throw "dumpbin returned only $($Dump.Count) lines; Core object symbols are not visible enough to verify." +} + +$Violations = [Collections.Generic.List[string]]::new() +$Alternation = ($TypeNames | Sort-Object { $_.Length } -Descending | ForEach-Object { [regex]::Escape($_) }) -join '|' +$GlobalTypePattern = [regex]::new('(? +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// VFJson is a small, stateless standalone input parser with no Unreal-shaped names. Keeping its +// value tree shared lets the viewer resolve its reference world and Core snapshot from one parse. +#include "Json.h" diff --git a/Standalone/Core/src/StandaloneSnapshotAdapter.h b/Standalone/Core/src/StandaloneSnapshotAdapter.h index bb879d5..f631508 100644 --- a/Standalone/Core/src/StandaloneSnapshotAdapter.h +++ b/Standalone/Core/src/StandaloneSnapshotAdapter.h @@ -1,12 +1,15 @@ #pragma once -#include "OwnerWorldLoader.h" +#include "Json.h" #include "VoxelForgeCore/VoxelForgeCore.h" -#include +#include namespace VoxelForgeCore { - // Private standalone adapter: keeps the compatibility holders behind the public value API. - ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr owner); + // Standalone-only bridge. Core parses into its namespaced compatibility snapshot and applies + // the same command-line overrides as the viewer's reference loader. + ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot, + std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride, + bool roundTunnels,bool enhancedTunnelWalls,std::string& error); } diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index c0b82a3..9d3255b 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1,20 +1,18 @@ +#include "CoreNamespacePreamble.h" #include "StandaloneSnapshotAdapter.h" +#include "VoxelForgeCore/VoxelForgeCore.h" +namespace vfcore::detail +{ #include "FarShellsInternal.h" #include "VoxelClipmapDesiredTiles.h" #include "VoxelMarchingCubesMesher.h" -#include "VoxelForgeCore/VoxelForgeCore.h" - -#include -#include -#include -#include -#include -#include -#include +} namespace VoxelForgeCore { + using namespace ::vfcore::detail; + struct ResolvedWorldSnapshot::Data { std::shared_ptr owner; @@ -185,8 +183,36 @@ namespace VoxelForgeCore } }; - ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr owner) - { return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner)); } + ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot, + std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride, + bool roundTunnels,bool enhancedTunnelWalls,std::string& error) + { + auto owner=std::make_shared(); + if(!StandaloneOwnerLoader::Resolve(jsonRoot,*owner,error))return {}; + + const bool seedChanged=owner->Seed!=seed; + owner->Settings->Seed=seed; + owner->Seed=seed; + owner->ClipRadius=clipRadius; + owner->Settings->ClipRadius=clipRadius; + if(seedChanged) + { + owner->Manager=std::make_unique(); + if(!owner->Manager->Initialize(owner->Settings.get(),seed)) + {error="standalone copied strate manager failed while applying viewer settings";return {};} + owner->Generator=std::make_unique(); + owner->Generator->InitializeSettings(owner->Settings.get()); + owner->Generator->SetStrateManager(owner->Manager.get()); + } + owner->Generator->bStandaloneResearchRoundTunnels=roundTunnels; + owner->Generator->bStandaloneResearchEnhancedWalls=enhancedTunnelWalls; + if(maxLodOverride>=0) + { + owner->MaxClipLevel=maxLodOverride; + owner->Settings->MaxClipLevel=maxLodOverride; + } + return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner)); + } std::optional ResolvedWorldSnapshot::FromJson( std::string_view json,std::string& error) diff --git a/Standalone/SHARED_CPP_LIBRARY_PLAN.md b/Standalone/SHARED_CPP_LIBRARY_PLAN.md index c668669..20f906e 100644 --- a/Standalone/SHARED_CPP_LIBRARY_PLAN.md +++ b/Standalone/SHARED_CPP_LIBRARY_PLAN.md @@ -23,9 +23,44 @@ changed. tile to `World::generateTile`. Shape-world `--stream-benchmark`, `--stream-pipeline`, edit, audit, and `--benchmark-level` runs use the Core world. Explicit evaluator and owner-comparison modes retain the compatibility generator only as the comparison side of those diagnostics. -- The build manifest compiles the copied CPU generator and mesher with the private compatibility - headers under `Compat`. Those headers and their Unreal-shaped stand-ins are implementation - details. They are absent from the public include tree and public API. +- The build compiles the copied CPU generator and mesher with the private compatibility headers + under `Compat`. Those headers and their Unreal-shaped stand-ins are implementation details. They + are absent from the public include tree and public API. + +## Implementation namespace and Unreal link safety + +The public `VoxelForgeCore.h` surface contains only standard C++ declarations. The copied +generator, mesher, and compatibility implementation compile under `vfcore::detail`, so engine +symbols such as `FVector`, `TArray`, `FMath`, and `UObject`-shaped stand-ins do not acquire the same +linker names as Unreal's definitions. `Core/src/VoxelForgeCore.cpp` owns the public API boundary; +it opens `vfcore::detail` only around its private implementation includes, then defines the +`VoxelForgeCore` API outside that namespace. + +For each non-boundary source in `Core/core_sources.txt`, `Build.ps1` writes a one-source shim under +`Build/VoxelForgeCore/wrappers`. The shim includes `CoreNamespacePreamble.h` before opening +`vfcore::detail`, then includes the canonical source path from the manifest. The preamble loads +the standard/platform headers and stateless JSON parser before namespace entry, preventing system +declarations from being accidentally nested in `vfcore::detail`. The `VoxelForgeCore.cpp` row is +compiled directly because it sets up its own public/private namespace boundary. Keep one +implementation source per shim. + +`Compat` still defines Unreal-shaped types and a few Unreal-named macros for the copied code, but +those declarations live in private implementation translation units only. The UBT module must +disable unity builds and PCHs for these shims, and must not force-include Unreal headers into +them. This keeps compatibility macros local to one translation unit. Normal plugin adapter +translation units may include `CoreMinimal.h` and the public `VoxelForgeCore.h` together; the +public header does not include any compatibility header or Unreal declaration. + +`Build.ps1` runs `Core/CheckCoreSymbols.ps1` on the completed static library. The check invokes +`dumpbin /SYMBOLS`, derives copied Unreal-style type names from `Compat`, `Source`, and `Core/src`, +and fails on an unqualified global occurrence. It also fails closed if `dumpbin` returns too little +symbol data to inspect. Rebuilding the `557f6aa` manifest directly and running the same checker +found 16,255 unqualified Unreal-style type symbols; the namespaced build checks 177,569 +symbol-table lines against 370 type names with no violations. The baseline and current logs are +retained in `Core/verification/link_safety_557f6aa_final.log` and +`Core/verification/link_safety_final.log`. `Core/verification/UnrealHeaderCoexistence.cpp` was +also compiled against the installed Unreal 5.7 `CoreMinimal.h` and the public header; see +`Core/verification/unreal_header_smoke_final.log`. ## API and ownership @@ -53,22 +88,46 @@ cancellation/version policy. Every client must apply the same sequence and brush ## Unreal integration: UBT module -Add a `VoxelForgeCore` runtime module to the plugin. Its `Build.cs` sets -`CppStandardVersion.Cpp20`, exports only `Standalone/Core/include`, and keeps these private include -roots available to the core implementation: +Add a `VoxelForgeCore` runtime module to the plugin and make the existing `VoxelForge` module depend +on it. Its `Build.cs` sets C++20, exports only `Standalone/Core/include`, and keeps the Standalone +root plus these private include roots available to the core implementation: +- `Standalone` - `Standalone/Core/src` - `Standalone/Compat` - `Standalone/Source` - `Standalone/Source/VoxelForge/Public` - `Standalone/Source/VoxelForge/Private` +Set `PCHUsage = PCHUsageMode.NoPCHs` and `bUseUnity = false` for the Core module. For example: + +```csharp +string PluginRoot = Path.GetFullPath(Path.Combine(ModuleDirectory, "../..")); +string Standalone = Path.Combine(PluginRoot, "Standalone"); +PublicIncludePaths.Add(Path.Combine(Standalone, "Core", "include")); +PrivateIncludePaths.AddRange(new[] { + Standalone, + Path.Combine(Standalone, "Core", "src"), + Path.Combine(Standalone, "Compat"), + Path.Combine(Standalone, "Source"), + Path.Combine(Standalone, "Source", "VoxelForge", "Public"), + Path.Combine(Standalone, "Source", "VoxelForge", "Private") +}); +CppStandard = CppStandardVersion.Cpp20; +PCHUsage = PCHUsageMode.NoPCHs; +bUseUnity = false; +``` + UBT discovers translation units under a module's `Source` tree. Add one thin `.cpp` include shim per row of `Core/core_sources.txt`, with each shim including exactly one canonical source file by a -relative path into `Standalone`. Keeping one included source per shim preserves the translation -unit boundaries used by `Build.ps1`. A manifest check in the UBT module should fail when a manifest -row lacks a shim or a shim names a different file. Do not copy implementation files into the -module. The standalone and UBT builds then compile the same source bytes. +path rooted at the private `Standalone` include path. For every implementation row except +`Core/src/VoxelForgeCore.cpp`, the shim includes `CoreNamespacePreamble.h`, opens +`namespace vfcore::detail`, then includes that one source. The special API row gets a shim that +includes `Core/src/VoxelForgeCore.cpp` directly; that source establishes its own namespace +boundary. Keeping one included source per shim preserves the translation-unit boundaries used by +`Build.ps1`. A manifest check in the UBT module should fail when a row lacks a shim, a shim names a +different file, or an extra shim exists. Do not copy implementation files into the module. The +standalone and UBT builds then compile the same canonical source bytes. The existing `VoxelForge` module owns the adapter and calls the new module. Its adapter runs on the game thread, resolves soft references and asset defaults, preserves array order, and serializes @@ -113,22 +172,40 @@ creation, or rendering into the library. ## Acceptance record -The retained Round 11 executable is the reference at checkout `631dd57`. The final Core build -matched its 11 hash rows (full detail, whole view and LOD0–8) at both W1 and W8. It retained the -5,642/5,642 route audit, passed the shape/look and far-survival audits, and the damaged-shell control -still reports 512 removed triangles, 16 missing boundary surfaces, 16 holes, and exit 14. Six -fixed 1280×800 captures compare byte-for-byte with the Round 11 references. The final single-tile +The `557f6aa` commit is the strict output baseline. The rebuilt namespaced Core matches all 11 +full-detail, whole-view and LOD0–8 hash rows at W1 and W8, with W1/W8 parity. The fresh records are +`Core/verification/last_standard_hash_w1.stdout.log`, +`Core/verification/last_standard_hash_w8.stdout.log`, and +`Core/verification/final_rebuilt_hash_parity.log`. + +The look and shape audits pass, and walkability remains 5,642/5,642. The dedicated passage-camera +detail audit passes at 13,271 passage samples with a detail ratio of 1.152. Far survival passes +with 7,200 surfaces, zero missing surfaces or holes, and 10,976 far connectors. The damaged-shell +control removes 512 triangles, reports 16 missing boundary surfaces and 16 holes, and exits 14. +The final rebuilt logs are `Core/verification/final_rebuilt_shape_audit.stdout.log`, +`Core/verification/final_rebuilt_passage_camera_audit.stdout.log`, +`Core/verification/final_rebuilt_far_survival.stdout.log`, and +`Core/verification/final_rebuilt_bad_shell_control.stdout.log`. + +The six fixed 1280×800 views were recaptured from the final rebuilt viewer and compare byte-for-byte +with the prior Core captures. Their images are `Core/verification/core_final_rebuild_*.png`; the SHA-256 +results are in `Core/verification/final_rebuilt_capture_parity.log`. The single-tile `--shape-world --benchmark-level=0` path reports `generator=VoxelForgeCore`. -The final no-hash, cold W8 direct full-payload benchmark was interleaved -reference/core/core/reference with `--stream-benchmark --shape-world --force-generate --workers=8 ---clip-radius=3`. Process wall means were 1.041 s for the reference (1.035, 1.047) and 1.0245 s -for Core (1.037, 1.012), 16.5 ms faster. Each run generated 2,226 tiles with the full 110.90 MiB -payload and zero cache hits. +The link-safety check demonstrates the failure before the change and the pass after it. Running it +on the `557f6aa` library finds 16,255 unqualified Unreal-style symbols; the rebuilt Core library +passes after checking 177,569 `dumpbin /SYMBOLS` lines against 370 copied Unreal-style type names. +`Core/verification/link_safety_557f6aa_final.log` and +`Core/verification/link_safety_final.log` retain the outputs. A smoke translation unit that +includes the installed Unreal 5.7 `CoreMinimal.h` and the public `VoxelForgeCore.h` also compiles; +see `Core/verification/unreal_header_smoke_final.log`. -The separate neighborhood-first `--stream-pipeline` benchmark was repeated in eight interleaved -processes per build. Whole-view-ready means were 0.887 s for the reference (0.878–0.899 s) and -0.894 s for Core (0.880–0.932 s). The 7 ms mean difference is smaller than the 54 ms combined -run-to-run range; Core reached the 27-tile neighborhood and 960-tile full-detail milestones at -comparable times. Direct benchmark, pipeline, audit, and capture logs are retained in -`Core/verification`. +**Speed gate: not met.** The no-hash, cold W8 `--stream-pipeline --shape-world --force-generate` +check was run in four interleaved baseline/Core pairs, each generating 2,226 tiles and the full +110.90 MiB mesh payload with no cache hits. `whole_view_ready` averaged 0.89175 s for the exact +`557f6aa` executable and 0.92050 s for the namespaced build, a 28.8 ms (3.2%) regression. Stream +wall time was effectively unchanged at 0.46000 s and 0.46025 s; the difference was in shape +compilation, including `intervals+signs` averaging 95.7 ms on baseline and 114.9 ms on Core. The +raw cold runs are in `Core/verification/final_rebuilt_pipeline_*.stdout.log`. API-only `/Ob3` and +`/Ob1` trials also retained hash parity but were slower, so the build is restored to `/O2 /MP8`. +The required “no slower” acceptance condition remains outstanding. diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index c8f7abd..7996a6a 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -351,6 +352,7 @@ namespace std::chrono::steady_clock::time_point FullShapeBuildStart{}; FStandaloneMeshCache MeshCache; std::string DataPath; + VFJson::Value OwnerJsonRoot; bool bHasCameraOverride=false; V3 CameraPositionOverride{}; float CameraYawOverride=0.0f,CameraPitchOverride=0.0f; @@ -656,7 +658,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; ShapeWorld.reset(); Snapshot={}; World=std::make_shared(); - if(!StandaloneOwnerLoader::Load(DataPath,*World,Error))return false; + if(!StandaloneOwnerLoader::Resolve(OwnerJsonRoot,*World,Error))return false; if(!bParityChecked&&Seed==World->Seed&&!bRoundTunnels)PrintParity(); const int32 ExportClipRadius=World->ClipRadius; World->ClipRadius=ClipRadius; @@ -722,8 +724,10 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; Min.X-=NearBuildPad;Min.Y-=NearBuildPad;Min.Z-=NearBuildPad; Max.X+=NearBuildPad;Max.Y+=NearBuildPad;Max.Z+=NearBuildPad; } - Snapshot=VoxelForgeCore::MakeStandaloneSnapshot(World); - if(!Snapshot){Error="the standalone adapter could not make a resolved core snapshot";return false;} + Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(OwnerJsonRoot,Seed,ClipRadius, + MaxClipLevelOverride,bRoundTunnels,bRoundTunnelEnhanced,Error); + if(!Snapshot) + {if(Error.empty())Error="the standalone adapter could not make a resolved core snapshot";return false;} ShapeWorld=std::make_unique(Snapshot); const int32 ShapeWorkers=bNeighborhoodOnly?(WorkerCount>=8?2:1):WorkerCount; VoxelForgeCore::ShapeBuildRequest Request; @@ -1941,6 +1945,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } int RunPipelinedStreamingBenchmark(std::shared_ptr OwnerWorld, + const VoxelForgeCore::ResolvedWorldSnapshot& CoreSnapshot, const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel, VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler, bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors, @@ -1948,7 +1953,15 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; { using Clock=std::chrono::steady_clock; FStandaloneOwnerWorld& World=*OwnerWorld; + const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels; + const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls; + VoxelForgeCore::World Shape(CoreSnapshot); FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels); + const auto CoreTiles=Shape.desiredTiles( + {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z}); + Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size()); + for(const auto& Key:CoreTiles) + Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); if(MaxLevel>=0) Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(), [&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end()); @@ -1978,7 +1991,15 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; if(Neighborhood.size()!=27) {std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;} - VoxelForgeCore::World Shape(VoxelForgeCore::MakeStandaloneSnapshot(OwnerWorld)); + // The initial desired set now owns all clipmap metadata needed by this Core-only run. + // Release the separate compatibility-world generation graph before compiling Core. + World.Generator.reset(); + World.Manager.reset(); + World.DefinitionStorage.clear(); + World.BiomeStorage.clear(); + World.OperationStorage.clear(); + World.DensitySamples.clear(); + std::string BuildError; const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels; const auto ShapeBuildStart=Clock::now(); @@ -1989,8 +2010,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z}; ShapeRequest.workerLimit=static_cast(std::clamp(WorkerCount,1,8)); ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld; - ShapeRequest.roundTunnels=World.Generator->bStandaloneResearchRoundTunnels; - ShapeRequest.enhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls; + ShapeRequest.roundTunnels=bRoundTunnels; + ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls; if(!Shape.compile(ShapeRequest,Scheduler,BuildError)) {std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;} const auto ShapeReady=Clock::now(); @@ -1998,7 +2019,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape); const auto StreamStart=Clock::now(); - const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels); + const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired); if(Update.DesiredTiles!=static_cast(Desired.Tiles.size())) { Streamer.Stop(); @@ -2153,11 +2174,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; auto PlayerNeighborhoodCompleted=std::chrono::steady_clock::time_point{}; std::vector CacheTiles; if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size()); + if(CoreWorld&&!FarAudit) + { + // Tile keys and band limits are already captured in Desired. Core owns field and + // mesh generation, so discard the unused compatibility generator before streaming. + World.Generator.reset(); + World.Manager.reset(); + World.DefinitionStorage.clear(); + World.BiomeStorage.clear(); + World.OperationStorage.clear(); + World.DensitySamples.clear(); + } const auto Start=std::chrono::steady_clock::now(); FStandaloneStreamingGenerator Streamer(World,WorkerCount, bHashDensity||!BakeCachePath.empty(),Cache,MaxLevel, bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,CoreWorld); - const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels); + const FStandaloneStreamingUpdate Update=CoreWorld + ?Streamer.StartWithDesired(Desired):Streamer.Start(CameraPositionVoxels); const double LOD0Width=static_cast(2*World.ClipRadius+2) *CHUNK_SIZE*VOXEL_SIZE/100.0; std::printf("[StreamBenchmarkConfig] clip_radius=%d lod0_cube=%.0fm voxel=25cm fullres_levels=%d max_level_filter=%d workers=%d full_detail_tiles=%d desired_tiles=%d variant=%s uv=%s colors=%s material_groups=%s hash=%s\n", @@ -2564,7 +2597,17 @@ int main(int Argc,char** Argv) App.bProfileDetailBand=bProfileDetailBand; std::string Error; App.World=std::make_shared(); - if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;} + { + std::ifstream JsonFile(App.DataPath,std::ios::binary); + if(!JsonFile){std::fprintf(stderr,"Owner data load failed: cannot open %s\n",App.DataPath.c_str());return 2;} + std::string OwnerJsonText{std::istreambuf_iterator(JsonFile),std::istreambuf_iterator{}}; + if(!JsonFile&& !JsonFile.eof()) + {std::fprintf(stderr,"Owner data load failed: read error for %s\n",App.DataPath.c_str());return 2;} + if(!VFJson::Parser(std::move(OwnerJsonText)).Parse(App.OwnerJsonRoot,Error)) + {std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;} + } + if(!StandaloneOwnerLoader::Resolve(App.OwnerJsonRoot,*App.World,Error)) + {std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;} App.Seed=App.World->Seed; if(App.bParityOnly) { @@ -2597,6 +2640,14 @@ int main(int Argc,char** Argv) &&!bStreamPipeline &&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld)) {std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;} + if(bStreamPipeline) + { + App.Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(App.OwnerJsonRoot, + App.Seed,App.ClipRadius,App.MaxClipLevelOverride,App.bRoundTunnels, + App.bRoundTunnelEnhanced,Error); + if(!App.Snapshot) + {std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;} + } if(App.bShapeAuditOnly) { if(bPassageDetailAuditOnly) @@ -2614,7 +2665,7 @@ int main(int Argc,char** Argv) if(bStreamBenchmark) { if(bStreamPipeline) - return RunPipelinedStreamingBenchmark(App.World, + return RunPipelinedStreamingBenchmark(App.World,App.Snapshot, FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0), App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler, bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups, diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 14ae290..6e8b363 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -49,6 +49,7 @@ struct FStandaloneCpuTile bool bHasTopologyHash=false; uint64 MeshPayloadAllocatedBytes=0; std::array ProfileCycles{}; + }; inline uint64 HashStandaloneMeshPayload(const FVoxelMeshData& Mesh) @@ -428,6 +429,19 @@ public: return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex); } + FStandaloneStreamingUpdate StartWithDesired(const FStandaloneDesiredTileSet& InitialDesired, + const std::vector& ResidentKeys={}, + uint64 CrossingIndex=0) + { + Stop(); + { + std::lock_guard Lock(Mutex); + bStop=false; + } + for(int32 I=0;I& Keys) { TileFilter.clear();TileFilter.reserve(Keys.size()*2+1); @@ -461,6 +475,33 @@ public: for(const auto& Key:CoreTiles) NewDesired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); } + return UpdateDesiredSet(std::move(NewDesired),ResidentKeys,CrossingIndex); + } + + std::vector TakeReady() + { + std::vector Out; + std::lock_guard Lock(Mutex); + Out.swap(Ready); + for(const FStandaloneCpuTile& Tile:Out)Outstanding.erase(Tile.Key); + return Out; + } + + bool IsDesired(const FVoxelTileKey& Key)const + {std::lock_guard Lock(Mutex);return Desired.contains(Key);} + int32 DesiredCount()const + {std::lock_guard Lock(Mutex);return static_cast(Desired.size());} + int32 FullResDesiredCount()const + {std::lock_guard Lock(Mutex);return FullResTileCount;} + FIntVector CenterChunk()const + {std::lock_guard Lock(Mutex);return CurrentDesired.CenterChunk;} + int32 GetWorkerCount()const{return WorkerCount;} + uint64 GetCacheHits()const{return CacheHitCount.load(std::memory_order_relaxed);} + +private: + FStandaloneStreamingUpdate UpdateDesiredSet(FStandaloneDesiredTileSet NewDesired, + const std::vector& ResidentKeys,uint64 CrossingIndex) + { if(MaxLevelFilter>=0) NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(), [&](const FVoxelTileKey& Key){return Key.Level>MaxLevelFilter;}),NewDesired.Tiles.end()); @@ -523,28 +564,6 @@ public: Wake.notify_all(); return Stats; } - - std::vector TakeReady() - { - std::vector Out; - std::lock_guard Lock(Mutex); - Out.swap(Ready); - for(const FStandaloneCpuTile& Tile:Out)Outstanding.erase(Tile.Key); - return Out; - } - - bool IsDesired(const FVoxelTileKey& Key)const - {std::lock_guard Lock(Mutex);return Desired.contains(Key);} - int32 DesiredCount()const - {std::lock_guard Lock(Mutex);return static_cast(Desired.size());} - int32 FullResDesiredCount()const - {std::lock_guard Lock(Mutex);return FullResTileCount;} - FIntVector CenterChunk()const - {std::lock_guard Lock(Mutex);return CurrentDesired.CenterChunk;} - int32 GetWorkerCount()const{return WorkerCount;} - uint64 GetCacheHits()const{return CacheHitCount.load(std::memory_order_relaxed);} - -private: FStandaloneCpuTile Generate(const FStandaloneTileJob& Job) { const auto StartTime=FClock::now(); diff --git a/Standalone/Source/VoxelForge/Public/VoxelTypes.h b/Standalone/Source/VoxelForge/Public/VoxelTypes.h index 113b0c2..91e0c82 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelTypes.h +++ b/Standalone/Source/VoxelForge/Public/VoxelTypes.h @@ -351,7 +351,7 @@ struct FVoxelTileKey FORCEINLINE uint32 GetTypeHash(const FVoxelTileKey& K) { - return HashCombine(GetTypeHash(K.Coord), ::GetTypeHash(K.Level)); + return HashCombine(GetTypeHash(K.Coord), GetTypeHash(K.Level)); } //=============================================================================