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:
1 parent
1276fd11e3
commit
303b7b9f24
17 files changed
+1401
-13
No files matched your search
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user