wip(assembler): round L7 -- shared column cache across workers: whole view 16-19% faster, world unchanged

SharedColumnCache.h: a world-owned, bounded 4-way second-tier cache of exact column values
(heightfield and floating islands), keyed by recipe/plan identity plus content hash, XY and island
id. Lock-free seqlock reads; set-local spin lock on misses. The FLazy thread-local tier stays
first. 83.6% of repeated column work was cross-worker; formula evaluations -60.9%.
W12 L1 view: Luna 2.165 -> 1.754 s; reviewer interleaved 2.059 -> 1.722 s. 8 m crossing new tiles
82 -> 52 ms. +66 MiB peak RSS. All hashes (whole view, per-LOD, exact queries, islands seed 0)
identical at W1/W8/W12, Clang off and GCC default. MSVC/UBT not compiled here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 21:46:15 +02:00
1 parent 9898e057fd
commit 4b1108bf86
12 files changed
+751 -92

No files matched your search

@@ -0,0 +1,183 @@
#pragma once
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <thread>
#include <type_traits>
#include <utility>
namespace VoxelForgeCoreShared
{
// Exact input identity for a pure XY column value. LOD is deliberately not part of this key:
// the same recipe/plan column has the same bits at every sampling step.
struct FColumnCacheKey
{
std::uint64_t recipeIdentity = 0;
std::uint64_t recipeContentHash = 0;
std::uint64_t planIdentity = 0;
std::uint64_t planContentHash = 0;
std::int64_t xSubvoxels = 0;
std::int64_t ySubvoxels = 0;
// Optional exact sub-domain identity for recipes with several independent columns
// at the same XY (for example, a floating-island anchor).
std::uint64_t contextIdentity = 0;
std::array<std::uint64_t, 7> words() const noexcept
{
return {recipeIdentity, recipeContentHash, planIdentity, planContentHash,
static_cast<std::uint64_t>(xSubvoxels), static_cast<std::uint64_t>(ySubvoxels),
contextIdentity};
}
};
// A bounded, world-owned second-level cache. Existing thread-local caches stay first, so
// their ordinary hit path remains private and lock-free. Shared hits read atomic words; only
// a shared miss takes the set spin lock. Entries are four-way set associative, evict on
// collision, and carry pointer identities plus content hashes to reject stale reuse.
// Les caches TLS restent le premier niveau; seul un defaut partage prend le verrou du groupe.
template <typename TValue, std::size_t SetCount, std::size_t Ways = 4>
class TSharedColumnValueCache
{
static_assert(SetCount > 0 && (SetCount & (SetCount - 1)) == 0,
"column cache set count must be a power of two");
static_assert(Ways > 0 && Ways <= 16, "unsupported column cache associativity");
static_assert(std::is_trivially_copyable_v<TValue>,
"shared column values must be trivially copyable");
static constexpr std::size_t ValueWordCount = (sizeof(TValue) + 7) / 8;
using FKeyWords = std::array<std::uint64_t, 7>;
using FValueWords = std::array<std::uint64_t, ValueWordCount>;
struct FEntry
{
std::atomic<std::uint64_t> Version{0};
std::atomic<std::uint64_t> Tag{0};
std::array<std::atomic<std::uint64_t>, 7> Key{};
std::array<std::atomic<std::uint64_t>, ValueWordCount> Value{};
};
struct FSet
{
std::atomic_flag Writer = ATOMIC_FLAG_INIT;
std::uint8_t NextWay = 0;
std::array<FEntry, Ways> Entries{};
};
struct FSpinGuard
{
explicit FSpinGuard(std::atomic_flag& value) : Flag(value)
{
while (Flag.test_and_set(std::memory_order_acquire))
std::this_thread::yield();
}
~FSpinGuard() { Flag.clear(std::memory_order_release); }
std::atomic_flag& Flag;
};
public:
TSharedColumnValueCache() : Sets(std::make_unique<FSet[]>(SetCount)) {}
TSharedColumnValueCache(const TSharedColumnValueCache&) = delete;
TSharedColumnValueCache& operator=(const TSharedColumnValueCache&) = delete;
static TSharedColumnValueCache* setThreadActive(TSharedColumnValueCache* value) noexcept
{
TSharedColumnValueCache* previous = Active;
Active = value;
return previous;
}
static TSharedColumnValueCache* threadActive() noexcept { return Active; }
template <typename TFactory>
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
bool& computed)
{
computed = false;
const FKeyWords keyWords = key.words();
const std::uint64_t tag = Hash(keyWords);
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
FSet& set = Sets[setIndex];
TValue value{};
if (TryReadSet(set, keyWords, tag, value)) return value;
FSpinGuard lock(set.Writer);
if (TryReadSet(set, keyWords, tag, value)) return value;
value = std::forward<TFactory>(factory)();
computed = true;
FValueWords valueWords{};
std::memcpy(valueWords.data(), &value, sizeof(TValue));
std::size_t replacement = Ways;
for (std::size_t way = 0; way < Ways; ++way)
if (set.Entries[way].Version.load(std::memory_order_seq_cst) == 0)
{ replacement = way; break; }
if (replacement == Ways)
{
replacement = set.NextWay;
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
}
FEntry& entry = set.Entries[replacement];
std::uint64_t version = entry.Version.load(std::memory_order_seq_cst);
if (version & 1u) ++version;
entry.Version.store(version + 1, std::memory_order_seq_cst);
for (std::size_t word = 0; word < keyWords.size(); ++word)
entry.Key[word].store(keyWords[word], std::memory_order_seq_cst);
for (std::size_t word = 0; word < valueWords.size(); ++word)
entry.Value[word].store(valueWords[word], std::memory_order_seq_cst);
entry.Tag.store(tag, std::memory_order_seq_cst);
entry.Version.store(version + 2, std::memory_order_seq_cst);
return value;
}
private:
static std::uint64_t Hash(const FKeyWords& words) noexcept
{
std::uint64_t hash = 0x9E3779B185EBCA87ull;
for (const std::uint64_t word : words)
{
hash ^= word + 0x9E3779B97F4A7C15ull + (hash << 6) + (hash >> 2);
hash ^= hash >> 30;
hash *= 0xBF58476D1CE4E5B9ull;
hash ^= hash >> 27;
hash *= 0x94D049BB133111EBull;
hash ^= hash >> 31;
}
return hash == 0 ? 1 : hash;
}
static bool TryRead(const FEntry& entry, const FKeyWords& keyWords,
std::uint64_t tag, TValue& out)
{
const std::uint64_t before = entry.Version.load(std::memory_order_seq_cst);
if (before == 0 || (before & 1u)
|| entry.Tag.load(std::memory_order_seq_cst) != tag) return false;
FKeyWords foundKey{};
for (std::size_t word = 0; word < foundKey.size(); ++word)
foundKey[word] = entry.Key[word].load(std::memory_order_seq_cst);
if (foundKey != keyWords) return false;
FValueWords foundValue{};
for (std::size_t word = 0; word < foundValue.size(); ++word)
foundValue[word] = entry.Value[word].load(std::memory_order_seq_cst);
const std::uint64_t after = entry.Version.load(std::memory_order_seq_cst);
if (before != after || (after & 1u)) return false;
std::memcpy(&out, foundValue.data(), sizeof(TValue));
return true;
}
static bool TryReadSet(const FSet& set, const FKeyWords& keyWords,
std::uint64_t tag, TValue& out)
{
for (const FEntry& entry : set.Entries)
if (TryRead(entry, keyWords, tag, out)) return true;
return false;
}
std::unique_ptr<FSet[]> Sets;
inline static thread_local TSharedColumnValueCache* Active = nullptr;
};
}
@@ -110,6 +110,13 @@ namespace VoxelForgeCore
enum class TileClass : std::uint8_t { allSolid, allAir, mixed, unknown };
struct HeightfieldColumnEvaluation
{
std::int64_t x = 0, y = 0;
std::uint64_t sequence = 0;
bool outsideTileFootprint = false;
};
struct TileGenerationStats
{
TileClass tileClass = TileClass::unknown;
@@ -124,6 +131,8 @@ namespace VoxelForgeCore
std::uint64_t columnCacheHits = 0;
std::uint64_t columnCacheMisses = 0;
std::uint64_t columnCacheConflictMisses = 0;
std::uint64_t sharedColumnCacheHits = 0;
std::uint64_t columnEvaluations = 0;
double classificationMilliseconds = 0.0;
double columnComputeMilliseconds = 0.0;
double columnEvaluationExclusiveMilliseconds = 0.0;
@@ -168,7 +177,7 @@ namespace VoxelForgeCore
bool capRefined = false;
std::uint64_t columnEvaluationWorkerHash = 0;
bool columnCoordinateCaptureComplete = true;
std::vector<std::array<std::int64_t, 2>> evaluatedColumnCoordinates;
std::vector<HeightfieldColumnEvaluation> evaluatedColumnCoordinates;
};
struct TileMesh
@@ -651,10 +660,12 @@ namespace VoxelForgeCore
std::uint64_t columnHits = 0;
std::uint64_t columnMisses = 0;
std::uint64_t columnConflictMisses = 0;
std::uint64_t sharedColumnHits = 0;
std::uint64_t columnEvaluations = 0;
double columnComputeMilliseconds = 0.0;
bool captureCoordinates = false;
bool coordinateCaptureComplete = true;
std::vector<std::array<std::int64_t, 2>> evaluatedCoordinates;
std::vector<HeightfieldColumnEvaluation> evaluatedCoordinates;
};
struct FieldInfluenceBounds
@@ -705,6 +716,9 @@ namespace VoxelForgeCore
std::int64_t maximumZSubvoxels, std::int32_t stepVoxels) const noexcept;
static void resetThreadColumnCacheStats(bool captureCoordinates = false,
bool clearCache = false, std::uint32_t octaveDrop = 0) noexcept;
static void setThreadColumnCaptureBounds(std::int64_t minimumXSubvoxels,
std::int64_t minimumYSubvoxels, std::int64_t maximumXSubvoxels,
std::int64_t maximumYSubvoxels) noexcept;
static void setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept;
static std::uint32_t threadColumnOctaveDrop() noexcept;
static void setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept;
@@ -1038,6 +1052,7 @@ namespace VoxelForgeCore
// Opt-in diagnostic only; records XY columns evaluated by this LOD8 tile.
// Diagnostic seulement; enregistre les colonnes XY evaluees par cette tuile LOD8.
bool captureColumnCoordinates = false;
bool useSharedColumnCache = true;
// Separate cold pass only; samples the MC inner stages without changing output.
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
bool profileMesherStages = false;
@@ -55,6 +55,7 @@
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "VoxelForgeCore/ThreadLocalStorage.h"
#include "VoxelForgeCore/SharedColumnCache.h"
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
+67 -34
View File
@@ -425,6 +425,9 @@ namespace floating_island_detail
struct FIslandColumnCacheEntry
{
bool bValid=false;
const FloatingIslandRecipeV1* RecipeIdentity=nullptr;
const FloatingIslandPlanV1* PlanIdentity=nullptr;
std::uint64_t RecipeContentHash=0,PlanContentHash=0;
std::uint32_t Seed=0;
std::uint64_t StableId=0;
std::int64_t SampleX=0,SampleY=0,CenterX=0,CenterY=0,TopZ=0,Radius=0,TopHalf=0;
@@ -432,9 +435,12 @@ namespace floating_island_detail
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
};
using FSharedIslandColumnCache =
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
inline const FIslandColumnCacheEntry& IslandColumnCache(
const FloatingIslandRecipeV1& recipe,const FloatingIslandAnchorV1& island,
double x,double y)
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
const FloatingIslandAnchorV1& island,double x,double y)
{
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
// across Z samples without changing the field value or touching shared world state.
@@ -450,7 +456,10 @@ namespace floating_island_detail
std::uint64_t IndexKey=island.stableId
^(static_cast<std::uint64_t>(SampleX)*0x9e3779b97f4a7c15ull)
^(static_cast<std::uint64_t>(SampleY)*0xc2b2ae3d27d4eb4full)
^(static_cast<std::uint64_t>(recipe.seed)<<23);
^(static_cast<std::uint64_t>(recipe.seed)<<23)
^recipe.contentHash^std::rotl(plan.contentHash,17)
^static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe))
^std::rotl(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),31);
IndexKey^=IndexKey>>33;
IndexKey*=0xff51afd7ed558ccdull;
IndexKey^=IndexKey>>33;
@@ -459,7 +468,9 @@ namespace floating_island_detail
for(std::size_t Way=0;Way<Ways;++Way)
{
FIslandColumnCacheEntry& Entry=cache[Base+Way];
const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed
const bool bKeyMatches=Entry.bValid&&Entry.RecipeIdentity==&recipe
&&Entry.PlanIdentity==&plan&&Entry.RecipeContentHash==recipe.contentHash
&&Entry.PlanContentHash==plan.contentHash&&Entry.Seed==recipe.seed
&&Entry.StableId==island.stableId
&&Entry.SampleX==SampleX&&Entry.SampleY==SampleY
&&Entry.CenterX==island.centerSubvoxels[0]
@@ -473,32 +484,53 @@ namespace floating_island_detail
if(!Entry.bValid)Replacement=&Entry;
}
FIslandColumnCacheEntry& Entry=*Replacement;
Entry.bValid=true;
Entry.Seed=recipe.seed;
Entry.StableId=island.stableId;
Entry.SampleX=SampleX;
Entry.SampleY=SampleY;
Entry.CenterX=island.centerSubvoxels[0];
Entry.CenterY=island.centerSubvoxels[1];
Entry.TopZ=island.topZSubvoxels;
Entry.Radius=island.radiusSubvoxels;
Entry.TopHalf=island.topHalfSubvoxels;
Entry.TopFlattenQ16=recipe.topFlattenQ16;
Entry.ContourWarp=recipe.contourWarpSubvoxels;
Entry.DeltaX=x-static_cast<double>(island.centerSubvoxels[0]);
Entry.DeltaY=y-static_cast<double>(island.centerSubvoxels[1]);
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
Entry.LobePhase=LobePhase(recipe,island);
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
Entry.LobePhase));
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
Entry.TopSurface=static_cast<double>(island.topZSubvoxels)-Drop;
return Entry;
const auto build=[&]()
{
FIslandColumnCacheEntry Entry;
Entry.bValid=true;
Entry.RecipeIdentity=&recipe;
Entry.PlanIdentity=&plan;
Entry.RecipeContentHash=recipe.contentHash;
Entry.PlanContentHash=plan.contentHash;
Entry.Seed=recipe.seed;
Entry.StableId=island.stableId;
Entry.SampleX=SampleX;
Entry.SampleY=SampleY;
Entry.CenterX=island.centerSubvoxels[0];
Entry.CenterY=island.centerSubvoxels[1];
Entry.TopZ=island.topZSubvoxels;
Entry.Radius=island.radiusSubvoxels;
Entry.TopHalf=island.topHalfSubvoxels;
Entry.TopFlattenQ16=recipe.topFlattenQ16;
Entry.ContourWarp=recipe.contourWarpSubvoxels;
Entry.DeltaX=x-static_cast<double>(island.centerSubvoxels[0]);
Entry.DeltaY=y-static_cast<double>(island.centerSubvoxels[1]);
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
Entry.LobePhase=LobePhase(recipe,island);
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
Entry.LobePhase));
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
Entry.TopSurface=static_cast<double>(island.topZSubvoxels)-Drop;
return Entry;
};
FIslandColumnCacheEntry Entry{};
if(auto* shared=FSharedIslandColumnCache::threadActive())
{
const VoxelForgeCoreShared::FColumnCacheKey key{
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe)),
recipe.contentHash,
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
plan.contentHash,SampleX,SampleY,island.stableId};
bool computed=false;
Entry=shared->getOrCompute(key,build,computed);
}
else Entry=build();
*Replacement=Entry;
return *Replacement;
}
inline std::int64_t ScaledQ16(std::int64_t value, std::int64_t scaleQ16)
@@ -641,6 +673,7 @@ namespace floating_island_detail
}
inline double IslandDensity(const FloatingIslandRecipeV1& recipe,
const FloatingIslandPlanV1& plan,
const FloatingIslandAnchorV1& island,
double x, double y, double z)
{
@@ -665,7 +698,7 @@ namespace floating_island_detail
std::max({z - top, radial - envelope, bottomZ - z}), x, y, z);
}
const FIslandColumnCacheEntry& Column=IslandColumnCache(recipe,island,x,y);
const FIslandColumnCacheEntry& Column=IslandColumnCache(recipe,plan,island,x,y);
const double dx=Column.DeltaX;
const double dy=Column.DeltaY;
const double radial=Column.Radial;
@@ -733,7 +766,7 @@ namespace floating_island_detail
double result = static_cast<double>(MaxDensity);
for (const auto& island : plan.islands)
{
double group = IslandDensity(recipe, island,
double group = IslandDensity(recipe, plan, island,
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
for (const auto& edge : plan.clusterEdges)
{
@@ -769,7 +802,7 @@ namespace floating_island_detail
const auto evaluateLocalGroup = [&](std::size_t islandIndex)
{
const auto& island = plan.islands[islandIndex];
double group = IslandDensity(recipe, island,
double group = IslandDensity(recipe, plan, island,
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
for (const std::uint32_t edgeIndex : featureIndices)
{
@@ -795,7 +828,7 @@ namespace floating_island_detail
const auto evaluateDistantGroup = [&](std::size_t islandIndex)
{
const auto& island = plan.islands[islandIndex];
double group = IslandDensity(recipe, island,
double group = IslandDensity(recipe, plan, island,
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
for (const auto& edge : plan.clusterEdges)
{
+46
View File
@@ -943,6 +943,10 @@ namespace VoxelForgeCore
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
WorldConfiguration configuration;
std::unique_ptr<FStandaloneShapeWorld> shape;
std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache>
sharedHeightfieldColumns;
std::unique_ptr<floating_island_detail::FSharedIslandColumnCache>
sharedIslandColumns;
ShapeBuildRequest buildRequest;
mutable std::shared_mutex tileRegionMutex;
std::unordered_map<TileKey,std::shared_ptr<FStandaloneShapeWorld>,TileKeyHash> tileRegions;
@@ -953,6 +957,23 @@ namespace VoxelForgeCore
std::uint64_t lastSequence=0;
};
struct FScopedSharedColumnCaches
{
using FHeightfieldCache=world_assembly_detail::FSharedHeightfieldColumnCache;
using FIslandCache=floating_island_detail::FSharedIslandColumnCache;
FHeightfieldCache* PreviousHeightfield=nullptr;
FIslandCache* PreviousIslands=nullptr;
FScopedSharedColumnCaches(FHeightfieldCache* Heightfield,FIslandCache* Islands)
: PreviousHeightfield(FHeightfieldCache::setThreadActive(Heightfield)),
PreviousIslands(FIslandCache::setThreadActive(Islands)) {}
~FScopedSharedColumnCaches()
{
FHeightfieldCache::setThreadActive(PreviousHeightfield);
FIslandCache::setThreadActive(PreviousIslands);
}
};
World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique<Impl>())
{
if(snapshot.data_)
@@ -960,6 +981,15 @@ namespace VoxelForgeCore
impl_->owner=snapshot.data_->owner;
impl_->worldAssembly=snapshot.data_->worldAssembly;
impl_->configuration=snapshot.data_->configuration;
if(impl_->worldAssembly)
{
if(!impl_->worldAssembly->plan.strates.empty())
impl_->sharedHeightfieldColumns=
std::make_unique<world_assembly_detail::FSharedHeightfieldColumnCache>();
if(!impl_->worldAssembly->floatingIslands.empty())
impl_->sharedIslandColumns=
std::make_unique<floating_island_detail::FSharedIslandColumnCache>();
}
}
}
World::~World()=default;
@@ -1337,6 +1367,9 @@ namespace VoxelForgeCore
options.legacyDirectMappedColumnCache||key.lod<=2);
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt())
{ error="world must be compiled before generating tiles";return std::nullopt; }
FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache
?impl_->sharedHeightfieldColumns.get():nullptr,
options.useSharedColumnCache?impl_->sharedIslandColumns.get():nullptr);
const auto& world=*impl_->owner;
std::shared_ptr<FStandaloneShapeWorld> page;
if(impl_->tileRegionCount.load(std::memory_order_acquire)!=0)
@@ -1500,6 +1533,15 @@ namespace VoxelForgeCore
FStandaloneShapeWorld::ResetThreadClassificationStats();
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(
options.captureColumnCoordinates);
if(options.captureColumnCoordinates)
{
constexpr std::int64_t SubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
VoxelForgeCore::Heightfield2D::setThreadColumnCaptureBounds(
static_cast<std::int64_t>(origin.X)*SubvoxelsPerVoxel,
static_cast<std::int64_t>(origin.Y)*SubvoxelsPerVoxel,
static_cast<std::int64_t>(origin.X+extent)*SubvoxelsPerVoxel,
static_cast<std::int64_t>(origin.Y+extent)*SubvoxelsPerVoxel);
}
bool bNativeSurfaceTile=false;
bool bNativeSurfaceBandSafe=false;
double classificationColumnComputeMilliseconds=0.0;
@@ -1544,6 +1586,8 @@ namespace VoxelForgeCore
out.diagnostics.columnCacheHits=Columns.columnHits;
out.diagnostics.columnCacheMisses=Columns.columnMisses;
out.diagnostics.columnCacheConflictMisses=Columns.columnConflictMisses;
out.diagnostics.sharedColumnCacheHits=Columns.sharedColumnHits;
out.diagnostics.columnEvaluations=Columns.columnEvaluations;
out.diagnostics.classificationMilliseconds=Counts.Milliseconds;
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
Counts.Milliseconds-classificationColumnComputeMilliseconds);
@@ -1829,6 +1873,8 @@ namespace VoxelForgeCore
out.diagnostics.columnCacheHits=Columns.columnHits;
out.diagnostics.columnCacheMisses=Columns.columnMisses;
out.diagnostics.columnCacheConflictMisses=Columns.columnConflictMisses;
out.diagnostics.sharedColumnCacheHits=Columns.sharedColumnHits;
out.diagnostics.columnEvaluations=Columns.columnEvaluations;
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
out.diagnostics.columnEvaluationExclusiveMilliseconds=Columns.columnComputeMilliseconds;
out.diagnostics.classificationMilliseconds=Counts.Milliseconds;
+54 -11
View File
@@ -5,6 +5,7 @@
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <algorithm>
#include <atomic>
#include <bit>
#include <charconv>
#include <chrono>
@@ -121,6 +122,9 @@ WorldAssemblyFeatureColumnRangeV1 BuildWorldAssemblyFeatureColumnRangeV1(
namespace world_assembly_detail
{
using FSharedHeightfieldColumnCache =
VoxelForgeCoreShared::TSharedColumnValueCache<HeightfieldPointSample,131072,4>;
// Preserve recipe order without allocating or looking up metadata in the point loop.
// Garder l'ordre de recette sans allocation ni recherche de metadonnees par point.
struct FeatureRange
@@ -1375,6 +1379,9 @@ namespace world_assembly_detail
std::array<std::uint8_t, HeightfieldColumnCacheSetCount>> HeightfieldColumnCacheNextWay;
inline thread_local HeightfieldColumnCacheStats HeightfieldColumnStats{};
inline thread_local bool HeightfieldColumnCacheLegacyDirectMapped=false;
inline thread_local std::array<std::int64_t,4> HeightfieldColumnCaptureBounds{};
inline thread_local bool HeightfieldColumnCaptureBoundsValid=false;
inline std::atomic<std::uint64_t> HeightfieldColumnCaptureSequence{0};
inline std::size_t HeightfieldColumnCacheSet(const WorldAssemblyRecipeV1& recipe,
const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y) noexcept
@@ -1587,22 +1594,46 @@ namespace world_assembly_detail
replacement=&cache[first+replacementWay];
}
auto& entry=*replacement;
const auto start=std::chrono::steady_clock::now();
HeightfieldPointSample column=EvaluateColumn(recipe,plan,x,y);
HeightfieldColumnStats.columnComputeMilliseconds+=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-start).count();
++HeightfieldColumnStats.columnMisses;
if(HeightfieldColumnStats.captureCoordinates&&HeightfieldColumnStats.coordinateCaptureComplete)
const auto compute=[&]()
{
try
const std::uint64_t sequence=HeightfieldColumnStats.captureCoordinates
?HeightfieldColumnCaptureSequence.fetch_add(1,std::memory_order_relaxed):0;
const auto start=std::chrono::steady_clock::now();
HeightfieldPointSample result=EvaluateColumn(recipe,plan,x,y);
HeightfieldColumnStats.columnComputeMilliseconds+=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-start).count();
++HeightfieldColumnStats.columnEvaluations;
if(HeightfieldColumnStats.captureCoordinates
&&HeightfieldColumnStats.coordinateCaptureComplete)
{
HeightfieldColumnStats.evaluatedCoordinates.push_back({x,y});
}
catch(...)
{
HeightfieldColumnStats.coordinateCaptureComplete=false;
try
{
const bool outside=HeightfieldColumnCaptureBoundsValid
&&(x<HeightfieldColumnCaptureBounds[0]||x>HeightfieldColumnCaptureBounds[2]
||y<HeightfieldColumnCaptureBounds[1]||y>HeightfieldColumnCaptureBounds[3]);
HeightfieldColumnStats.evaluatedCoordinates.push_back({x,y,sequence,outside});
}
catch(...)
{
HeightfieldColumnStats.coordinateCaptureComplete=false;
}
}
return result;
};
HeightfieldPointSample column{};
if(auto* shared=FSharedHeightfieldColumnCache::threadActive())
{
const VoxelForgeCoreShared::FColumnCacheKey key{
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe)),
recipe.contentHash,
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
plan.contentHash,x,y};
bool computed=false;
column=shared->getOrCompute(key,compute,computed);
if(!computed)++HeightfieldColumnStats.sharedColumnHits;
}
else column=compute();
entry={&recipe,&plan,recipe.contentHash,plan.contentHash,x,y,column};
return column;
}
@@ -2087,10 +2118,22 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl
Stats.columnHits=0;
Stats.columnMisses=0;
Stats.columnConflictMisses=0;
Stats.sharedColumnHits=0;
Stats.columnEvaluations=0;
Stats.columnComputeMilliseconds=0.0;
Stats.captureCoordinates=captureCoordinates;
Stats.coordinateCaptureComplete=true;
Stats.evaluatedCoordinates.clear();
world_assembly_detail::HeightfieldColumnCaptureBoundsValid=false;
}
void Heightfield2D::setThreadColumnCaptureBounds(std::int64_t minimumXSubvoxels,
std::int64_t minimumYSubvoxels,std::int64_t maximumXSubvoxels,
std::int64_t maximumYSubvoxels) noexcept
{
world_assembly_detail::HeightfieldColumnCaptureBounds={minimumXSubvoxels,
minimumYSubvoxels,maximumXSubvoxels,maximumYSubvoxels};
world_assembly_detail::HeightfieldColumnCaptureBoundsValid=true;
}
void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept