standalone: VoxelForgeCore link-safe next to Unreal -- internals in vfcore::detail, symbol check

- The copied generator, mesher and Compat stand-ins compile inside vfcore::detail.
- CheckCoreSymbols.ps1 (run by Build.ps1) fails on any unqualified Unreal-style symbol in the
  library: 16,255 on 557f6aa, 0 now. A UE 5.7 CoreMinimal.h + VoxelForgeCore.h smoke compile passes.
- Output identical: 11 hash rows at W1/W8, routes 5642/5642, look/passage/far audits pass,
  bad-shell control fails, six captures byte-identical.
Cost, measured by me interleaved (8 workers, cold, 4 runs each, first run excluded): 0.963 s vs
0.937 s for 557f6aa, ~27 ms in shape compilation; same math, so codegen/inlining. To be found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-05 15:20:29 +02:00
1 parent 557f6aac30
commit aeeedaa73d
11 files changed
+461 -87

No files matched your search

+52 -5
View File
@@ -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
+18 -8
View File
@@ -1,8 +1,18 @@
#pragma once
#include <charconv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <iterator>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
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<double>(Data) : Fallback; }
int64 Integer(int64 Fallback = 0) const { return IsNumber() ? static_cast<int64>(std::get<double>(Data)) : Fallback; }
std::int64_t Integer(std::int64_t Fallback = 0) const { return IsNumber() ? static_cast<std::int64_t>(std::get<double>(Data)) : Fallback; }
bool Boolean(bool Fallback = false) const { return std::holds_alternative<bool>(Data) ? std::get<bool>(Data) : Fallback; }
const std::string& String() const { static const std::string Empty; return IsString() ? std::get<std::string>(Data) : Empty; }
};
@@ -49,23 +59,23 @@ namespace VFJson
size_t Position = 0;
void SkipSpace() { while (Position < Text.size() && static_cast<unsigned char>(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<char>(Code));
else if (Code <= 0x7ff) { Out.push_back(static_cast<char>(0xc0 | (Code >> 6))); Out.push_back(static_cast<char>(0x80 | (Code & 0x3f))); }
else if (Code <= 0xffff) { Out.push_back(static_cast<char>(0xe0 | (Code >> 12))); Out.push_back(static_cast<char>(0x80 | ((Code >> 6) & 0x3f))); Out.push_back(static_cast<char>(0x80 | (Code & 0x3f))); }
else { Out.push_back(static_cast<char>(0xf0 | (Code >> 18))); Out.push_back(static_cast<char>(0x80 | ((Code >> 12) & 0x3f))); Out.push_back(static_cast<char>(0x80 | ((Code >> 6) & 0x3f))); Out.push_back(static_cast<char>(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<uint32>(N);
Out = (Out << 4) | static_cast<std::uint32_t>(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;
+5
View File
@@ -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**.
+82
View File
@@ -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('(?<![A-Za-z0-9_:])(?:' + $Alternation + ')(?![A-Za-z0-9_])',
[Text.RegularExpressions.RegexOptions]::Compiled)
$PrivateTypePattern = [regex]::new('vfcore::detail::(?:class\s+|struct\s+|enum\s+)?(?:' + $Alternation + ')(?![A-Za-z0-9_])',
[Text.RegularExpressions.RegexOptions]::Compiled)
$PrivateDestructorPattern = [regex]::new('~(?:' + $Alternation + ')(?![A-Za-z0-9_])',
[Text.RegularExpressions.RegexOptions]::Compiled)
foreach ($Line in $Dump) {
if ($Line -notmatch '\|') { continue }
$Symbol = ($Line -split '\|',2)[1]
$DemangleOpen = $Symbol.IndexOf('(')
if ($DemangleOpen -lt 0) { continue }
$Demangled = $Symbol.Substring($DemangleOpen+1)
# dumpbin repeats decorated-only linker helpers in parentheses when it has no readable
# demangled form. Ignore those and remove known vfcore-qualified type spellings, including the
# repeated destructor spelling after "::~", before looking for global type names.
if ($Demangled -match '^\s*[?$]') { continue }
$Demangled = $PrivateTypePattern.Replace($Demangled,'')
$Demangled = $PrivateDestructorPattern.Replace($Demangled,'')
# A type preceded by a scope separator belongs to a namespace. An unqualified instance in
# the demangled dump is a global Unreal-style type and can ODR-collide with the engine.
if ($GlobalTypePattern.IsMatch($Demangled)) {
$Violations.Add($Line.Trim())
}
}
if ($Violations.Count -gt 0) {
$ResultLines = @("CORE_LINK_SAFETY_FAIL global Unreal-style symbols: $($Violations.Count)") +
@($Violations | Select-Object -First 40)
if ($ResultPath) { [IO.File]::WriteAllLines([IO.Path]::GetFullPath($ResultPath),$ResultLines) }
$ResultLines | ForEach-Object { Write-Output $_ }
throw "Core archive contains $($Violations.Count) unqualified Unreal-style type symbols."
}
$ResultLines = @("CORE_LINK_SAFETY_PASS checked $($Dump.Count) dumpbin lines against $($TypeNames.Count) copied Unreal-style type names.")
if ($ResultPath) { [IO.File]::WriteAllLines([IO.Path]::GetFullPath($ResultPath),$ResultLines) }
$ResultLines | ForEach-Object { Write-Output $_ }
@@ -0,0 +1,54 @@
#pragma once
// Every standard or platform header used by the copied implementation must be included before
// its wrapper opens vfcore::detail. This prevents system declarations from being nested in the
// private namespace when the implementation includes its Unreal-shaped compatibility headers.
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <cassert>
#include <cctype>
#include <charconv>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <immintrin.h>
#include <initializer_list>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <math.h>
#include <memory>
#include <memory_resource>
#include <mutex>
#include <new>
#include <numeric>
#include <optional>
#include <shared_mutex>
#include <span>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
// 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"
@@ -1,12 +1,15 @@
#pragma once
#include "OwnerWorldLoader.h"
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <memory>
#include <string>
namespace VoxelForgeCore
{
// Private standalone adapter: keeps the compatibility holders behind the public value API.
ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr<FStandaloneOwnerWorld> 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);
}
+37 -11
View File
@@ -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 <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <map>
#include <numeric>
#include <stdexcept>
}
namespace VoxelForgeCore
{
using namespace ::vfcore::detail;
struct ResolvedWorldSnapshot::Data
{
std::shared_ptr<FStandaloneOwnerWorld> owner;
@@ -185,8 +183,36 @@ namespace VoxelForgeCore
}
};
ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr<FStandaloneOwnerWorld> 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<FStandaloneOwnerWorld>();
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<UVoxelStrateManager>();
if(!owner->Manager->Initialize(owner->Settings.get(),seed))
{error="standalone copied strate manager failed while applying viewer settings";return {};}
owner->Generator=std::make_unique<UVoxelGenerator>();
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> ResolvedWorldSnapshot::FromJson(
std::string_view json,std::string& error)
+103 -26
View File
@@ -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.
+61 -10
View File
@@ -26,6 +26,7 @@
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <limits>
#include <map>
#include <numeric>
@@ -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<FStandaloneOwnerWorld>();
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<VoxelForgeCore::World>(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<FStandaloneOwnerWorld> 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<uint32>(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<int32>(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<FStandaloneCpuTile> 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<double>(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<FStandaloneOwnerWorld>();
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<char>(JsonFile),std::istreambuf_iterator<char>{}};
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,
+41 -22
View File
@@ -49,6 +49,7 @@ struct FStandaloneCpuTile
bool bHasTopologyHash=false;
uint64 MeshPayloadAllocatedBytes=0;
std::array<uint64,VoxelDensityProfile::BucketCount> 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<FVoxelTileKey>& ResidentKeys={},
uint64 CrossingIndex=0)
{
Stop();
{
std::lock_guard<std::mutex> Lock(Mutex);
bStop=false;
}
for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();});
return UpdateDesiredSet(InitialDesired,ResidentKeys,CrossingIndex);
}
void SetDesiredTileFilter(const std::vector<FVoxelTileKey>& 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<FStandaloneCpuTile> TakeReady()
{
std::vector<FStandaloneCpuTile> Out;
std::lock_guard<std::mutex> 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<std::mutex> Lock(Mutex);return Desired.contains(Key);}
int32 DesiredCount()const
{std::lock_guard<std::mutex> Lock(Mutex);return static_cast<int32>(Desired.size());}
int32 FullResDesiredCount()const
{std::lock_guard<std::mutex> Lock(Mutex);return FullResTileCount;}
FIntVector CenterChunk()const
{std::lock_guard<std::mutex> 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<FVoxelTileKey>& 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<FStandaloneCpuTile> TakeReady()
{
std::vector<FStandaloneCpuTile> Out;
std::lock_guard<std::mutex> 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<std::mutex> Lock(Mutex);return Desired.contains(Key);}
int32 DesiredCount()const
{std::lock_guard<std::mutex> Lock(Mutex);return static_cast<int32>(Desired.size());}
int32 FullResDesiredCount()const
{std::lock_guard<std::mutex> Lock(Mutex);return FullResTileCount;}
FIntVector CenterChunk()const
{std::lock_guard<std::mutex> 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();
@@ -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));
}
//=============================================================================