wip(assembler): round L2 -- the cross-platform hash check can fail; FP contraction changes the world

Clang -ffp-contract=on/fast and GCC's default (fast) all change the density hash on the L1 view;
contraction off reproduces L1 (= Windows S13) at W1/W12. Seed sweep: 48 FloatingIslands seeds and
14 effective SurfaceWorld seeds (34 of 48 fell on a seed-independent cube) match across Clang/GCC
and W1/W12. Standalone gains --seed/--recipe/--render-distance-chunks (viewer + headless) and a
Windows replay script. Linux standalone workers get 32 MiB pthread stacks (TLS is 17.3 MiB/thread),
so no external stack limit is needed. No Core or world-output change; MSVC compile unverified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 18:18:34 +02:00
1 parent 1276fd11e3
commit 303b7b9f24
17 files changed
+1401 -13

No files matched your search

+3 -2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "StandaloneWorkerThread.h"
#include <algorithm>
#include <atomic>
@@ -23,7 +24,7 @@ public:
const std::size_t grain=std::max<std::size_t>(1,batchSize);
std::atomic<std::size_t> next{0};
std::vector<double> active(workers,0.0);
std::vector<std::thread> threads;
std::vector<FStandaloneWorkerThread> threads;
threads.reserve(workers);
for(std::size_t worker=0;worker<workers;++worker)
threads.emplace_back([&,worker]
@@ -39,7 +40,7 @@ public:
active[worker]=std::chrono::duration<double>(
std::chrono::steady_clock::now()-started).count();
});
for(std::thread& thread:threads)thread.join();
for(FStandaloneWorkerThread& thread:threads)thread.join();
double total=0.0;
for(std::size_t worker=0;worker<workers;++worker)
{
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include "StandaloneWorkerThread.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdint>
#include <fstream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#if defined(__linux__)
#include <unistd.h>
#endif
namespace StandaloneTlsProbe
{
#if defined(__linux__)
struct FMemorySample
{
std::uint64_t virtualKiB=0;
std::uint64_t residentKiB=0;
};
inline FMemorySample ReadMemory()
{
std::ifstream statm("/proc/self/statm");
std::uint64_t virtualPages=0,residentPages=0;
statm>>virtualPages>>residentPages;
const long pageSize=sysconf(_SC_PAGESIZE);
if(!statm||pageSize<=0)return {};
return {virtualPages*static_cast<std::uint64_t>(pageSize)/1024,
residentPages*static_cast<std::uint64_t>(pageSize)/1024};
}
inline int Run(int workerCount)
{
workerCount=std::clamp(workerCount,0,12);
std::mutex mutex;
std::condition_variable changed;
bool release=false;
std::atomic<int> ready{0};
std::vector<FStandaloneWorkerThread> workers;
workers.reserve(static_cast<std::size_t>(workerCount));
FMemorySample previous=ReadMemory();
std::printf("[TlsProbe] workers=0 create_ms=0.000 rss_kib=%llu rss_delta_kib=0 vms_kib=%llu stack_bytes=%zu\n",
static_cast<unsigned long long>(previous.residentKiB),
static_cast<unsigned long long>(previous.virtualKiB),
FStandaloneWorkerThread::StackSizeBytes);
for(int index=1;index<=workerCount;++index)
{
const auto start=std::chrono::steady_clock::now();
workers.emplace_back([&]
{
ready.fetch_add(1,std::memory_order_release);
changed.notify_all();
std::unique_lock lock(mutex);
changed.wait(lock,[&]{return release;});
});
{
std::unique_lock lock(mutex);
changed.wait(lock,[&]{return ready.load(std::memory_order_acquire)>=index;});
}
const double createMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-start).count();
const FMemorySample current=ReadMemory();
const std::int64_t rssDelta=static_cast<std::int64_t>(current.residentKiB)
-static_cast<std::int64_t>(previous.residentKiB);
std::printf("[TlsProbe] workers=%d create_ms=%.3f rss_kib=%llu rss_delta_kib=%lld vms_kib=%llu\n",
index,createMs,static_cast<unsigned long long>(current.residentKiB),
static_cast<long long>(rssDelta),static_cast<unsigned long long>(current.virtualKiB));
previous=current;
}
{
std::lock_guard lock(mutex);
release=true;
}
changed.notify_all();
for(FStandaloneWorkerThread& worker:workers)worker.join();
std::printf("[TlsProbe] status=PASS threads_joined=%d\n",workerCount);
return 0;
}
#else
inline int Run(int)
{
std::fprintf(stderr,"TLS probe is Linux-only.\n");
return 2;
}
#endif
}
+47 -1
View File
@@ -8877,6 +8877,11 @@ int main(int Argc,char** Argv)
bool bStreamPipeline=false;
bool bProfileColumnOverlap=false;
bool bCoarseFirstStreamOrder=false;
bool bHasSeedOverride=false;
int32 SeedOverride=0;
bool bHasRenderDistanceOverride=false;
int32 RenderDistanceOverride=0;
bool bFloatingIslandsRecipe=false;
bool bProfileDetailBand=false;
bool bPassageDetailAuditOnly=false;
bool bGridStairAudit=false;
@@ -8898,6 +8903,33 @@ int main(int Argc,char** Argv)
{
const std::string_view Arg=Argv[I];
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
else if(Arg.starts_with("--seed="))
{
const std::string Value(Arg.substr(std::string_view("--seed=").size()));
char* End=nullptr;
const long long Parsed=std::strtoll(Value.c_str(),&End,10);
if(End==Value.c_str()||*End!='\0'||Parsed<INT32_MIN||Parsed>INT32_MAX)
{std::fprintf(stderr,"Invalid --seed value: %s\n",Value.c_str());return 2;}
SeedOverride=static_cast<int32>(Parsed);bHasSeedOverride=true;
}
else if(Arg.starts_with("--render-distance-chunks="))
{
const std::string Value(Arg.substr(std::string_view("--render-distance-chunks=").size()));
char* End=nullptr;
const long long Parsed=std::strtoll(Value.c_str(),&End,10);
if(End==Value.c_str()||*End!='\0'||Parsed<0||Parsed>INT32_MAX)
{std::fprintf(stderr,"Invalid --render-distance-chunks value: %s\n",Value.c_str());return 2;}
RenderDistanceOverride=static_cast<int32>(Parsed);bHasRenderDistanceOverride=true;
}
else if(Arg.starts_with("--recipe="))
{
const std::string_view Recipe=Arg.substr(std::string_view("--recipe=").size());
if(Recipe=="surface-world")bFloatingIslandsRecipe=false;
else if(Recipe=="floating-islands")
{bFloatingIslandsRecipe=true;App.bWorldAssemblerPrototype=true;App.bShapeWorld=true;}
else
{std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(Recipe.size()),Recipe.data());return 2;}
}
else if(Arg.starts_with("--far-horizon-lod8-step="))
App.FarHorizonLOD8InteriorStepVoxels=std::max(0,std::atoi(
std::string(Arg.substr(std::string_view("--far-horizon-lod8-step=").size())).c_str()));
@@ -9134,6 +9166,16 @@ int main(int Argc,char** Argv)
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(bHasSeedOverride
&&!StandaloneWorldAssemblyPrototype::OverrideStandaloneSeed(App.OwnerJsonRoot,SeedOverride,Error))
{std::fprintf(stderr,"Seed override failed: %s\n",Error.c_str());return 2;}
if(bHasRenderDistanceOverride
&&!StandaloneWorldAssemblyPrototype::OverrideStandaloneRenderDistance(
App.OwnerJsonRoot,RenderDistanceOverride,Error))
{std::fprintf(stderr,"Render-distance override failed: %s\n",Error.c_str());return 2;}
if(bFloatingIslandsRecipe
&&!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(App.OwnerJsonRoot,Error))
{std::fprintf(stderr,"Floating-island recipe selection 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;}
std::printf("[ColdTimeline] stage=owner_data_load end_ms=%.3f\n",
@@ -9165,8 +9207,12 @@ int main(int Argc,char** Argv)
if(App.bWorldAssemblerPrototype)
{
const auto PlanStart=std::chrono::steady_clock::now();
VoxelForgeCore::WorldAssemblyManifest WorldAssembly;
const bool bPlanReady=StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot,
Error,WorldAssemblyManifestOutputPath);
Error,WorldAssemblyManifestOutputPath,&WorldAssembly)
&&(!bFloatingIslandsRecipe
||StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
*App.World,App.OwnerJsonRoot,WorldAssembly,Error));
const double PlanMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-PlanStart).count();
std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=%s\n",
@@ -0,0 +1,96 @@
#pragma once
#include <cstddef>
#include <memory>
#include <stdexcept>
#include <system_error>
#include <thread>
#include <type_traits>
#include <utility>
#if defined(__linux__)
#include <pthread.h>
#endif
// Linux's static TLS image is larger than the default pthread stack in this standalone build.
// Give only host-owned generation workers enough stack for that TLS plus normal call frames.
#if defined(__linux__)
class FStandaloneWorkerThread
{
public:
static constexpr std::size_t StackSizeBytes=32u*1024u*1024u;
FStandaloneWorkerThread()=default;
template<class F>
explicit FStandaloneWorkerThread(F&& function)
{
using FTask=std::decay_t<F>;
auto task=std::make_unique<FTask>(std::forward<F>(function));
pthread_attr_t attributes;
int result=pthread_attr_init(&attributes);
if(result!=0)throw std::system_error(result,std::generic_category(),"pthread_attr_init");
result=pthread_attr_setstacksize(&attributes,StackSizeBytes);
if(result==0)
{
result=pthread_create(&thread_,&attributes,&RunTask<FTask>,task.get());
if(result==0)task.release();
}
const int destroyResult=pthread_attr_destroy(&attributes);
if(result!=0)throw std::system_error(result,std::generic_category(),"pthread_create");
if(destroyResult!=0)
{
(void)pthread_join(thread_,nullptr);
throw std::system_error(destroyResult,std::generic_category(),"pthread_attr_destroy");
}
joinable_=true;
}
FStandaloneWorkerThread(const FStandaloneWorkerThread&)=delete;
FStandaloneWorkerThread& operator=(const FStandaloneWorkerThread&)=delete;
FStandaloneWorkerThread(FStandaloneWorkerThread&& other) noexcept
:thread_(other.thread_),joinable_(std::exchange(other.joinable_,false))
{}
FStandaloneWorkerThread& operator=(FStandaloneWorkerThread&& other) noexcept
{
if(this!=&other)
{
if(joinable_)(void)pthread_join(thread_,nullptr);
thread_=other.thread_;
joinable_=std::exchange(other.joinable_,false);
}
return *this;
}
~FStandaloneWorkerThread()
{
if(joinable_)(void)pthread_join(thread_,nullptr);
}
bool joinable()const noexcept{return joinable_;}
void join()
{
if(!joinable_)throw std::system_error(std::make_error_code(std::errc::invalid_argument));
const int result=pthread_join(thread_,nullptr);
if(result!=0)throw std::system_error(result,std::generic_category(),"pthread_join");
joinable_=false;
}
private:
template<class F>
static void* RunTask(void* raw)
{
std::unique_ptr<F> task(static_cast<F*>(raw));
(*task)();
return nullptr;
}
pthread_t thread_{};
bool joinable_=false;
};
#else
using FStandaloneWorkerThread=std::thread;
#endif
+54 -3
View File
@@ -2,6 +2,7 @@
#include "StandaloneCoreScheduler.h"
#include "StreamBenchmarkPipeline.h"
#include "WorldAssemblyPrototype.h"
#include "StandaloneTlsProbe.h"
#include "../Core/src/StandaloneSnapshotAdapter.h"
#include <algorithm>
@@ -32,6 +33,13 @@ namespace
bool StreamHash=false;
bool ProfileColumnOverlap=false;
bool ForceGenerate=false;
bool HasSeedOverride=false;
int SeedOverride=0;
bool HasRenderDistanceOverride=false;
int RenderDistanceOverride=0;
bool FloatingIslandsRecipe=false;
bool TlsProbeRequested=false;
int TlsProbeWorkers=0;
bool HasCamera=false;
FVector CameraMeters=FVector::ZeroVector;
};
@@ -72,6 +80,9 @@ namespace
" --workers=N --clip-radius=N --max-clip-level=N --max-level=N\n"
" --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n"
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
" --seed=N --recipe=surface-world|floating-islands\n"
" --render-distance-chunks=N (0 disables the extra far ring)\n"
" --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n"
" --process-affinity=MASK --process-priority=Normal --frame-limit=FPS (viewer compatibility only)\n",
argv[0]);
return false;
@@ -82,6 +93,28 @@ namespace
else if(arg=="--stream-hash")options.StreamHash=true;
else if(arg=="--profile-column-overlap")options.ProfileColumnOverlap=true;
else if(arg=="--force-generate")options.ForceGenerate=true;
else if(arg.starts_with("--recipe="))
{
const std::string_view recipe=arg.substr(std::string_view("--recipe=").size());
if(recipe=="surface-world")options.FloatingIslandsRecipe=false;
else if(recipe=="floating-islands")options.FloatingIslandsRecipe=true;
else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(recipe.size()),recipe.data());return false;}
}
else if(arg.starts_with("--seed="))
{if(!readInt("--seed=",options.SeedOverride))return false;options.HasSeedOverride=true;}
else if(arg.starts_with("--render-distance-chunks="))
{
if(!readInt("--render-distance-chunks=",options.RenderDistanceOverride))return false;
options.RenderDistanceOverride=std::max(0,options.RenderDistanceOverride);
options.HasRenderDistanceOverride=true;
}
else if(arg.starts_with("--tls-probe="))
{
if(!readInt("--tls-probe=",options.TlsProbeWorkers))return false;
if(options.TlsProbeWorkers<0||options.TlsProbeWorkers>12)
{std::fprintf(stderr,"--tls-probe must be in 0..12\n");return false;}
options.TlsProbeRequested=true;
}
else if(arg.starts_with("--camera="))
{
if(!ParseCamera(arg.substr(9),options.CameraMeters))
@@ -112,6 +145,7 @@ namespace
options.ClipRadius=std::clamp(options.ClipRadius,1,64);
options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8);
options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels);
if(options.TlsProbeRequested)return true;
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
{
std::fprintf(stderr,"This headless target runs the world-assembler streaming pipeline; pass --world-assembler-prototype --stream-benchmark --stream-pipeline.\n");
@@ -127,10 +161,12 @@ int main(int argc,char** argv)
const auto coldStart=Clock::now();
FOptions options;
if(!ParseOptions(argc,argv,options))return 2;
std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s\n",
if(options.TlsProbeRequested)return StandaloneTlsProbe::Run(options.TlsProbeWorkers);
std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s recipe=%s seed_override=%s render_distance_override=%s\n",
options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel,
options.MaxLevel,options.StreamHash?"on":"off",options.ProfileColumnOverlap?"on":"off",
options.ForceGenerate?"on":"off");
options.ForceGenerate?"on":"off",options.FloatingIslandsRecipe?"floating-islands":"surface-world",
options.HasSeedOverride?"yes":"no",options.HasRenderDistanceOverride?"yes":"no");
std::printf("[ColdTimeline] stage=owner_data_load begin_ms=%.3f\n",
std::chrono::duration<double,std::milli>(Clock::now()-coldStart).count());
@@ -147,6 +183,16 @@ int main(int argc,char** argv)
if(!VFJson::Parser(std::move(ownerJsonText)).Parse(ownerJsonRoot,error))
{std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;}
}
if(options.HasSeedOverride
&&!StandaloneWorldAssemblyPrototype::OverrideStandaloneSeed(ownerJsonRoot,options.SeedOverride,error))
{std::fprintf(stderr,"Seed override failed: %s\n",error.c_str());return 2;}
if(options.HasRenderDistanceOverride
&&!StandaloneWorldAssemblyPrototype::OverrideStandaloneRenderDistance(
ownerJsonRoot,options.RenderDistanceOverride,error))
{std::fprintf(stderr,"Render-distance override failed: %s\n",error.c_str());return 2;}
if(options.FloatingIslandsRecipe
&&!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(ownerJsonRoot,error))
{std::fprintf(stderr,"Floating-island recipe selection failed: %s\n",error.c_str());return 2;}
if(!StandaloneOwnerLoader::Resolve(ownerJsonRoot,*ownerWorld,error))
{std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;}
std::printf("[ColdTimeline] stage=owner_data_load end_ms=%.3f\n",
@@ -167,9 +213,14 @@ int main(int argc,char** argv)
cameraVoxels=FVector(options.CameraMeters.X*4.0f,options.CameraMeters.Y*4.0f,options.CameraMeters.Z*4.0f);
const auto planStart=Clock::now();
VoxelForgeCore::WorldAssemblyManifest worldAssembly;
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
*ownerWorld,ownerJsonRoot,error,{}))
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
if(options.FloatingIslandsRecipe
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
*ownerWorld,ownerJsonRoot,worldAssembly,error))
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=PASS\n",
std::chrono::duration<double,std::milli>(Clock::now()-planStart).count());
@@ -78,6 +78,28 @@ namespace StandaloneStreamBenchmark
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());
if(!Desired.Tiles.empty())
{
FIntVector volumeMin(INT32_MAX,INT32_MAX,INT32_MAX);
FIntVector volumeMax(INT32_MIN,INT32_MIN,INT32_MIN);
int32 minimumLod=INT32_MAX,maximumLod=INT32_MIN;
for(const FVoxelTileKey& tile:Desired.Tiles)
{
const FIntVector origin=tile.OriginVoxels();
const int32 extent=tile.ExtentVoxels();
volumeMin.X=std::min(volumeMin.X,origin.X);
volumeMin.Y=std::min(volumeMin.Y,origin.Y);
volumeMin.Z=std::min(volumeMin.Z,origin.Z);
volumeMax.X=std::max(volumeMax.X,origin.X+extent);
volumeMax.Y=std::max(volumeMax.Y,origin.Y+extent);
volumeMax.Z=std::max(volumeMax.Z,origin.Z+extent);
minimumLod=std::min(minimumLod,tile.Level);
maximumLod=std::max(maximumLod,tile.Level);
}
std::printf("[PipelineVolume] tiles=%zu lod=%d..%d min_voxel=(%d,%d,%d) max_voxel=(%d,%d,%d)\n",
Desired.Tiles.size(),minimumLod,maximumLod,volumeMin.X,volumeMin.Y,volumeMin.Z,
volumeMax.X,volumeMax.Y,volumeMax.Z);
}
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
for(const FVoxelTileKey& Tile:Desired.Tiles)
+7 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "WorldMesher.h"
#include "StandaloneWorkerThread.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <condition_variable>
@@ -11,10 +12,12 @@
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <deque>
#include <functional>
#include <fstream>
#include <limits>
#include <unordered_set>
#include <utility>
@@ -509,7 +512,7 @@ public:
Queue.clear();PriorityTasks.clear();
}
Wake.notify_all();
for(std::thread& Worker:Workers)if(Worker.joinable())Worker.join();
for(FStandaloneWorkerThread& Worker:Workers)if(Worker.joinable())Worker.join();
Workers.clear();
std::lock_guard<std::mutex> Lock(Mutex);
Ready.clear();Outstanding.clear();Desired.clear();Running.clear();
@@ -797,7 +800,8 @@ private:
{
VoxelForgeCore::TileGenerationOptions Options;
Options.bandMinZVoxel=Job.BandZMin;Options.bandMaxZVoxel=Job.BandZMax;
Options.hashDensity=bHashDensity;Options.skipUvAndColors=bSkipUvAndColors;
Options.hashDensity=bHashDensity;
Options.skipUvAndColors=bSkipUvAndColors;
Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors;
Options.captureColumnCoordinates=bCaptureColumnCoordinates;
Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels;
@@ -977,7 +981,7 @@ private:
mutable std::mutex Mutex;
std::condition_variable Wake;
bool bStop=true;
std::vector<std::thread> Workers;
std::vector<FStandaloneWorkerThread> Workers;
std::deque<std::function<void()>> PriorityTasks;
std::vector<FStandaloneTileJob> Queue;
std::vector<FStandaloneCpuTile> Ready;
+104 -1
View File
@@ -10,7 +10,9 @@
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <limits>
#include <numeric>
#include <string>
#include <vector>
@@ -186,7 +188,8 @@ namespace StandaloneWorldAssemblyPrototype
}
inline bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root,
std::string& error,const std::string& manifestOutputPath)
std::string& error,const std::string& manifestOutputPath,
VoxelForgeCore::WorldAssemblyManifest* outManifest=nullptr)
{
if(!owner.Manager||!owner.Generator)
{error="world assembler prototype needs the resolved standalone owner world";return false;}
@@ -384,6 +387,7 @@ namespace StandaloneWorldAssemblyPrototype
stratum.routeConstraints.minimumSupportProbes);
manifest.plan.strates.push_back(std::move(stratum));
if(!manifest.finalize(error))return false;
if(outManifest)*outManifest=manifest;
const std::string manifestText=manifest.ToJson();
const auto roundTrip=VoxelForgeCore::WorldAssemblyManifest::FromJson(manifestText,error);
if(!roundTrip||!roundTrip->validate(error)||roundTrip->ToJson()!=manifestText
@@ -424,4 +428,103 @@ namespace StandaloneWorldAssemblyPrototype
return true;
}
inline bool OverrideStandaloneSeed(VFJson::Value& root,int32 seed,std::string& error)
{
auto* object=std::get_if<VFJson::Value::Object>(&root.Data);
if(!object){error="owner snapshot root is not a JSON object";return false;}
(*object)["seed"].Data=static_cast<double>(seed);
return true;
}
inline bool OverrideStandaloneRenderDistance(VFJson::Value& root,int32 chunks,std::string& error)
{
auto* object=std::get_if<VFJson::Value::Object>(&root.Data);
if(!object){error="owner snapshot root is not a JSON object";return false;}
(*object)["render_distance_chunks"].Data=static_cast<double>(std::max(0,chunks));
return true;
}
inline bool SelectTopStrateAsFloatingIslands(VFJson::Value& root,std::string& error)
{
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
if(!rootObject){error="owner snapshot root is not a JSON object";return false;}
auto stratesEntry=rootObject->find("strates");
if(stratesEntry==rootObject->end())
{error="owner snapshot has no strata array";return false;}
auto* strates=std::get_if<VFJson::Value::Array>(&stratesEntry->second.Data);
if(!strates||strates->empty())
{error="owner snapshot strata array is empty";return false;}
auto top=strates->begin();
for(auto it=std::next(strates->begin());it!=strates->end();++it)
if(it->Get("top_chunk_z").Number()>top->Get("top_chunk_z").Number())top=it;
auto* strateObject=std::get_if<VFJson::Value::Object>(&top->Data);
if(!strateObject){error="top owner strate is not an object";return false;}
auto assetEntry=strateObject->find("asset");
if(assetEntry==strateObject->end())
{error="top owner strate has no resolved asset";return false;}
auto* asset=std::get_if<VFJson::Value::Object>(&assetEntry->second.Data);
if(!asset){error="top owner strate asset is not an object";return false;}
(*asset)["GeneratorType"].Data=static_cast<double>(
static_cast<std::uint8_t>(ECaveGeneratorType::FloatingIslands));
return true;
}
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
{
if(!owner.Manager)
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
const FStrateSlot* islandSlot=nullptr;
for(const FStrateSlot& slot:owner.Manager->GetLayout())
if(slot.Definition&&slot.Definition->GeneratorType==ECaveGeneratorType::FloatingIslands)
{islandSlot=&slot;break;}
if(!islandSlot)
{error="floating-island recipe requested but no FloatingIslands strate was resolved";return false;}
const std::uint64_t divisor=std::gcd<std::uint64_t>(owner.OwnerVoxelSizeMicrons,
owner.CoreVoxelSizeMicrons);
if(divisor==0)
{error="owner/Core voxel import ratio is invalid for the floating-island recipe";return false;}
VoxelForgeCore::FloatingIslandRecipeV1 recipe;
recipe.seed=static_cast<std::uint32_t>(owner.Seed);
recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons;
recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons;
recipe.ownerToCoreScaleNumerator=owner.OwnerVoxelSizeMicrons/static_cast<std::uint32_t>(divisor);
recipe.ownerToCoreScaleDenominator=owner.CoreVoxelSizeMicrons/static_cast<std::uint32_t>(divisor);
const std::int64_t coreVoxelsPerOwnerChunk=static_cast<std::int64_t>(CHUNK_SIZE)
*recipe.ownerToCoreScaleNumerator/recipe.ownerToCoreScaleDenominator;
const std::int64_t minimumZ=static_cast<std::int64_t>(islandSlot->BottomChunkZ)
*coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
+2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const std::int64_t maximumZ=static_cast<std::int64_t>(islandSlot->TopChunkZ+1)
*coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
-2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
if(maximumZ<=minimumZ)
{error="FloatingIslands strate is too short for a bounded Core recipe";return false;}
recipe.layoutMinimumZSubvoxels=minimumZ;
recipe.layoutMaximumZSubvoxels=maximumZ;
recipe.layoutCenterTopZSubvoxels=minimumZ+(maximumZ-minimumZ)/2;
auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
if(!planned)return false;
VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1 islands;
islands.strateIndex=islandSlot->StrateIndex;
islands.topChunkZ=islandSlot->TopChunkZ;
islands.bottomChunkZ=islandSlot->BottomChunkZ;
islands.manifest=std::move(*planned);
assembly.floatingIslands.push_back(std::move(islands));
if(!assembly.finalize(error))return false;
VFJson::Value assemblyJson;
if(!VFJson::Parser(assembly.ToJson()).Parse(assemblyJson,error))return false;
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
if(!rootObject){error="owner snapshot root is not a JSON object";return false;}
(*rootObject)["world_assembly"]=std::move(assemblyJson);
std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu recipe_hash=%016llx status=PASS\n",
owner.Seed,islandSlot->StrateIndex,islandSlot->TopChunkZ,islandSlot->BottomChunkZ,
assembly.floatingIslands.back().manifest.plan.islands.size(),
static_cast<unsigned long long>(assembly.floatingIslands.back().manifest.recipe.contentHash));
return true;
}
} // namespace StandaloneWorldAssemblyPrototype