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:
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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user