wip(unreal): VoxelForgeCore UBT module + asset adapter reproduce the standalone world
Luna, integration round 1 (BuildHost only; owner's project and assets untouched): - New runtime module VoxelForgeCore: one shim per Standalone/Core/core_sources.txt row (sources stay canonical under Standalone/), NoPCH/no unity, manifest check; VoxelForge links it (no runtime routing yet). UE link-safety audit: 18 shims, 83,843 symbol lines vs 399 Unreal-style names, 0 clashes. - Adapter (editor commandlet path) turns staged copies of the owner's DA_* assets into schema-v1 JSON; with clip_radius=3/max_clip_level=8 it reproduces all four Core hashes (0xdb2991c73215af4c, 0x8236c5edd7fc8e4f, 0x4c48fa75a543d58e, 0xbea22e76b34a4dfc) and walkability 5,642/5,642 at worker limits 1, 8 and 16. - Noted: the owner's settings asset says max clip level 6 (standalone used 8); Core uses 25 cm voxels vs the plugin runtime's 12.5 cm. Not done yet: AVoxelWorld switch and Core runtime route (round 2). Docs: CODEMAP, ARCHITECTURE, SHARED_CPP_LIBRARY_PLAN, UNREAL_INTEGRATION_REPORT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
a1dfc0422e
commit
29ca99af7f
32 files changed
+919
-69
No files matched your search
@@ -79,6 +79,9 @@ public class VoxelForge : ModuleRules
|
||||
// Private dependencies - only used in our .cpp files, not exposed in headers
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
// Core module is linked; runtime routing is not wired yet.
|
||||
// Le module Core est lié; la génération Runtime ne l'utilise pas encore.
|
||||
"VoxelForgeCore",
|
||||
"Json", // Diffable Tier 5 promoted-record and season-manifest stores
|
||||
"ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview)
|
||||
"RHI", // Texture3D create + RHIUpdateTexture3D for the density volume (mini-sun shadows)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Compat/StandaloneDataAssetGlue.cpp"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Compat/UnrealPerlin.cpp"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// The public API is standard-only; this UBT boundary supplies its module export token locally.
|
||||
#ifndef DLLEXPORT
|
||||
#if defined(_WIN32)
|
||||
#define DLLEXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define DLLEXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
#endif
|
||||
#include "Core/src/VoxelForgeCore.cpp"
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelBiomeDefinition.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelCaveMorphology.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelDensityAblation.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelDensityOpStack.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelDensityProfile.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelDiffLayer.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelGenerator.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelHeightOpStack.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelSettings.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelStrateComposer.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelStrateManager.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelStrateMeasure.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelTerrainOpDefinition.cpp"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
|
||||
#define VFCORE_PRIVATE_IMPLEMENTATION 1
|
||||
#include "CoreNamespacePreamble.h"
|
||||
namespace vfcore::detail
|
||||
{
|
||||
#include "Source/VoxelForge/Private/VoxelWormField.cpp"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
// The module entry must see UE's real CoreMinimal; the Core shims intentionally use Compat/CoreMinimal.
|
||||
#ifndef VFCORE_ENGINE_COREMINIMAL_HEADER
|
||||
#error VoxelForgeCore must provide the engine CoreMinimal header path for its module entry.
|
||||
#endif
|
||||
#include VFCORE_ENGINE_COREMINIMAL_HEADER
|
||||
@@ -0,0 +1,6 @@
|
||||
// Core module entry point. Le module garde ses wrappers Core isolés du PCH Unreal.
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "ModuleManager.h"
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, VoxelForgeCore)
|
||||
@@ -0,0 +1,132 @@
|
||||
// VoxelForgeCore compiles the canonical Standalone sources in isolated, one-source shims.
|
||||
// Le module compile les sources canoniques Standalone via des wrappers isolés.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class VoxelForgeCore : ModuleRules
|
||||
{
|
||||
public VoxelForgeCore(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
string PluginRoot = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", ".."));
|
||||
string StandaloneRoot = Path.Combine(PluginRoot, "Standalone");
|
||||
string ManifestPath = Path.Combine(StandaloneRoot, "Core", "core_sources.txt");
|
||||
string ShimRoot = Path.Combine(ModuleDirectory, "Private", "Manifest");
|
||||
string CoreMinimalHeader = Path.Combine(
|
||||
EngineDirectory, "Source", "Runtime", "Core", "Public", "CoreMinimal.h").Replace('\\', '/');
|
||||
|
||||
ValidateManifest(ManifestPath, ShimRoot, StandaloneRoot);
|
||||
|
||||
PublicIncludePaths.Add(Path.Combine(StandaloneRoot, "Core", "include"));
|
||||
PrivateIncludePaths.AddRange(new string[]
|
||||
{
|
||||
StandaloneRoot,
|
||||
Path.Combine(EngineDirectory, "Source", "Runtime", "Core", "Public", "Modules"),
|
||||
Path.Combine(StandaloneRoot, "Core", "src"),
|
||||
Path.Combine(StandaloneRoot, "Compat"),
|
||||
Path.Combine(StandaloneRoot, "Source"),
|
||||
Path.Combine(StandaloneRoot, "Source", "VoxelForge", "Public"),
|
||||
Path.Combine(StandaloneRoot, "Source", "VoxelForge", "Private")
|
||||
});
|
||||
PrivateDefinitions.Add("VFCORE_ENGINE_COREMINIMAL_HEADER=\"" + CoreMinimalHeader + "\"");
|
||||
PrivateDefinitions.Add("UE_BUILD_SHIPPING=" + (Target.Configuration == UnrealTargetConfiguration.Shipping ? "1" : "0"));
|
||||
// The wrapped VoxelForge sources use the standalone compatibility layer, which excludes editor-only fields.
|
||||
PrivateDefinitions.Add("WITH_EDITORONLY_DATA=0");
|
||||
|
||||
PublicDependencyModuleNames.Add("Core");
|
||||
CppStandard = CppStandardVersion.Cpp20;
|
||||
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
|
||||
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
|
||||
FPSemantics = FPSemanticsMode.Precise;
|
||||
PCHUsage = PCHUsageMode.NoPCHs;
|
||||
bUseUnity = false;
|
||||
bEnableExceptions = true;
|
||||
bUseRTTI = true;
|
||||
}
|
||||
|
||||
private static void ValidateManifest(string ManifestPath, string ShimRoot, string StandaloneRoot)
|
||||
{
|
||||
if (!File.Exists(ManifestPath))
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore source manifest is missing: {0}", ManifestPath);
|
||||
}
|
||||
|
||||
string Root = Path.GetFullPath(StandaloneRoot) + Path.DirectorySeparatorChar;
|
||||
string[] Sources = File.ReadAllLines(ManifestPath)
|
||||
.Select(Line => Line.Trim().Replace('\\', '/'))
|
||||
.Where(Line => Line.Length != 0 && !Line.StartsWith("#", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (Sources.Length == 0)
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore source manifest is empty: {0}", ManifestPath);
|
||||
}
|
||||
|
||||
if (Sources.Distinct(StringComparer.Ordinal).Count() != Sources.Length)
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore source manifest contains duplicate rows: {0}", ManifestPath);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(ShimRoot))
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore manifest shim directory is missing: {0}", ShimRoot);
|
||||
}
|
||||
|
||||
HashSet<string> ExpectedShimNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string Source in Sources)
|
||||
{
|
||||
if (Path.IsPathRooted(Source) || Source.Split('/').Any(Part => Part == ".."))
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore manifest path escapes Standalone: {0}", Source);
|
||||
}
|
||||
|
||||
string CanonicalPath = Path.GetFullPath(Path.Combine(StandaloneRoot, Source.Replace('/', Path.DirectorySeparatorChar)));
|
||||
if (!CanonicalPath.StartsWith(Root, StringComparison.OrdinalIgnoreCase) || !File.Exists(CanonicalPath))
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore canonical source is missing or outside Standalone: {0}", Source);
|
||||
}
|
||||
|
||||
string ShimName = "VFCore_" + Regex.Replace(Source, "[^A-Za-z0-9]+", "_") + ".cpp";
|
||||
ExpectedShimNames.Add(ShimName);
|
||||
string ShimPath = Path.Combine(ShimRoot, ShimName);
|
||||
if (!File.Exists(ShimPath))
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore manifest row has no shim: {0} -> {1}", Source, ShimPath);
|
||||
}
|
||||
|
||||
string Shim = File.ReadAllText(ShimPath);
|
||||
string EscapedSource = Regex.Escape(Source);
|
||||
if (Regex.Matches(Shim, "(?m)^\\s*#include\\s+\"" + EscapedSource + "\"\\s*$").Count != 1)
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore shim must include its canonical manifest source exactly once: {0}", ShimPath);
|
||||
}
|
||||
|
||||
bool IsApiBoundary = String.Equals(Source, "Core/src/VoxelForgeCore.cpp", StringComparison.Ordinal);
|
||||
if (IsApiBoundary)
|
||||
{
|
||||
if (Regex.Matches(Shim, "(?m)^\\s*#include\\s+\"").Count != 1)
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore API boundary shim must include only its canonical source: {0}", ShimPath);
|
||||
}
|
||||
}
|
||||
else if (!Regex.IsMatch(Shim, "(?s)#include\\s+\"CoreNamespacePreamble\\.h\".*namespace\\s+vfcore::detail\\s*\\{.*#include\\s+\"" + EscapedSource + "\""))
|
||||
{
|
||||
throw new BuildException("VoxelForgeCore implementation shim must preload headers and wrap its source in vfcore::detail: {0}", ShimPath);
|
||||
}
|
||||
}
|
||||
|
||||
string[] ActualShims = Directory.GetFiles(ShimRoot, "*.cpp", SearchOption.TopDirectoryOnly)
|
||||
.Select(Path.GetFileName)
|
||||
.ToArray();
|
||||
if (ActualShims.Length != ExpectedShimNames.Count
|
||||
|| ActualShims.Any(Name => !ExpectedShimNames.Contains(Name)))
|
||||
{
|
||||
throw new BuildException(
|
||||
"VoxelForgeCore shims and Core/core_sources.txt differ. Expected {0} shims under {1}; found {2}.",
|
||||
ExpectedShimNames.Count, ShimRoot, ActualShims.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,10 +48,18 @@
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelTypes.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -61,6 +69,9 @@ constexpr int32 DefaultRenderWidth = 512;
|
||||
constexpr int32 DefaultRenderHeight = 288;
|
||||
constexpr float DefaultRenderStepVoxels = 0.5f; // 6.25 cm, as at 25 cm/voxel.
|
||||
constexpr float DefaultRenderMaxDistanceVoxels = 320.0f; // 40 m.
|
||||
// Schema-v1 follows Standalone's 25 cm voxel scale, independent of the legacy runtime module's 12.5 cm scale.
|
||||
// Le schéma-v1 suit l'échelle Standalone de 25 cm, indépendante de l'échelle historique de 12,5 cm du module Runtime.
|
||||
constexpr double StandaloneCoreVoxelSizeCm = 25.0;
|
||||
constexpr int32 DefaultExportSize = 128;
|
||||
constexpr int32 DefaultExportStep = 1;
|
||||
constexpr int32 DefaultMaxWalkCells = 32000000;
|
||||
@@ -12268,20 +12279,28 @@ TSharedPtr<FJsonValue> StandaloneSerializeProperty(
|
||||
if (const FSetProperty* Set = CastField<FSetProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
|
||||
FScriptSetHelper Helper(Set, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
|
||||
{
|
||||
if (Helper.IsValidIndex(Index))
|
||||
{
|
||||
Values.Add(StandaloneSerializeProperty(
|
||||
FString StableKey;
|
||||
Set->ElementProp->ExportTextItem_Direct(
|
||||
StableKey, Helper.GetElementPtr(Index), nullptr, nullptr, PPF_None);
|
||||
OrderedValues.Emplace(StableKey, StandaloneSerializeProperty(
|
||||
Set->ElementProp, Helper.GetElementPtr(Index), ActiveObjects));
|
||||
}
|
||||
}
|
||||
OrderedValues.Sort([](const auto& A, const auto& B) { return A.Key < B.Key; });
|
||||
Values.Reserve(OrderedValues.Num());
|
||||
for (const auto& Entry : OrderedValues) Values.Add(Entry.Value);
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FMapProperty* Map = CastField<FMapProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
|
||||
FScriptMapHelper Helper(Map, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
|
||||
{
|
||||
@@ -12291,8 +12310,14 @@ TSharedPtr<FJsonValue> StandaloneSerializeProperty(
|
||||
Map->KeyProp, Helper.GetKeyPtr(Index), ActiveObjects));
|
||||
Pair->SetField(TEXT("value"), StandaloneSerializeProperty(
|
||||
Map->ValueProp, Helper.GetValuePtr(Index), ActiveObjects));
|
||||
Values.Add(MakeShared<FJsonValueObject>(Pair));
|
||||
FString StableKey;
|
||||
Map->KeyProp->ExportTextItem_Direct(
|
||||
StableKey, Helper.GetKeyPtr(Index), nullptr, nullptr, PPF_None);
|
||||
OrderedValues.Emplace(StableKey, MakeShared<FJsonValueObject>(Pair));
|
||||
}
|
||||
OrderedValues.Sort([](const auto& A, const auto& B) { return A.Key < B.Key; });
|
||||
Values.Reserve(OrderedValues.Num());
|
||||
for (const auto& Entry : OrderedValues) Values.Add(Entry.Value);
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FStructProperty* Struct = CastField<FStructProperty>(Property))
|
||||
@@ -12368,9 +12393,16 @@ void StandaloneWriteJsonValue(
|
||||
else if (Value->Type == EJson::Object)
|
||||
{
|
||||
Writer.WriteObjectStart();
|
||||
for (const TPair<FString, TSharedPtr<FJsonValue>>& Field : Value->AsObject()->Values)
|
||||
TArray<FString> Keys;
|
||||
Value->AsObject()->Values.GetKeys(Keys);
|
||||
Keys.Sort();
|
||||
for (const FString& Key : Keys)
|
||||
{
|
||||
StandaloneWriteJsonValue(Writer, *Field.Key, Field.Value);
|
||||
const TSharedPtr<FJsonValue>* Field = Value->AsObject()->Values.Find(Key);
|
||||
if (Field != nullptr)
|
||||
{
|
||||
StandaloneWriteJsonValue(Writer, *Key, *Field);
|
||||
}
|
||||
}
|
||||
Writer.WriteObjectEnd();
|
||||
}
|
||||
@@ -12465,6 +12497,273 @@ struct FStandaloneExportSample
|
||||
float Density = 0.0f;
|
||||
};
|
||||
|
||||
class FStandaloneCoreAdapterScheduler final : public VoxelForgeCore::JobScheduler
|
||||
{
|
||||
public:
|
||||
double parallel_for(std::size_t Count, std::size_t BatchSize, std::size_t WorkerLimit,
|
||||
void* Context, Task Callback, double* WorkerActiveSeconds,
|
||||
std::size_t WorkerActiveCapacity) const override
|
||||
{
|
||||
if (Count == 0) return 0.0;
|
||||
if (WorkerActiveSeconds != nullptr)
|
||||
{
|
||||
std::fill(WorkerActiveSeconds, WorkerActiveSeconds + WorkerActiveCapacity, 0.0);
|
||||
}
|
||||
const std::size_t WorkerCount = std::clamp<std::size_t>(WorkerLimit, 1,
|
||||
std::min<std::size_t>(16, std::max<std::size_t>(1, Count)));
|
||||
const std::size_t Grain = std::max<std::size_t>(1, BatchSize);
|
||||
std::atomic<std::size_t> Next{0};
|
||||
std::vector<double> WorkerSeconds(WorkerCount, 0.0);
|
||||
std::vector<std::thread> Threads;
|
||||
Threads.reserve(WorkerCount);
|
||||
for (std::size_t Worker = 0; Worker < WorkerCount; ++Worker)
|
||||
{
|
||||
Threads.emplace_back([&, Worker]
|
||||
{
|
||||
const auto Started = std::chrono::steady_clock::now();
|
||||
for (;;)
|
||||
{
|
||||
const std::size_t First = Next.fetch_add(Grain, std::memory_order_relaxed);
|
||||
if (First >= Count) break;
|
||||
const std::size_t End = First + std::min(Grain, Count - First);
|
||||
for (std::size_t Index = First; Index < End; ++Index)
|
||||
{
|
||||
Callback(Context, Index, Worker);
|
||||
}
|
||||
}
|
||||
WorkerSeconds[Worker] = std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - Started).count();
|
||||
});
|
||||
}
|
||||
for (std::thread& Thread : Threads) Thread.join();
|
||||
double TotalSeconds = 0.0;
|
||||
for (std::size_t Worker = 0; Worker < WorkerCount; ++Worker)
|
||||
{
|
||||
TotalSeconds += WorkerSeconds[Worker];
|
||||
if (WorkerActiveSeconds != nullptr && Worker < WorkerActiveCapacity)
|
||||
{
|
||||
WorkerActiveSeconds[Worker] = WorkerSeconds[Worker];
|
||||
}
|
||||
}
|
||||
return TotalSeconds;
|
||||
}
|
||||
};
|
||||
|
||||
struct FStandaloneCoreAdapterTileHashes
|
||||
{
|
||||
VoxelForgeCore::TileKey Key;
|
||||
std::array<std::uint64_t, 4> Hashes{};
|
||||
};
|
||||
|
||||
void StandaloneCoreHashWord(std::uint64_t& Hash, std::uint64_t Word, int32 ByteCount)
|
||||
{
|
||||
for (int32 Byte = 0; Byte < ByteCount; ++Byte)
|
||||
{
|
||||
Hash ^= static_cast<std::uint8_t>(Word >> (Byte * 8));
|
||||
Hash *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::uint64_t, 4> StandaloneCoreAggregateHashes(
|
||||
std::vector<FStandaloneCoreAdapterTileHashes>& Rows)
|
||||
{
|
||||
std::sort(Rows.begin(), Rows.end(), [](const auto& A, const auto& B)
|
||||
{
|
||||
if (A.Key.lod != B.Key.lod) return A.Key.lod < B.Key.lod;
|
||||
if (A.Key.coordinate.z != B.Key.coordinate.z)
|
||||
return A.Key.coordinate.z < B.Key.coordinate.z;
|
||||
if (A.Key.coordinate.y != B.Key.coordinate.y)
|
||||
return A.Key.coordinate.y < B.Key.coordinate.y;
|
||||
return A.Key.coordinate.x < B.Key.coordinate.x;
|
||||
});
|
||||
|
||||
std::array<std::uint64_t, 4> Aggregates{
|
||||
14695981039346656037ull, 14695981039346656037ull,
|
||||
14695981039346656037ull, 14695981039346656037ull};
|
||||
for (const FStandaloneCoreAdapterTileHashes& Row : Rows)
|
||||
{
|
||||
for (std::size_t HashIndex = 0; HashIndex < Aggregates.size(); ++HashIndex)
|
||||
{
|
||||
std::uint64_t& Hash = Aggregates[HashIndex];
|
||||
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.lod), 4);
|
||||
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.coordinate.x), 4);
|
||||
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.coordinate.y), 4);
|
||||
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.coordinate.z), 4);
|
||||
StandaloneCoreHashWord(Hash, Row.Hashes[HashIndex], 8);
|
||||
}
|
||||
}
|
||||
return Aggregates;
|
||||
}
|
||||
|
||||
bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
{
|
||||
FTCHARToUTF8 Utf8Json(*Json);
|
||||
std::string CoreError;
|
||||
auto OwnerSnapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
|
||||
if (!OwnerSnapshot)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("VoxelForgeCore rejected adapter schema-v1 JSON: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reference hashes use the established stream-benchmark view: clip radius 3 and max LOD 8.
|
||||
// Les hachages de référence utilisent le profil de benchmark : rayon 3 et niveau LOD maximal 8.
|
||||
// Keep asset-derived settings in owner_world.json; apply benchmark controls only to the copy.
|
||||
// Garder les valeurs des assets dans owner_world.json; appliquer les réglages au seul duplicata.
|
||||
const std::int32_t OwnerClipRadius = OwnerSnapshot->configuration().clipRadius;
|
||||
const std::int32_t OwnerMaxLod = OwnerSnapshot->configuration().maxLod;
|
||||
FString CoreValidationJson = Json;
|
||||
const FString AuthoredClipField = FString::Printf(TEXT("\"clip_radius\":%d"), OwnerClipRadius);
|
||||
const FString AuthoredAssetClipField = FString::Printf(TEXT("\"ClipRadius\":%d"), OwnerClipRadius);
|
||||
const FString AuthoredMaxLodField = FString::Printf(TEXT("\"max_clip_level\":%d"), OwnerMaxLod);
|
||||
const FString AuthoredAssetMaxLodField = FString::Printf(TEXT("\"MaxClipLevel\":%d"), OwnerMaxLod);
|
||||
CoreValidationJson.ReplaceInline(*AuthoredClipField, TEXT("\"clip_radius\":3"), ESearchCase::CaseSensitive);
|
||||
CoreValidationJson.ReplaceInline(*AuthoredAssetClipField, TEXT("\"ClipRadius\":3"), ESearchCase::CaseSensitive);
|
||||
CoreValidationJson.ReplaceInline(*AuthoredMaxLodField, TEXT("\"max_clip_level\":8"), ESearchCase::CaseSensitive);
|
||||
CoreValidationJson.ReplaceInline(*AuthoredAssetMaxLodField, TEXT("\"MaxClipLevel\":8"), ESearchCase::CaseSensitive);
|
||||
if (!CoreValidationJson.Contains(TEXT("\"clip_radius\":3"), ESearchCase::CaseSensitive)
|
||||
|| !CoreValidationJson.Contains(TEXT("\"ClipRadius\":3"), ESearchCase::CaseSensitive)
|
||||
|| !CoreValidationJson.Contains(TEXT("\"max_clip_level\":8"), ESearchCase::CaseSensitive)
|
||||
|| !CoreValidationJson.Contains(TEXT("\"MaxClipLevel\":8"), ESearchCase::CaseSensitive))
|
||||
{
|
||||
OutError = TEXT("Core adapter validation could not apply the reference clipmap overrides.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const std::size_t WorkerLimit : {1u, 8u, 16u})
|
||||
{
|
||||
FTCHARToUTF8 Utf8ValidationJson(*CoreValidationJson);
|
||||
auto Snapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(Utf8ValidationJson.Get(),
|
||||
static_cast<std::size_t>(Utf8ValidationJson.Length())), CoreError);
|
||||
if (!Snapshot)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("VoxelForgeCore rejected reference-validation JSON: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
VoxelForgeCore::World CoreWorld(std::move(*Snapshot));
|
||||
const VoxelForgeCore::WorldConfiguration& Configuration = CoreWorld.configuration();
|
||||
const VoxelForgeCore::Vec3d Camera = Configuration.viewerStartFound
|
||||
? Configuration.viewerStartVoxels : VoxelForgeCore::Vec3d{};
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired = CoreWorld.desiredTiles(Camera);
|
||||
if (Desired.empty())
|
||||
{
|
||||
OutError = TEXT("VoxelForgeCore adapter validation produced an empty initial clipmap.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::int32_t IntMax = std::numeric_limits<std::int32_t>::max();
|
||||
const std::int32_t IntMin = std::numeric_limits<std::int32_t>::min();
|
||||
VoxelForgeCore::Vec3i FineMin{IntMax, IntMax, IntMax};
|
||||
VoxelForgeCore::Vec3i FineMax{IntMin, IntMin, IntMin};
|
||||
VoxelForgeCore::Vec3i ClipMin{IntMax, IntMax, IntMax};
|
||||
VoxelForgeCore::Vec3i ClipMax{IntMin, IntMin, IntMin};
|
||||
auto IncludeExtent = [](std::int32_t& Min, std::int32_t& Max,
|
||||
std::int32_t Origin, std::int32_t Extent)
|
||||
{
|
||||
Min = std::min(Min, Origin);
|
||||
Max = std::max(Max, Origin + Extent);
|
||||
};
|
||||
for (const VoxelForgeCore::TileKey& Key : Desired)
|
||||
{
|
||||
const std::int32_t Extent = 32 << Key.lod;
|
||||
const std::int32_t OriginX = Key.coordinate.x * Extent;
|
||||
const std::int32_t OriginY = Key.coordinate.y * Extent;
|
||||
const std::int32_t OriginZ = Key.coordinate.z * Extent;
|
||||
IncludeExtent(ClipMin.x, ClipMax.x, OriginX, Extent);
|
||||
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
|
||||
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
|
||||
if (Key.lod < Configuration.fullResolutionLodCount)
|
||||
{
|
||||
IncludeExtent(FineMin.x, FineMax.x, OriginX, Extent);
|
||||
IncludeExtent(FineMin.y, FineMax.y, OriginY, Extent);
|
||||
IncludeExtent(FineMin.z, FineMax.z, OriginZ, Extent);
|
||||
}
|
||||
}
|
||||
if (FineMin.x == IntMax || ClipMin.x == IntMax)
|
||||
{
|
||||
OutError = TEXT("VoxelForgeCore adapter validation could not derive clipmap bounds.");
|
||||
return false;
|
||||
}
|
||||
|
||||
VoxelForgeCore::ShapeBuildRequest Request;
|
||||
Request.minVoxel = FineMin;
|
||||
Request.maxVoxel = FineMax;
|
||||
Request.clipMinVoxel = ClipMin;
|
||||
Request.clipMaxVoxel = ClipMax;
|
||||
Request.workerLimit = WorkerLimit;
|
||||
Request.detail = VoxelForgeCore::DetailVariant::coherent;
|
||||
Request.includeFarWorld = true;
|
||||
FStandaloneCoreAdapterScheduler Scheduler;
|
||||
if (!CoreWorld.compile(Request, Scheduler, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("VoxelForgeCore adapter compile failed at workers=%llu: %s"),
|
||||
static_cast<unsigned long long>(WorkerLimit), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
VoxelForgeCore::TileGenerationOptions TileOptions;
|
||||
TileOptions.hashDensity = true;
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> Rows;
|
||||
Rows.reserve(Desired.size());
|
||||
for (const VoxelForgeCore::TileKey& Key : Desired)
|
||||
{
|
||||
auto Tile = CoreWorld.generateTile(Key, TileOptions, CoreError);
|
||||
if (!Tile)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("VoxelForgeCore adapter tile generation failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (!Tile->hasDensityHash || !Tile->hasMeshHash
|
||||
|| !Tile->hasPayloadHash || !Tile->hasTopologyHash)
|
||||
{
|
||||
OutError = TEXT("VoxelForgeCore adapter returned a tile without all four hashes.");
|
||||
return false;
|
||||
}
|
||||
Rows.push_back({Key, {Tile->densityHash, Tile->meshHash,
|
||||
Tile->payloadHash, Tile->topologyHash}});
|
||||
}
|
||||
|
||||
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
|
||||
const std::array<std::uint64_t, 4> Expected{
|
||||
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
|
||||
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
|
||||
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
|
||||
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected=5642/5642"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
static_cast<unsigned long long>(Rows.size()),
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
static_cast<unsigned long long>(Actual[1]),
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]),
|
||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||
static_cast<unsigned long long>(Stats.walkSamples));
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreAdapter] pass schema=1 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=5642/5642"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
OwnerClipRadius,
|
||||
OwnerMaxLod,
|
||||
static_cast<unsigned long long>(Rows.size()),
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
static_cast<unsigned long long>(Actual[1]),
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutError, FString* OutJson)
|
||||
{
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||
@@ -12626,7 +12925,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("schema_version"), 1);
|
||||
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
||||
Writer->WriteValue(TEXT("voxel_size_cm"), VOXEL_SIZE);
|
||||
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
|
||||
Writer->WriteValue(TEXT("seed"), Seed);
|
||||
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
@@ -12724,9 +13023,14 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
return false;
|
||||
}
|
||||
if (OutJson) *OutJson = Json;
|
||||
if (!VerifyStandaloneCoreAdapter(Json, OutError))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
||||
Seed, VOXEL_SIZE, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||
Samples.Num(), bCameraFound ? 1 : 0, *Path);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public class VoxelForgeEditor : ModuleRules
|
||||
"Json",
|
||||
"ImageWrapper",
|
||||
"VoxelForge",
|
||||
"VoxelForgeCore",
|
||||
});
|
||||
|
||||
if (Target.Platform == UnrealTargetPlatform.Win64)
|
||||
|
||||
Reference in new issue
Block a user