diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 79ec2ad..8c329b7 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -206,7 +206,7 @@ namespace FAutoConsoleVariableRef CVarVoxelForgeStreamingCoarseFirst( TEXT("voxel.StreamingCoarseFirst"), GVoxelForgeStreamingCoarseFirst, - TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order.")); + TEXT("Prioritize walkable tiles, then finish LOD0-2 before far clipmap work; controls far-level order.")); int32 GVoxelForgeStreamingDynamicNearReserve = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve( @@ -11631,7 +11631,8 @@ void AVoxelWorld::EnqueueStreamingResult(FChunkResult&& Result) { EditedProcessQueue.Enqueue(MoveTemp(Result)); } - else if (Result.WorkPriority == EVoxelTileWorkPriority::LOD0) + else if (Result.WorkPriority == EVoxelTileWorkPriority::LOD0 + || Result.Tile.Level <= 2) { NearProcessQueue.Enqueue(MoveTemp(Result)); } @@ -11947,8 +11948,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const int32 NearTaskReserve = EffectiveNearTaskReserve; const int32 NearAdmissionCap = bPawnMoving ? MaxTasks : TotalAdmissionCap; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth; - const int32 FirstCoarseLevel = FMath::Max( - 2, Settings ? Settings->FullResClipLevels : 2); + constexpr int32 FirstFarLevel = 3; const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0 && bWalkableRingReadyForReserve; int32 SchedulerSubmittedByLevel[9] = {}; @@ -12425,43 +12425,65 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } }; - // Coarse-first gives the far shell the first non-critical admissions. Keep the current - // collision-ready 3x3 ring above it, then spend the remaining slots on full-resolution - // refinement. The opt-out reproduces the S4 LOD0-to-LOD8 submission order for a control. - // Le shell lointain passe apres l'anneau de collision, puis le raffinement fin. L'option - // inverse restaure l'ordre LOD0-vers-LOD8 de S4 comme controle negatif. - // Keep the walkable ring on the legacy near-first path; only spend its reserved task slots - // on coarse work after all nine collision gates are ready. - // Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions. - if (bUseCoarseFirstOrder) + const auto AreCriticalTilesReady = [this]() { - for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level) + for (const FVoxelTileKey& Tile : CriticalDesiredTiles) { - SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, - CoarseTaskCap, true); + if (!LoadedTiles.Contains(Tile) && !CollisionNotRequiredTiles.Contains(Tile)) + { + return false; + } } - for (int32 Level = 0; Level < FirstCoarseLevel && Level <= MaxDesiredLevel; ++Level) + return true; + }; + const auto AreNearLODTilesReady = [this]() + { + for (const FVoxelTileKey& Tile : DesiredSorted) + { + if (Tile.Level <= 2 && !LoadedTiles.Contains(Tile) + && !CollisionNotRequiredTiles.Contains(Tile)) + { + return false; + } + } + return true; + }; + + // The support and collision-critical set drains first. Then LOD0, LOD1, and LOD2 + // use the near lane. Far tiles are not admitted until every requested near tile is ready. + if (AreCriticalTilesReady() && bWalkableRingReadyForReserve) + { + for (int32 Level = 0; Level <= 2 && Level <= MaxDesiredLevel; ++Level) { const EVoxelTileWorkPriority Priority = Level == 0 ? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon; - const int32 LevelAdmissionCap = Priority == EVoxelTileWorkPriority::Horizon - ? CoarseTaskCap : NearAdmissionCap; - SubmitLevel(Level, Priority, LevelAdmissionCap, true); + SubmitLevel(Level, Priority, NearAdmissionCap, true); } } - else + + if (AreNearLODTilesReady()) { - SubmitLevel(0, EVoxelTileWorkPriority::LOD0, NearAdmissionCap, false); - for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level) + if (bUseCoarseFirstOrder) { - SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, - CoarseTaskCap, false); - if (GetGenerationAdmissionCount() >= CoarseTaskCap) + for (int32 Level = MaxDesiredLevel; Level >= FirstFarLevel; --Level) { - if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; } - SchedulerOrderStopLevel = Level; - SchedulerOrderStopCap = CoarseTaskCap; - break; + SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, + CoarseTaskCap, true); + } + } + else + { + for (int32 Level = FirstFarLevel; Level <= MaxDesiredLevel; ++Level) + { + SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, + CoarseTaskCap, false); + if (GetGenerationAdmissionCount() >= CoarseTaskCap) + { + if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; } + SchedulerOrderStopLevel = Level; + SchedulerOrderStopCap = CoarseTaskCap; + break; + } } } } @@ -12621,9 +12643,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw > ResultBacklogCapStopsAtUpdateEntry; for (int32 Level = 0; Level < 9; ++Level) { - AdmissionCapByLevel[Level] = bUseCoarseFirstOrder - ? (Level >= FirstCoarseLevel || Level > 0 ? CoarseTaskCap : NearAdmissionCap) - : (Level == 0 ? NearAdmissionCap : CoarseTaskCap); + AdmissionCapByLevel[Level] = Level <= 2 ? NearAdmissionCap : CoarseTaskCap; if (DesiredMissingByLevel[Level] <= 0) continue; if (bAllChunksLoaded) @@ -12711,7 +12731,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw "\"max_tasks\":%d,\"ready_queue_depth\":%d," "\"total_admission_cap\":%d,\"near_admission_cap\":%d," "\"near_reserve\":%d," - "\"coarse_cap\":%d,\"first_coarse_level\":%d," + "\"coarse_cap\":%d,\"first_far_level\":%d," "\"submission_stop_level\":%d,\"submission_stop_cap\":%d," "\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d," "\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d," @@ -12734,7 +12754,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, ReadyQueueDepth, - TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, CoarseTaskCap, FirstCoarseLevel, + TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, CoarseTaskCap, FirstFarLevel, SchedulerOrderStopLevel, SchedulerOrderStopCap, PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard, ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate, @@ -12789,15 +12809,19 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor const int32 ReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; - const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; + const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon + || Tile.Level <= 2; const int32 NearReserve = EffectiveNearTaskReserve >= 0 ? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1)) : GetNearTaskReserve(MaxTasks); const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon + && Tile.Level > 2 ? CoarseTaskCap - : (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap); + : ((WorkPriority == EVoxelTileWorkPriority::LOD0 + || (WorkPriority == EVoxelTileWorkPriority::Horizon && Tile.Level <= 2)) + ? NearAdmissionCap : TotalAdmissionCap); if (GetGenerationAdmissionCount() >= AdmissionCap) { if (GVoxelForgeProfileTileGeneration != 0 @@ -13132,15 +13156,19 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority const int32 ReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; - const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; + const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon + || Tile.Level <= 2; const int32 NearReserve = EffectiveNearTaskReserve >= 0 ? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1)) : GetNearTaskReserve(MaxTasks); const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon + && Tile.Level > 2 ? CoarseTaskCap - : (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap); + : ((WorkPriority == EVoxelTileWorkPriority::LOD0 + || (WorkPriority == EVoxelTileWorkPriority::Horizon && Tile.Level <= 2)) + ? NearAdmissionCap : TotalAdmissionCap); if (GetGenerationAdmissionCount() >= AdmissionCap) return; PendingTiles.Add(Tile); diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index e26c3ba..903d23b 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -201,6 +201,37 @@ public: passageColumns_=passageRoutes_; passageMouthSupports_=passageRoutes_; passageMouthSupportSegments_.resize(passageRoutes_.size()); + if(shape.ResolvedLayout&&!shape.ResolvedLayout->passages.empty()) + { + constexpr double Q=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double qPerMetre=Q*1000000.0/ + std::max(1, + shape.ResolvedLayout->coreVoxelSizeMicrometres); + const double horizontalReach=15.0*qPerMetre; + const double verticalReach=8.0*qPerMetre; + const bool failOpen=minimumSubvoxels_[0]>maximumSubvoxels_[0] + ||minimumSubvoxels_[1]>maximumSubvoxels_[1] + ||minimumSubvoxels_[2]>maximumSubvoxels_[2]; + for(std::size_t passageIndex=0; + passageIndexpassages.size();++passageIndex) + { + if(failOpen) + { + upperMouthSupportPassages_.push_back(passageIndex); + continue; + } + const auto& mouth=shape.ResolvedLayout->passages[ + passageIndex].upperMouthSubvoxels; + if(static_cast(mouth[0])+horizontalReach>=minimumSubvoxels_[0] + &&static_cast(mouth[0])-horizontalReach<=maximumSubvoxels_[0] + &&static_cast(mouth[1])+horizontalReach>=minimumSubvoxels_[1] + &&static_cast(mouth[1])-horizontalReach<=maximumSubvoxels_[1] + &&static_cast(mouth[2])+verticalReach>=minimumSubvoxels_[2] + &&static_cast(mouth[2])-verticalReach<=maximumSubvoxels_[2]) + upperMouthSupportPassages_.push_back(passageIndex); + } + } for(const auto& crossing:shape.PassageVoidCrossings) if(crossing.needsColumn &&crossing.bottomZSubvoxels<=maximumSubvoxels_[2] @@ -257,6 +288,10 @@ public: [](std::uint8_t value){return value!=0;});} bool hasRouteFeatureOverlap() const{return routeFeatureOverlap_;} bool hasColumnZOverlap() const{return columnZOverlap_;} + bool mayContainUpperMouthSupport() const + {return !upperMouthSupportPassages_.empty();} + const std::vector& upperMouthSupportPassages() const + {return upperMouthSupportPassages_;} bool mayContainResolvedBoundaryBand() const{return mayContainResolvedBoundaryBand_;} bool mayContainTransitionPassageFade() const{return mayContainTransitionPassageFade_;} bool mayContainResolvedBoundaryBandAtZ(double zMicrometres) const @@ -282,6 +317,7 @@ public: std::vector cavernFeatureIndices_; std::vector passageRoutes_,passageColumns_,passageMouthSupports_; std::vector> passageMouthSupportSegments_; + std::vector upperMouthSupportPassages_; std::size_t routeSegmentCandidateCount_=0; bool routeFeatureOverlap_=false,columnZOverlap_=false; bool mayContainResolvedBoundaryBand_=true; @@ -1004,6 +1040,11 @@ public: auto* passageProfile=passageTileScopeMatches ?passageTileQueries->profileStats():nullptr; if(passageProfile)++passageProfile->passageComposerQueries; + if(passageTileScopeMatches&&density>=0.0f + &&!passageTileQueries->hasRouteFeatureOverlap() + &&!passageTileQueries->hasColumnZOverlap() + &&!passageTileQueries->hasAnyMouthSupport() + &&!passageTileQueries->mayContainUpperMouthSupport())return density; bool queryNearUpperMouthSupport=false; const auto checkUpperMouth=[&](const auto& passage) { @@ -1015,10 +1056,22 @@ public: if(dx*dx+dy*dy<=reach*reach&&std::abs(dz)<=8.0*qPerMetre) queryNearUpperMouthSupport=true; }; - for(const auto& passage:ResolvedLayout->passages) + if(passageTileScopeMatches) { - checkUpperMouth(passage); - if(queryNearUpperMouthSupport)break; + for(const std::size_t passageIndex: + passageTileQueries->upperMouthSupportPassages()) + { + checkUpperMouth(ResolvedLayout->passages[passageIndex]); + if(queryNearUpperMouthSupport)break; + } + } + else + { + for(const auto& passage:ResolvedLayout->passages) + { + checkUpperMouth(passage); + if(queryNearUpperMouthSupport)break; + } } if(passageTileScopeMatches&&density>=0.0f &&!passageTileQueries->hasRouteFeatureOverlap() @@ -3862,6 +3915,7 @@ private: { std::array minimum{},maximum{}; double maximumRadius=0.0; + std::size_t minimumSegment=0,maximumSegment=0; std::vector segments; int left=-1,right=-1; }; @@ -4676,6 +4730,8 @@ private: node.maximumRadius=std::max({node.maximumRadius, static_cast(a.radiusSubvoxels),static_cast(b.radiusSubvoxels)}); } + node.minimumSegment=*std::min_element(segments.begin(),segments.end()); + node.maximumSegment=*std::max_element(segments.begin(),segments.end()); node.segments=segments; std::sort(node.segments.begin(),node.segments.end()); const int nodeIndex=static_cast(PassageNearestNodes[passageIndex].size()); @@ -5284,7 +5340,11 @@ private: if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return; const auto& nodes=PassageNearestNodes[passageIndex]; const auto& node=nodes[static_cast(nodeIndex)]; - const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); + if(node.maximumSegment=end)return; + const bool allSegmentsInRange=first<=node.minimumSegment + &&node.maximumSegment=end)return; const double lowerBound=PassageNearestNodeLowerBound(node,qx,qy,qz); if(lowerBound>bestDistance)return; @@ -5325,7 +5385,11 @@ private: if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false; const auto& nodes=PassageNearestNodes[passageIndex]; const auto& node=nodes[static_cast(nodeIndex)]; - const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); + if(node.maximumSegment=end)return false; + const bool allSegmentsInRange=first<=node.minimumSegment + &&node.maximumSegment=end)return false; if(PassageNearestNodeLowerBound(node,qx,qy,qz)>signedDistanceLimit)return false; if(node.left<0)return true; @@ -5448,7 +5512,11 @@ private: if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false; const auto& nodes=PassageNearestNodes[passageIndex]; const auto& node=nodes[static_cast(nodeIndex)]; - auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); + if(node.maximumSegment=end)return false; + const bool allSegmentsInRange=first<=node.minimumSegment + &&node.maximumSegment=end)return false; const double xyReach=0.39*qPerMetre; if(qx+xyReachnode.maximum[0] diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index d4ab2c4..ad27766 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1891,6 +1891,17 @@ namespace VoxelForgeCore std::vector output(tiles.begin(),tiles.end()); std::sort(output.begin(),output.end(),[&](const TileKey& a,const TileKey& b) { + const auto tier=[&](const TileKey& key) + { + if(key.lod==0&&std::abs(key.coordinate.x-centerChunk.x)<=1 + &&std::abs(key.coordinate.y-centerChunk.y)<=1 + &&std::abs(key.coordinate.z-centerChunk.z)<=1)return 0; + return key.lod<=2?1:2; + }; + const int tierA=tier(a),tierB=tier(b); + if(tierA!=tierB)return tierAb.lod; const double da=TileDistanceSquared(a,centerChunk),db=TileDistanceSquared(b,centerChunk); if(da!=db)return da #include #include #include @@ -244,11 +245,70 @@ VF_FIELD_INLINE float VF_FieldValueNoise(float x, float y, float z, return VF_FieldLerp(VF_FieldLerp(z00, z10, ty), VF_FieldLerp(z01, z11, ty), tz); } +#if !defined(VF_FIELD_HLSL) +struct VF_FieldValueNoiseCacheEntry +{ + VF_FieldI32 x=0,y=0,z=0,wavelength=0; + VF_FieldU32 seed=0; + bool valid=false; + std::array corners{}; +}; + +// Reuse the eight lattice values while adjacent voxel samples remain in the same +// value-noise cell. Only the integer hash lookups are shared; interpolation keeps +// the same inputs and operation order as VF_FieldValueNoise. +VF_FIELD_INLINE float VF_FieldCachedValueNoise(float x,float y,float z, + VF_FieldI32 wavelength,VF_FieldU32 seed) +{ + static thread_local std::array cache{}; + const float inv=1.0f/VF_FIELD_CAST_F32(wavelength); + const float fx=x*inv,fy=y*inv,fz=z*inv; + const VF_FieldI32 x0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fx)); + const VF_FieldI32 y0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fy)); + const VF_FieldI32 z0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fz)); + const VF_FieldU32 cellHash=VF_FieldMix(seed + ^VF_FIELD_CAST_U32(x0)*0x68E31DA4u + ^VF_FIELD_CAST_U32(y0)*0x1B56C4E9u + ^VF_FIELD_CAST_U32(z0)*0x9E3779B9u + ^VF_FIELD_CAST_U32(wavelength)*0x85EBCA77u); + auto& entry=cache[cellHash&(cache.size()-1)]; + if(!entry.valid||entry.x!=x0||entry.y!=y0||entry.z!=z0 + ||entry.wavelength!=wavelength||entry.seed!=seed) + { + entry.x=x0;entry.y=y0;entry.z=z0;entry.wavelength=wavelength; + entry.seed=seed;entry.valid=true; + entry.corners={ + VF_FieldNoiseValue(x0,y0,z0,seed), + VF_FieldNoiseValue(x0+1,y0,z0,seed), + VF_FieldNoiseValue(x0,y0+1,z0,seed), + VF_FieldNoiseValue(x0+1,y0+1,z0,seed), + VF_FieldNoiseValue(x0,y0,z0+1,seed), + VF_FieldNoiseValue(x0+1,y0,z0+1,seed), + VF_FieldNoiseValue(x0,y0+1,z0+1,seed), + VF_FieldNoiseValue(x0+1,y0+1,z0+1,seed)}; + } + const float tx=VF_FieldFade(fx-VF_FIELD_CAST_F32(x0)); + const float ty=VF_FieldFade(fy-VF_FIELD_CAST_F32(y0)); + const float tz=VF_FieldFade(fz-VF_FIELD_CAST_F32(z0)); + const float z00=VF_FieldLerp(entry.corners[0],entry.corners[1],tx); + const float z10=VF_FieldLerp(entry.corners[2],entry.corners[3],tx); + const float z01=VF_FieldLerp(entry.corners[4],entry.corners[5],tx); + const float z11=VF_FieldLerp(entry.corners[6],entry.corners[7],tx); + return VF_FieldLerp(VF_FieldLerp(z00,z10,ty),VF_FieldLerp(z01,z11,ty),tz); +} +#endif + VF_FIELD_INLINE float VF_FieldSurfaceRelief(float x, float y, float z, VF_FieldU32 seed) { +#if defined(VF_FIELD_HLSL) const float macro = VF_FieldValueNoise(x, y, z, 32, seed ^ 0xB5297A4Du); const float mid = VF_FieldValueNoise(x, y, z, 16, seed ^ 0x63D83595u); const float fine = VF_FieldValueNoise(x, y, z, 4, seed ^ 0xA511E9B3u); +#else + const float macro=VF_FieldCachedValueNoise(x,y,z,32,seed^0xB5297A4Du); + const float mid=VF_FieldCachedValueNoise(x,y,z,16,seed^0x63D83595u); + const float fine=VF_FieldCachedValueNoise(x,y,z,4,seed^0xA511E9B3u); +#endif return 1.20f * macro + (0.80f + 0.25f * macro) * (0.40f * mid + 0.60f * fine); } @@ -335,12 +395,21 @@ VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_Field { // Use long, non-commensurate rotated scales. The shared passage wall keeps broad, // uneven rock relief without a short wavelength that can read as repeated rings. +#if defined(VF_FIELD_HLSL) const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43, seed^0xB5297A4Du); const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29, seed^0x63D83595u); const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19, seed^0xA511E9B3u); +#else + const float broad=VF_FieldCachedValueNoise(x+17.0f,y-23.0f,z+11.0f,43, + seed^0xB5297A4Du); + const float mid=VF_FieldCachedValueNoise(y-7.0f,z+19.0f,x-31.0f,29, + seed^0x63D83595u); + const float fine=VF_FieldCachedValueNoise(z+5.0f,x+29.0f,y-13.0f,19, + seed^0xA511E9B3u); +#endif return 0.50f*broad+0.35f*mid+0.15f*fine; } diff --git a/Standalone/Research/WorldAssemblerM3/Linux78/AnalyzeL78Profiles.py b/Standalone/Research/WorldAssemblerM3/Linux78/AnalyzeL78Profiles.py new file mode 100644 index 0000000..50c7ba0 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux78/AnalyzeL78Profiles.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Summarize sampler closure, phase cost, and route census from L78 timing logs.""" +from __future__ import annotations + +import csv +import json +import re +import statistics +from collections import defaultdict +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +TIMING = ROOT / "Saved/Linux/l78/timing" +OUT = ROOT / "Saved/Linux/l78" +SAMPLE = re.compile( + r"^\[L77Sample\] pc=\S+ lod=(\d+) stage=(\S+) route=(\S+) " + r"phase=(\S+) halo=(\d+) weight=(\d+)$", re.M) +SAMPLER = re.compile( + r"^\[L77Sampler\] action=stop .*?samples=(\d+) .*?lost=(\d+) " + r"timer_overruns=(\d+).*?profiled_thread_cpu_s=([0-9.]+).*?" + r"handler_cpu_ms=([0-9.]+) handler_share_pct=([0-9.]+) " + r"observed_us_per_tick=([0-9.]+)", re.M) +DISPATCH = re.compile( + r"^\[WorldAssemblerL77Dispatch\] lod=(\d+) stage=(\S+) route=(\S+) " + r"calls=(\d+) points=(\d+)$", re.M) +LOD_COST = re.compile( + r"^\[WorldAssemblerLODCostDetail\] lod=(\d+) tiles=(\d+).*?" + r"mc_grid_ms=([0-9.]+).*?worker_ms_per_tile=([0-9.]+)", re.M) + + +def analyze(row: dict) -> tuple[dict, list[dict], list[dict]]: + path = ROOT / row["log"] + content = path.read_text(encoding="utf-8", errors="replace") + sampler_match = SAMPLER.search(content) + if sampler_match is None: + raise SystemExit(f"sampler summary missing from {path}") + samples = [ + (int(lod), stage, route, phase, int(halo), int(weight)) + for lod, stage, route, phase, halo, weight in SAMPLE.findall(content) + ] + sample_count, lost, overruns = map(int, sampler_match.group(1, 2, 3)) + profiled_cpu_s, handler_cpu_ms, handler_share_pct, period_us = map( + float, sampler_match.group(4, 5, 6, 7)) + period_s = period_us / 1_000_000.0 + lod_costs = { + int(lod): (int(tiles), float(grid_ms) / 1000.0, + float(worker_ms) * int(tiles) / 1000.0) + for lod, tiles, grid_ms, worker_ms in LOD_COST.findall(content) + } + dispatch = { + (int(lod), stage, route): (int(calls), int(points)) + for lod, stage, route, calls, points in DISPATCH.findall(content) + } + + scope_metrics = [] + for scope, predicate in (("LOD0-2", lambda lod: 0 <= lod <= 2), + ("whole_view", lambda lod: 0 <= lod <= 8)): + selected = [item for item in samples if predicate(item[0])] + sampled_cpu_s = sum(item[5] for item in selected) * period_s + sampled_grid_s = sum(item[5] for item in selected if item[1] == "grid_fill") * period_s + sampled_passage_s = sum(item[5] for item in selected + if item[3] == "passages") * period_s + sampled_support_s = sum(item[5] for item in selected + if item[3] == "support") * period_s + direct_grid_s = sum(value[1] for lod, value in lod_costs.items() if predicate(lod)) + direct_worker_s = sum(value[2] for lod, value in lod_costs.items() if predicate(lod)) + scope_metrics.append({ + "strate": row["strate"], "mode": row["mode"], "pair": row["pair"], + "scope": scope, "sample_events": len(selected), + "sampled_cpu_s": sampled_cpu_s, + "sampled_grid_fill_s": sampled_grid_s, + "direct_grid_fill_s": direct_grid_s, + "grid_closure_pct": 100.0 * sampled_grid_s / direct_grid_s if direct_grid_s else 0.0, + "sampled_passage_s": sampled_passage_s, + "sampled_passage_pct": 100.0 * sampled_passage_s / sampled_cpu_s + if sampled_cpu_s else 0.0, + "sampled_support_s": sampled_support_s, + "sampled_support_pct": 100.0 * sampled_support_s / sampled_cpu_s + if sampled_cpu_s else 0.0, + "direct_worker_cpu_s": direct_worker_s, + }) + + route_metrics = [] + scopes = (("LOD0-2", lambda lod: 0 <= lod <= 2), + ("whole_view", lambda lod: 0 <= lod <= 8)) + for scope, predicate in scopes: + route_keys = { + (item[1], item[2]) for item in samples if predicate(item[0]) + } | { + (stage, route) for (lod, stage, route) in dispatch if predicate(lod) + } + sampled_total = sum(item[5] for item in samples if predicate(item[0])) * period_s + for stage, route in sorted(route_keys): + weighted = sum(item[5] for item in samples + if predicate(item[0]) and item[1] == stage and item[2] == route) + calls = sum(value[0] for (lod, current_stage, current_route), value in dispatch.items() + if predicate(lod) and current_stage == stage and current_route == route) + points = sum(value[1] for (lod, current_stage, current_route), value in dispatch.items() + if predicate(lod) and current_stage == stage and current_route == route) + sampled_s = weighted * period_s + route_metrics.append({ + "strate": row["strate"], "mode": row["mode"], "pair": row["pair"], + "scope": scope, "caller_stage": stage, "route": route, + "calls": calls, "points": points, "sampled_cpu_s": sampled_s, + "share_scope_sampled_cpu_pct": 100.0 * sampled_s / sampled_total + if sampled_total else 0.0, + }) + + summary = { + "strate": row["strate"], "mode": row["mode"], "pair": row["pair"], + "log": row["log"], "sampler_events": sample_count, + "parsed_sampler_events": len(samples), "lost": lost, + "timer_overruns": overruns, "profiled_thread_cpu_s": profiled_cpu_s, + "handler_cpu_ms": handler_cpu_ms, "handler_share_pct": handler_share_pct, + "observed_us_per_tick": period_us, + } + return summary, scope_metrics, route_metrics + + +def write_csv(path: Path, rows: list[dict]) -> None: + if not rows: + return + with path.open("w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter(stream, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + + +def main() -> int: + timing = json.loads((TIMING / "summary.json").read_text(encoding="utf-8")) + sampler_rows, scope_rows, route_rows = [], [], [] + for row in timing["runs"]: + sampler, scopes, routes = analyze(row) + sampler_rows.append(sampler) + scope_rows.extend(scopes) + route_rows.extend(routes) + + write_csv(OUT / "sampler-census.csv", sampler_rows) + write_csv(OUT / "closure-census.csv", scope_rows) + write_csv(OUT / "route-census.csv", route_rows) + + averaged = [] + for strate in range(4): + for mode in ("before", "after"): + samples = [row for row in scope_rows + if row["strate"] == strate and row["mode"] == mode] + for scope in ("LOD0-2", "whole_view"): + values = [row for row in samples if row["scope"] == scope] + averaged.append({ + "strate": strate, "mode": mode, "scope": scope, + "runs": len(values), + "sampled_cpu_s_mean": statistics.mean(row["sampled_cpu_s"] for row in values), + "sampled_grid_fill_s_mean": statistics.mean(row["sampled_grid_fill_s"] for row in values), + "direct_grid_fill_s_mean": statistics.mean(row["direct_grid_fill_s"] for row in values), + "grid_closure_pct_mean": statistics.mean(row["grid_closure_pct"] for row in values), + "passage_s_mean": statistics.mean(row["sampled_passage_s"] for row in values), + "passage_sample_pct_mean": statistics.mean( + row["sampled_passage_pct"] for row in values), + "support_s_mean": statistics.mean(row["sampled_support_s"] for row in values), + "support_sample_pct_mean": statistics.mean( + row["sampled_support_pct"] for row in values), + "direct_worker_cpu_s_mean": statistics.mean(row["direct_worker_cpu_s"] for row in values), + }) + (OUT / "profile-summary.json").write_text( + json.dumps({"sampler_runs": sampler_rows, "scope_summary": averaged}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux78/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux78/REPORT.md new file mode 100644 index 0000000..e9de5ac --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux78/REPORT.md @@ -0,0 +1,46 @@ +# Linux78 report + +## Passage cost and world identity + +The expensive path is `ApplyResolvedStratePassageCarves`: negative density samples enter the passage loop and query the segment index even when their tile has little passage overlap. The route relief calls `VF_FieldPassageWallRelief`, three 3D value-noise octaves at 43/29/19 voxel wavelengths; mouth relief uses the three-octave surface relief. A representative W8 tunnel LOD0 composer census recorded 6.0M composer queries, 18.1M passage iterations, 45.1M segment visits, and 40.8M relief calls. + +The retained exact changes are a conservative tile-level upper-mouth candidate list and early return, segment-index min/max rejection before `lower_bound`, and a 64-entry thread-local cache of value-noise lattice corners. The cache preserves the existing interpolation operations and values. The passage share remains high: FI/TN near-LOD passage phase averages 63.0%/63.7% after the change, versus 64.6%/65.9% before; support is 13.8%/13.5% versus 14.1%/13.2%. + +The world is exact. The L75 full-grid owner hash and all 24 Clang/GCC × W1/W8/W12 × four-strate density, mesh, payload, and topology comparisons match. No version bump or new captures were needed. Fast-tier passage audits passed. + +## Readiness, W8, three interleaved pairs, median seconds + +| Strate | LOD0–2 before | LOD0–2 after | Whole view before | Whole view after | +|---|---:|---:|---:|---:| +| SurfaceWorld | 4.36 | 1.76 | 4.36 | 3.43 | +| FloatingIslands | 6.89 | 5.26 | 6.89 | 6.93 | +| TunnelNetwork | 4.88 | 3.22 | 4.88 | 4.55 | +| VerticalShafts | 2.71 | 1.56 | 2.71 | 2.84 | + +The headless and Unreal schedulers now submit the collision-critical walkable ring first, then LOD0, LOD1, and LOD2. Far LOD work waits until the requested near LODs are ready and runs at lower priority. The ~1 s near-LOD target is not met for FI/TN; their medians are 5.26 s and 3.22 s. Whole-view times are reported separately above. + +## L77 sampler closure and phase census + +Values are mean sampled grid-fill/direct grid-fill closure; passage and support are their sampled CPU shares. Near is LOD0–2. + +| Strate | Near closure B/A | Whole closure B/A | Passage share near B/A | Support share near B/A | +|---|---:|---:|---:|---:| +| SurfaceWorld | 99.1% / 99.9% | 100.1% / 99.6% | 45.1% / 47.6% | 15.0% / 12.8% | +| FloatingIslands | 99.9% / 99.5% | 99.8% / 99.8% | 64.6% / 63.0% | 14.1% / 13.8% | +| TunnelNetwork | 99.9% / 100.1% | 99.8% / 100.2% | 65.9% / 63.7% | 13.2% / 13.5% | +| VerticalShafts | 100.0% / 100.2% | 99.9% / 100.9% | 45.9% / 42.9% | 5.9% / 6.6% | + +Sampler handler cost stayed below 0.05% of profiled thread CPU. Detailed samples and route counts are in `Saved/Linux/l78/sampler-census.csv`, `closure-census.csv`, and `route-census.csv`. + +## Builds and checks + +- Fast tier: PASS, 366.8 s. +- Hash matrix: PASS, 24/24; the pinned L75 full-grid owner hash also matches. +- Linux VFHostEditor: PASS after timing, `-MaxParallelActions=4`, no address-space limit; UBT reported 70.35 s in its parallel executor. +- No installs, commit, push, or stash. + +## Failed runs and rejected experiment + +- The first fast-tier launch pointed at an output directory without its expected verifier binaries; corrected, then the final fast tier passed. +- L76's broad per-tile passage-loop experiment caused 612 density mismatches and changed mesh, payload, and topology hashes on 24 tiles; L76 rejected it. It is not part of the accepted L76 reference. In L78, per-brick and direct per-sample feature-bound filters also failed the pinned L75 owner hash, so both were removed. The retained path matches the accepted L76 matrix and pinned L75 owner hash. +- The first direct UBT call could not find `dotnet` in PATH; rerunning through the engine `Build.sh` wrapper succeeded. diff --git a/Standalone/Research/WorldAssemblerM3/Linux78/RunL78HashMatrix.py b/Standalone/Research/WorldAssemblerM3/Linux78/RunL78HashMatrix.py new file mode 100644 index 0000000..5f3b198 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux78/RunL78HashMatrix.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Compare L78 exact tile hashes with the retained L76/L77-equivalent matrix.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l78/hashes" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "clang": ROOT / "Saved/Linux/l78/bin/VoxelForgeCoreHeadless-l78-clang", + "gcc": ROOT / "Saved/Linux/l78/bin/VoxelForgeCoreHeadless-l78-gcc", +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +HASH = re.compile( + r"^\[PipelineTileHash\] lod=(\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) " + r"density=(0x[0-9a-fA-F]+) mesh=(0x[0-9a-fA-F]+) " + r"payload=(0x[0-9a-fA-F]+) topology=(0x[0-9a-fA-F]+)$", + re.M, +) + + +def read_hashes(path: Path) -> dict[tuple[int, int, int, int], tuple[str, ...]]: + content = path.read_text(encoding="utf-8", errors="replace") + return { + tuple(int(match.group(i)) for i in range(1, 5)): + tuple(value.lower() for value in match.groups()[4:]) + for match in HASH.finditer(content) + } + + +def run(compiler: str, workers: int, strate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / compiler / f"w{workers}" / f"strate{strate}-after.log" + log.parent.mkdir(parents=True, exist_ok=True) + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[compiler]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", f"--workers={workers}", "--clip-radius=3", + "--max-clip-level=8", "--max-level=8", f"--camera={camera}", + "--stream-hash-tiles", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + hashes = read_hashes(log) + if result.returncode != 0 or not hashes: + raise SystemExit(f"hash generation failed: {log}; exit={result.returncode}") + reference = ROOT / "Saved/Linux/l76/hashes" / compiler / f"w{workers}" / f"strate{strate}-before.log" + expected = read_hashes(reference) + missing, extra = expected.keys() - hashes.keys(), hashes.keys() - expected.keys() + mismatches = {name: 0 for name in ("density", "mesh", "payload", "topology")} + examples = [] + for key in sorted(expected.keys() & hashes.keys()): + for index, name in enumerate(mismatches): + if expected[key][index] != hashes[key][index]: + mismatches[name] += 1 + if len(examples) < 12: + examples.append({"tile": key, "field": name, + "before": expected[key][index], + "after": hashes[key][index]}) + row = { + "compiler": compiler, + "workers": workers, + "strate": strate, + "exit_code": result.returncode, + "wall_s": round(time.monotonic() - started, 3), + "expected_tiles": len(expected), + "candidate_tiles": len(hashes), + "missing_keys": len(missing), + "extra_keys": len(extra), + "mismatches": mismatches, + "examples": examples, + "equal": not missing and not extra and not any(mismatches.values()), + "reference": str(reference.relative_to(ROOT)), + "log": str(log.relative_to(ROOT)), + } + print("[L78HashCompare] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + missing = [str(path) for path in (DATA, *BINARIES.values()) if not path.is_file()] + if missing: + raise SystemExit("missing input: " + ", ".join(missing)) + rows = [] + for compiler in ("clang", "gcc"): + for workers in (1, 8, 12): + for strate in range(4): + row = run(compiler, workers, strate) + rows.append(row) + (OUT / "summary-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + if not row["equal"]: + raise SystemExit(f"hash mismatch; see {row['log']}") + result = {"comparisons": rows, "equal": all(row["equal"] for row in rows)} + (OUT / "summary.json").write_text(json.dumps(result, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux78/RunL78TimingPairs.py b/Standalone/Research/WorldAssemblerM3/Linux78/RunL78TimingPairs.py new file mode 100644 index 0000000..7a3c91f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux78/RunL78TimingPairs.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Run interleaved L77/L78 cold readiness pairs for the four owner strates.""" +from __future__ import annotations + +import json +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l78/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "before": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l77-clang", + "after": ROOT / "Saved/Linux/l78/bin/VoxelForgeCoreHeadless-l78-clang-profile", +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +LOD_READY = re.compile( + r"^\[WorldAssemblerL77LODReady\] lod=(\d+) tiles=(\d+) cold_ready_s=([0-9.]+)$", + re.M, +) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M) +ORDERS = (("before", "after"), ("after", "before"), ("before", "after")) + + +def run(strate: int, pair: int, mode: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"strate{strate}-pair{pair}-{mode}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[mode]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--linux77-profile", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline_match = PIPELINE.search(content) + ready = {int(lod): {"tiles": int(tiles), "seconds": float(seconds)} + for lod, tiles, seconds in LOD_READY.findall(content)} + if result.returncode != 0 or pipeline_match is None or any(lod not in ready for lod in (0, 1, 2)): + raise SystemExit(f"readiness run failed or LOD0-2 data missing: {log}; exit={result.returncode}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + row = { + "strate": strate, + "pair": pair, + "mode": mode, + "exit_code": result.returncode, + "wall_s": round(time.monotonic() - started, 3), + "lod02_ready_s": max(ready[lod]["seconds"] for lod in (0, 1, 2)), + "lod_ready_s": {str(lod): ready[lod]["seconds"] for lod in sorted(ready)}, + "whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(pipeline["whole_tiles"].split("/")[0]), + "sampler": next((line for line in content.splitlines() + if line.startswith("[L77Sampler] action=stop")), None), + "source_stamp": STAMP.search(content).group(1) if STAMP.search(content) else None, + "log": str(log.relative_to(ROOT)), + } + print("[L78TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + missing = [str(path) for path in (DATA, *BINARIES.values()) if not path.is_file()] + if missing: + raise SystemExit("missing input: " + ", ".join(missing)) + rows = [] + for strate in range(4): + for pair, order in enumerate(ORDERS, 1): + paired = {} + for mode in order: + row = run(strate, pair, mode) + rows.append(row) + paired[mode] = row + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + summary_row = { + "strate": strate, + "pair": pair, + "before_lod02_s": paired["before"]["lod02_ready_s"], + "after_lod02_s": paired["after"]["lod02_ready_s"], + "before_whole_s": paired["before"]["whole_view_ready_s"], + "after_whole_s": paired["after"]["whole_view_ready_s"], + } + print("[L78TimingPair] " + json.dumps(summary_row, sort_keys=True), flush=True) + summary = [] + for strate in range(4): + group = [row for row in rows if row["strate"] == strate] + before_near = [row["lod02_ready_s"] for row in group if row["mode"] == "before"] + after_near = [row["lod02_ready_s"] for row in group if row["mode"] == "after"] + before_whole = [row["whole_view_ready_s"] for row in group if row["mode"] == "before"] + after_whole = [row["whole_view_ready_s"] for row in group if row["mode"] == "after"] + summary.append({ + "strate": strate, + "pairs": len(before_near), + "before_lod02_s": before_near, + "after_lod02_s": after_near, + "before_lod02_median_s": statistics.median(before_near), + "after_lod02_median_s": statistics.median(after_near), + "before_whole_s": before_whole, + "after_whole_s": after_whole, + "before_whole_median_s": statistics.median(before_whole), + "after_whole_median_s": statistics.median(after_whole), + }) + result = {"runs": rows, "summary": summary} + (OUT / "summary.json").write_text(json.dumps(result, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index fda4e7f..01b2e03 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -5061,6 +5061,7 @@ namespace StandaloneStreamBenchmark *VoxelForgeCore::Linux77Profiler::RouteCount*2>,9> L77DispatchByLOD{}; std::array L77GridInteriorByLOD{},L77GridHaloByLOD{}; std::array L77LODReadySeconds{}; + std::array LODReadySeconds{}; std::array SkirtTrianglesByLOD{}; std::array TileCountByLOD{}; std::array ClassifiedAirTilesByLOD{},ClassifiedSolidTilesByLOD{}; @@ -5129,6 +5130,9 @@ namespace StandaloneStreamBenchmark const std::size_t LOD=static_cast(Tile.Key.Level); const auto& TileDiagnostics=Tile.CoreDiagnostics; ++TileCountByLOD[LOD]; + if(TileCountByLOD[LOD]==ExpectedTilesByLOD[LOD]) + LODReadySeconds[LOD]=std::chrono::duration( + Tile.CompletedAt-ColdStart).count(); if(bLinux77Profile) { const auto& D=TileDiagnostics; @@ -5551,6 +5555,23 @@ namespace StandaloneStreamBenchmark } } } + double NearLODReadySeconds=0.0; + uint64 NearLODExpected=0,NearLODCompleted=0; + for(std::size_t LOD=0;LOD<=2;++LOD) + { + NearLODExpected+=ExpectedTilesByLOD[LOD]; + NearLODCompleted+=TileCountByLOD[LOD]; + if(ExpectedTilesByLOD[LOD]!=0) + NearLODReadySeconds=std::max(NearLODReadySeconds,LODReadySeconds[LOD]); + } + std::printf("[WorldAssemblerNearLODReady] lods=0-2 tiles=%llu/%llu cold_ready_s=%.6f\n", + static_cast(NearLODCompleted), + static_cast(NearLODExpected),NearLODReadySeconds); + for(std::size_t LOD=0;LOD(TileCountByLOD[LOD]), + LODReadySeconds[LOD]); Streamer.Stop(); Streamer.DumpColdTimelineTrace(); bool bScreenSpaceGatePassing=true; diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 13eeca9..1953fad 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -581,6 +581,7 @@ public: Workers.clear(); std::lock_guard Lock(Mutex); Ready.clear();Outstanding.clear();Desired.clear();Running.clear(); + NearOutstandingJobs=0; } FStandaloneStreamingUpdate UpdateDesired( @@ -680,6 +681,7 @@ public: { if(Desired.contains(Job.Key))return false; Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs; + if(Job.Key.Level<=2&&NearOutstandingJobs>0)--NearOutstandingJobs; ++CancelledByCrossing[Job.RequestedCrossing];return true; }); Queue.erase(QueueEnd,Queue.end()); @@ -774,15 +776,14 @@ private: std::vector Prioritized=NewDesired.Tiles; if(IsCoarseFirstSchedulingEnabled()) { - // The local walkable ring leads, the coarse enclosure follows, then target-detail work. - // L'anneau praticable local passe d'abord, puis l'enveloppe grossiere, puis le detail cible. - const int32 FirstCoarseLevel=FMath::Max(2,World->FullResClipLevels); + // Finish the local walkable ring, then all near LODs, before admitting far work. + // Terminer l'anneau praticable, puis tous les LOD proches avant le champ lointain. auto Tier=[&](const FVoxelTileKey& Key) { if(Key.Level==0&&std::abs(Key.Coord.X-NewDesired.CenterChunk.X)<=1 &&std::abs(Key.Coord.Y-NewDesired.CenterChunk.Y)<=1 &&std::abs(Key.Coord.Z-NewDesired.CenterChunk.Z)<=1)return 0; - return Key.Level>=FirstCoarseLevel?1:2; + return Key.Level<=2?1:2; }; auto IsAhead=[&](const FVoxelTileKey& Key) { @@ -797,7 +798,8 @@ private: { const int32 TierA=Tier(A),TierB=Tier(B); if(TierA!=TierB)return TierAB.Level; + if(TierA==1&&A.Level!=B.Level)return A.LevelB.Level; const bool bAheadA=IsAhead(A),bAheadB=IsAhead(B); if(bAheadA!=bAheadB)return bAheadA; const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A,NewDesired.CenterChunk); @@ -844,6 +846,7 @@ private: RecordTimelineEvent(FStandaloneCpuTileTimelineEvent::EType::Queued,Key,-1, Queue.size(),Running.size(),Job.PriorityRank,Job.QueuedAt,Job.QueuedAt); Outstanding.insert(Key);++Stats.NewJobs; + if(Key.Level<=2)++NearOutstandingJobs; } CurrentDesired=std::move(NewDesired); std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{}); @@ -1025,10 +1028,13 @@ private: size_t DispatchQueueDepth=0,DispatchActiveWorkers=0; { std::unique_lock Lock(Mutex); - Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty()||!Queue.empty();}); + Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty() + ||(!Queue.empty()&&(NearOutstandingJobs==0||Queue.front().Key.Level<=2));}); if(bStop)return; + const bool bNearTileEligible=!Queue.empty() + &&(NearOutstandingJobs==0||Queue.front().Key.Level<=2); const bool bPreferPriority=!PriorityTasks.empty() - &&(Queue.empty()||ConsecutivePriorityTasks Lock(Mutex); if(bStop)return; Running.erase(Tile.Key); + if(Tile.Key.Level<=2&&NearOutstandingJobs>0)--NearOutstandingJobs; if(bColdTimelineTrace) RecordTimelineEvent(FStandaloneCpuTileTimelineEvent::EType::Finish,Tile.Key, WorkerId,Queue.size(),Running.size(),Job.PriorityRank,Tile.CompletedAt, @@ -1065,6 +1072,7 @@ private: Tile.PayloadBuildMilliseconds); Ready.push_back(std::move(Tile)); } + Wake.notify_all(); } } @@ -1208,6 +1216,7 @@ private: mutable std::mutex Mutex; std::condition_variable Wake; bool bStop=true; + size_t NearOutstandingJobs=0; std::vector Workers; std::deque> PriorityTasks; std::vector Queue;