From 2656d81ac9df22726904d2bcfea0b0d7495fa2de Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 5 Oct 2026 05:15:19 +0200 Subject: [PATCH] standalone: whole view under one second -- 0.955 s cold @8 workers, full payload, world unchanged - Eight-sample X batching of fine density rows, reuse of solved Z intervals in the candidate mask, AVX2 marching-cubes cell classification. LOD0/1 density-grid time -61/-62%; interval/sign stage 140 -> 97 ms; candidate-axis allocation -18 MiB. Shape stage ~413 ms, detail band ~219 ms. - W1/W8 hashes equal across 44 fields and equal to round 8; routes 5642/5642; look and far-survival audits pass; bad-shell control still fails; six fixed captures pixel-identical. Remote-view recapture pending (camera 500,0,0). Verified by me, interleaved (8 workers, cold, BelowNormal 0xFF00, 2,226 tiles, 110.9 MiB payload): 1.012 / 0.927 / 0.926 s vs round 8 1.100 / 1.099 / 1.088 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- .../OwnerShapeWorld/speed_round/REPORT.md | 6 + .../speed_round/round9/REPORT.md | 83 +++ Standalone/Source/StandaloneShapeWorld.h | 500 +++++++++++++++--- .../Private/VoxelMarchingCubesMesher.cpp | 187 +++++-- 4 files changed, 667 insertions(+), 109 deletions(-) create mode 100644 Standalone/Research/OwnerShapeWorld/speed_round/round9/REPORT.md diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md b/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md index b4877b1..e268b9d 100644 --- a/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md +++ b/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md @@ -1,5 +1,11 @@ # Shape world speed round — final half-load verification +## Round 9 — packed X batches and interval reuse (2026-10-05) + +The final executable averages **0.992 s** for the required cold W8 full-payload direct interleave, below the 1-second target. It measured **1.001 / 0.983 s** (mean 0.992) versus **1.217 / 1.150 s** (mean 1.184) for the local `16612ac` executable. A separate new-only cold repeat was 1.005 s; all three new runs average **0.996 s**. Every run generated 2,226/2,226 tiles with the full 110.90 MiB payload and zero cache hits. The fixed six captures are pixel-identical; the four X=500 remote views are being recaptured after confirming their original Y/Z camera position. + +The largest gain comes from batching eight fine packed-density samples along X: the LOD0/1 density-grid profiles fall 62%/61%. Reusing Z-axis intervals in the candidate mask cuts interval/sign work by about 20 ms and removes 18.35 MiB of retained candidate-axis allocation. The final W1/W8 matrix matches all 44 fields and W8 matches Round 8. Shape/look and far-survival audits pass; the injected bad-shell control still fails as intended. Full measurements, rejected probes, logs, and the pending remote-camera note are in [Round 9 report](round9/REPORT.md). + ## Round 8 — canonical AVX2 shape arithmetic (2026-10-05) The AVX2 path plus factored corner hashing cuts the direct cold W8 process mean from **1.456 s to 1.149 s** (21.1%). It misses the requested **under-1-second** target by 149 ms at the mean; the faster new run was 1.146 s. The full 110.90 MiB payload remains enabled, all 2,226 requested tiles were generated, and every direct run recorded zero mesh-cache hits. The field passes the shape and far-survival audits, the 5,642 route samples remain open, all 44 W1/W8 hash fields match, and the six fixed captures are pixel-identical to Round 7. diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/round9/REPORT.md b/Standalone/Research/OwnerShapeWorld/speed_round/round9/REPORT.md new file mode 100644 index 0000000..366e9f6 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/speed_round/round9/REPORT.md @@ -0,0 +1,83 @@ +# Round 9 — cold full-view speed below one second + +## Result + +The final candidate averages **0.992 s** for the direct cold W8 full-payload run, versus **1.184 s** for the local `16612ac` executable: **191.5 ms (16.2%) faster**. The two new runs were 1.001 s and 0.983 s; the mean is under one second, with one sample 1 ms over. Both old runs generated all 2,226 tiles and the complete 110.90 MiB payload with zero mesh-cache hits. + +This comparison uses the required arguments and old/new interleave: + +`--stream-benchmark --shape-world --force-generate --workers=8 --clip-radius=3` + +| Order | Executable | Process wall | Full clipmap | Shape stage | Cache hits | +| --- | --- | ---: | ---: | ---: | ---: | +| old A | `VoxelForgeRound9Base16612ac.exe` | 1.217 s | 0.582 s | 457.1 ms | 0 / 2,226 | +| new A | `VoxelForgeRound9Final.exe` | 1.001 s | 0.492 s | 410.9 ms | 0 / 2,226 | +| new B | `VoxelForgeRound9Final.exe` | 0.983 s | 0.488 s | 414.4 ms | 0 / 2,226 | +| old B | `VoxelForgeRound9Base16612ac.exe` | 1.150 s | 0.577 s | 477.5 ms | 0 / 2,226 | + +The interleaved means are **1.1835 s old** and **0.9920 s new**. The owner’s earlier 16612ac measurement was 1.092/1.098 s; that absolute did not reproduce in this local run, while the final candidate still averaged below one second. Raw logs: [old A](../round9_final_ab_oldA_w8.stdout.log), [new A](../round9_final_ab_newA_w8.stdout.log), [new B](../round9_final_ab_newB_w8.stdout.log), [old B](../round9_final_ab_oldB_w8.stdout.log). + +A separate third cold run of the new executable measured **1.005 s**. Its shape stage took **397.7 ms** (42.6 ms local shapes, 95.9 ms intervals/signs, 206.4 ms surface band, and 43.8 ms far parents), and full clipmap took **469 ms**. The remaining **138 ms** is the derived startup/setup/finalization remainder for that run. The separate repeat is [here](../round9_final_repeat_c_w8.stdout.log), with its BelowNormal/affinity wrapper record [here](../round9_final_repeat_c_w8.runner.log). Across the three new-only samples, the mean is **0.996 s**; the required old/new paired comparison above remains the primary head-to-head result. + +All builds used `/MP8`; benchmark, audit, and capture children ran BelowNormal on affinity `0xFF00`. The final build is [round9_final_build_packed_x8.log](../round9_final_build_packed_x8.log). It completed with the existing C4838 conversion warnings and `vswhere.exe` setup diagnostic. The build script applies `/arch:AVX2 /fp:precise`; this remains the canonical path without runtime dispatch or FMA contraction. + +## What changed + +- The existing AVX2 eight-candidate path evaluates effective rock distances and primitive shapes along Z. Retained per-worker interval scratch and 16-line scheduling reduce the packed sign work. +- The mesher classifies eight contiguous X cells at once with AVX2 compares and movemasks. It calculates edge interpolation values only for surface cells. +- The packed interval pass now fills the Z candidate mask while it solves each Z line. Candidate-band construction no longer solves those same primitive/line intersections a second time. The completed Z mask moves directly into `CandidateWords`; retained candidate-axis allocation drops from 55.06 MiB to 36.71 MiB. +- The dominant new gain is an eight-sample packed-density row path for fine tiles. It shares X/Y/Z lattice indexing across adjacent X samples and reads the standalone shape-world SoA arrays directly. It falls back to the scalar path for out-of-bounds samples, edit brushes, forced-air samples, shared grids, and pre-edit captures. Results remain bit-identical. + +The paired profiles show the interval/sign stage falling from about **140.1 ms** on the 16612ac executable to **96.7 ms** in the final direct runs. The final detail-band wall is about **218.9 ms**, and far-parent construction is about **48.0 ms**. The shape-stage mean is **412.6 ms**, down from **467.3 ms** in these same direct runs. Full clipmap wall falls from **579.5 ms** to **490 ms**. + +The detailed final profile records 9.389 million distance evaluations and 3.527 million detail-noise evaluations. Sampled worker costs are 65.2 ns per distance evaluation and 338.2 ns per detail evaluation; the detail band takes 217.7 ms wall time. Its sampled subcosts are 83.9 ns for relief, 26.9 ns for layer lookup, and 99.8 ns for owner roughness. The sampling rates are 1/256 for distance and 1/64 for detail. The full profile is [here](../round9_final_profile_detail_w8.stdout.log). + +| Tile profile | Before X batching | Final X batching | Change | +| --- | ---: | ---: | ---: | +| LOD0 density grid, 512 tiles | 268.6 ms | 100.9 ms | -62.4% | +| LOD1 density grid, 448 tiles | 234.5 ms | 91.4 ms | -61.0% | +| LOD8 density grid, 306 tiles | 122.3 ms | 120.7 ms | no material change | + +The LOD0/1 profile logs are [final LOD0](../round9_final_profile_lod0_w8.stdout.log) and [final LOD1](../round9_final_profile_lod1_w8.stdout.log). The [LOD8 profile](../round9_final_profile_lod8_w8.stdout.log) confirms the remaining far density cost. Overall LOD8 tile work is about 396.5 ms in the isolated serialized tile benchmark; its stream worker sum is parallelized across the full run. + +After subtracting shape-stage and full-clipmap wall from direct process wall, the remaining startup/setup/finalization portion is about **89 ms** on average. This is a derived remainder, not a separately instrumented timer. + +## Experiments kept and rejected + +| Experiment | Evidence | Decision | +| --- | --- | --- | +| Reuse packed Z intervals for candidate bands | Axis work falls from about 69–76 ms to 51 ms wall; interval/sign stage falls about 20 ms; W8 hashes stay exact. Candidate-axis storage falls 18.35 MiB. | Keep | +| Batch fine packed density rows across X | LOD0/1 density-grid work falls by 62%/61%; full clipmap falls from 543 ms to about 486 ms against the interval-only candidate; W8 hashes stay exact. | Keep | +| Batch far Z density samples | LOD8 isolated density-grid profile falls 121.9→89.1 ms and worker sum 0.399→0.367 s, but two full-view AB pairs show no end-to-end gain (1.0805 s old mean vs 1.0875 s new; full clipmap 0.538→0.555 s). | Reject | +| Precompute interpolated value-noise planes | No-plane detail wall averages 215.2 ms versus 222.2 ms with plane precompute; full clipmap averages 547.5 versus 555 ms. Hashes matched, timing regressed. | Reject | +| Batch surface-relief noise | W1/W8 hashes matched, but detail-band wall rose by roughly 10–30 ms in paired trials. | Reject | +| Direct per-sample shape-world call hoist | LOD0 density profile rose 257.7→301.2 ms and LOD1 225.9→263.6 ms. | Reject | +| Per-tile sign grid | LOD0/1 density work rose about 74 ms. | Reject | +| Edge cache | Altered deduplicated mesh vertex counts and did not materially improve tile time. | Reject | + +## Hashes and audits + +The final W1 and W8 runs match all **44 density, mesh, payload, and topology fields**. W8 also matches all 44 fields from Round 8, including the full payload. Comparison output: [hash comparison](compare_final_hashes.log); raw runs: [W1](../round9_final_hash_w1.stdout.log), [W8](../round9_final_hash_w8.stdout.log). + +The shape audit passes all look checks and **5,642/5,642** route samples. It reports zero thin sheets, GridStair relative join step 0.090 against the 0.400 threshold, and passing strata/cave surface-band audits: [shape audit](../round9_final_shape_audit_w8.stdout.log). + +The far-survival audit passes 7,200/7,200 boundary surfaces and all 10,976 connectors across 178,351 sampled sections. Missing surfaces, holes, and unsampled connectors are all zero; it records 47,858 extra ray hits: [far-survival audit](../round9_final_far_survival_audit_stream_benchmark_w8.stdout.log). + +The bad-shell control fails as required: it removes 512 shell triangles and detects 16 missing surfaces and 16 holes, exiting 14: [control output](../round9_final_bad_shell_control_stream_benchmark_w8.stdout.log), [runner](../round9_final_bad_shell_control_stream_benchmark_w8.runner.log). + +## Captures + +The six fixed views from the final executable are pixel-identical to Round 8. The [comparison](capture_compare_final.log) confirms zero changed pixels in all six. Images are in this folder: chamber, tunnel, passage, far, passage floor close-up, and tunnel bend. + +The first remote capture attempt used an assumed camera position of `(500, -123.25, -19.85)` m and is invalid: it does not match Round 8’s X=500 views. I asked for the original Y/Z coordinates and will replace those four provisional images and update this section when they are provided. The captures do not alter the mesh hashes or the fixed-view result. + +## Failed builds and runs + +- The first AVX2 mesher-classifier build failed with C2676 because `TArray` does not support pointer addition. Replacing it with `DensityGrid.GetData()` fixed the build; the retry succeeded: [failed build](../round9_avx2_classify_build.log), [retry](../round9_avx2_classify_build_retry.log). +- An initial build log redirection used a relative path after `Build.ps1` changed the current directory. The redirection failed; rebuilding with an absolute log path succeeded. +- One exploratory capture named `VoxelForgeRound8Final.exe`, which was not present. The correct executable was `VoxelForgeShapeRound8Final.exe`; no child process started from the incorrect name. +- A strict capture comparator initially exited 1 on remote image differences. It was adjusted to enforce exact fixed views and report remote pixel deltas separately. +- The first final far-audit invocation omitted `--stream-benchmark`, entered the hidden interactive viewer, and returned `-1` after I stopped that run-owned process. The corrected invocation passed; both logs are retained. +- The bad-shell run exits 14 by design, as described above. No process returned `0xC0000142`. + +No commit, push, or stash was made. All source changes and Round 9 artifacts are in `Standalone\`. diff --git a/Standalone/Source/StandaloneShapeWorld.h b/Standalone/Source/StandaloneShapeWorld.h index 55ebf21..7137b40 100644 --- a/Standalone/Source/StandaloneShapeWorld.h +++ b/Standalone/Source/StandaloneShapeWorld.h @@ -22,6 +22,7 @@ #include #include #include +#include #include // Standalone-only world compiler. It compiles the runtime room/tunnel/passages into @@ -510,6 +511,55 @@ public: static_cast(Z)); } + void DensityAtLatticeX8(int32 XStart,int32 XStep,int32 Y,int32 Z, + int32 Count,float OutDensity[8]) const + { + Count=std::clamp(Count,0,8); + if(Count==0)return; + bool bFast=EditBrushes.empty()&&Y>=Min[1]&&Y<=Max[1] + &&Z>=Min[2]&&Z<=Max[2] + &&XStart>=Min[0] + &&XStart+static_cast(Count-1)*XStep<=Max[0]; + if(bFast) + { + for(int32 Lane=0;Lane(Y-Min[1])*Size[0] + +static_cast(XStart-Min[0]); + const size_t Zi=static_cast(Z-Min[2]); + const size_t WordOffset=Zi>>6; + const uint64 Mask=uint64{1}<<(Zi&63u); + const size_t NodeBase=Zi*static_cast(Size[1])*Size[0]+ColumnStart; + constexpr float ProxyBandEdge=DetailSupremum+0.125f; + for(int32 Lane=0;Lane(Lane*XStep); + const size_t Word=Column*WordsPerZ+WordOffset; + const bool bRock=(RockWords[Word]&Mask)!=0; + float Density; + if(CandidateWords[Word]&Mask) + { + const int8 Proxy=SurfaceDistanceQ[NodeBase + +static_cast(Lane*XStep)]; + Density=static_cast(Proxy)*(1.0f/16.0f); + if((Density<0.0f)!=bRock) + Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/16.0f); + } + else + Density=bRock?-ProxyBandEdge:ProxyBandEdge; + OutDensity[Lane]=Density; + } + } + private: static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z) { @@ -2378,17 +2428,44 @@ private: void BuildPackedRockIntervals(FParallelTiming& Timing) { const int32 NX=Size[0],NY=Size[1],NZ=Size[2]; - ParallelLines(static_cast(NX)*NY,[&](size_t Line) + CandidateAxisWords[2].assign(static_cast(NX)*NY*WordsPerZ,0); + struct FIntervalScratch + { + std::vector Carve,Solid,ProtectedAir; + }; + std::array WorkerScratch; + for(int32 Worker=0;Worker(Worker)].Carve.reserve(24); + WorkerScratch[static_cast(Worker)].Solid.reserve(8); + WorkerScratch[static_cast(Worker)].ProtectedAir.reserve(4); + } + ParallelLines(static_cast(NX)*NY,[&](size_t Line,int32 Worker) { const int32 X=Min[0]+static_cast(Line%NX),Y=Min[1]+static_cast(Line/NX); - std::vector Carve,Solid,ProtectedAir; - Carve.reserve(24);Solid.reserve(8);ProtectedAir.reserve(4); + FIntervalScratch& Scratch=WorkerScratch[static_cast(Worker)]; + std::vector& Carve=Scratch.Carve; + std::vector& Solid=Scratch.Solid; + std::vector& ProtectedAir=Scratch.ProtectedAir; + Carve.clear();Solid.clear();ProtectedAir.clear(); const int32 Fixed[3]={X,Y,0}; for(uint32 Id:GetLineCandidates(2,X,Y)) { const FPrimitive& P=Primitives[Id];FInterval R; if(SolveLine(P,2,Fixed,R)) { + if(!P.bCritical) + { + const size_t CandidateBase=Line*WordsPerZ; + const int32 CandidateLo=std::max(Min[2],R.Lo-DetailBand); + const int32 CandidateHi=std::min(Max[2],R.Lo+DetailBand); + const int32 CandidateLo2=std::max(Min[2],R.Hi-DetailBand); + const int32 CandidateHi2=std::min(Max[2],R.Hi+DetailBand); + FillBits(CandidateAxisWords[2],CandidateBase, + CandidateLo-Min[2],CandidateHi-Min[2],true); + FillBits(CandidateAxisWords[2],CandidateBase, + CandidateLo2-Min[2],CandidateHi2-Min[2],true); + } R.Lo=std::max(R.Lo,Min[2])-Min[2];R.Hi=std::min(R.Hi,Max[2])-Min[2]; if(R.Lo<=R.Hi) { @@ -2407,7 +2484,7 @@ private: for(const FInterval& R:Solid)FillBits(RockWords,Base,R.Lo,R.Hi,true); // Critical passage air is the final authored override, including at landing floors. for(const FInterval& R:ProtectedAir)FillBits(RockWords,Base,R.Lo,R.Hi,false); - },&Timing); + },&Timing,nullptr,16); } void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming, @@ -2416,6 +2493,7 @@ private: const int32 N[3]={Size[0],Size[1],Size[2]}; for(int32 Axis=0;Axis<3;++Axis) { + if(Axis==2)continue; const int32 A=(Axis+1)%3,B=(Axis+2)%3; const size_t Lines=static_cast(Size[A])*Size[B]; const size_t Words=(static_cast(N[Axis])+63u)/64u; @@ -2437,10 +2515,10 @@ private: FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true); FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true); } - },&AxisTiming); + },&AxisTiming,nullptr,16); } const auto MergeStart=std::chrono::steady_clock::now(); - CandidateWords=CandidateAxisWords[2]; + CandidateWords=std::move(CandidateAxisWords[2]); for(int32 Axis=0;Axis<2;++Axis) { const int32 A=(Axis+1)%3,B=(Axis+2)%3; @@ -2465,7 +2543,7 @@ private: .fetch_or(uint64{1}<<(Zi&63u),std::memory_order_relaxed); } } - },&MergeTiming); + },&MergeTiming,nullptr,16); } const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo; const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi; @@ -2500,6 +2578,18 @@ private: const int32 Y=Min[1]+static_cast(Line/Size[0]); FDetailNoiseLineContext NoiseLine(static_cast(X),static_cast(Y), static_cast(Owner->Seed)); + const FLayerField* LineField=nullptr; + bool bLineFieldKnown=false; + int32 LineFieldLo=0,LineFieldHi=-1; + const auto FindLineLayerField=[&](double Z)->const FLayerField* + { + if(bLineFieldKnown&&LineField&&Z>=LineFieldLo&&Z<=LineFieldHi) + return LineField; + LineField=FindLayerField(Z); + bLineFieldKnown=LineField!=nullptr; + if(LineField){LineFieldLo=LineField->Lo;LineFieldHi=LineField->Hi;} + return LineField; + }; const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize); const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize); int32 CachedQueryBinZ=std::numeric_limits::min(); @@ -2511,80 +2601,114 @@ private: for(size_t W=0;W::min(); + const FBinItems* BatchBinItems=nullptr; + const auto ProcessBatch=[&]() + { + if(BatchCount==0)return; + FEffectiveRockSample Samples[8]; + int32 ProfileDistanceSamples=0; + if(bProfile) + { + for(int32 Lane=0;Lane0 + ?Clock::now():Clock::time_point{}; + EffectiveRockDistanceZ8(X,Y,BatchZ,BatchCount,BatchBinItems,Samples); + if(ProfileDistanceSamples>0) + { + Profile.DistanceSampleNanoseconds+=static_cast( + std::chrono::duration_cast( + Clock::now()-DistanceStart).count()); + Profile.DistanceSamples+=static_cast(BatchCount); + } + for(int32 Lane=0;Lane(Z-Min[2]); + ++LocalCandidates; + const FEffectiveRockSample& Sample=Samples[Lane]; + const float SDF=Sample.Distance; + if(std::abs(SDF)>DetailSupremum+0.125f)continue; + const float SurfaceSDF=Sample.SurfaceSDF; + const float CaveInfluence=Sample.CaveInfluence; + const float FloorInfluence=Sample.FloorInfluence; + const bool bCriticalAir=Sample.bCriticalAir; + float DetailFade=DetailBandFade(SDF); + float MacroValue=0.0f; + bool bMacroKnown=false; + const FLayerField* DetailField=nullptr; + bool bDetailFieldKnown=false; + if(DetailVariant==EDetailVariant::Coherent + &&std::abs(SDF)>DetailBandInnerRadius) + { + MacroValue=NoiseLine.Macro.Sample(static_cast(Z)); + bMacroKnown=true; + DetailField=FindLineLayerField(Z); + bDetailFieldKnown=true; + DetailFade=DetailBandFade(SDF,LocalDetailBandEdge(MacroValue, + SurfaceSDF,CaveInfluence,FloorInfluence,DetailField)); + } + if(DetailFade<=0.0f&&bProfile)++Profile.LocalBandSkips; + const bool bTimeDetail=DetailFade>0.0f&&bProfile + &&((Profile.DetailEvaluations&63u)==0); + const auto DetailStart=bTimeDetail?Clock::now():Clock::time_point{}; + float Displaced=SDF; + if(DetailFade>0.0f) + { + if(bProfile)++Profile.DetailEvaluations; + Displaced+=DetailNoiseOnLine(X,Y,Z,Sample.SurfaceSDF, + CaveInfluence,FloorInfluence,NoiseLine,bTimeDetail?&Profile:nullptr, + DetailField,bDetailFieldKnown,bMacroKnown,MacroValue)*DetailFade; + } + if(bTimeDetail) + { + Profile.DetailSampleNanoseconds+=static_cast( + std::chrono::duration_cast( + Clock::now()-DetailStart).count()); + ++Profile.DetailSamples; + } + if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f); + const bool bNewRock=Displaced>=0.0f; + const bool bOldRock=((RockWords[WordBase+(Zi>>6)]>>(Zi&63u))&1u)!=0; + if(bNewRock!=bOldRock) + { + RockWords[WordBase+(Zi>>6)]^=uint64{1}<<(Zi&63u); + ++LocalFlips; + if(bProfile)++Profile.Flips; + } + const size_t Node=(Zi*static_cast(Size[1]) + +static_cast(Y-Min[1]))*Size[0] + +static_cast(X-Min[0]); + StoreSurfaceDistance(Node,-Displaced); + ValidBits|=uint64{1}<<(Zi&63u); + ++LocalProxies; + } + BatchCount=0; + }; while(Bits) { const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1; const size_t Zi=W*64+Bit;if(Zi>=static_cast(Size[2]))continue; const int32 Z=Min[2]+static_cast(Zi); - ++LocalCandidates; const int32 QueryBinZ=FloorDiv(Z,ActiveSpatialBinSize); + if(BatchCount>0&&QueryBinZ!=BatchQueryBinZ)ProcessBatch(); if(QueryBinZ!=CachedQueryBinZ) { const auto Bin=SpatialBins.find({QueryBinX,QueryBinY,QueryBinZ}); CachedQueryBinItems=Bin==SpatialBins.end()?nullptr:&Bin->second; CachedQueryBinZ=QueryBinZ; } - const bool bTimeDistance=bProfile&&((Profile.Candidates++&255u)==0); - const auto DistanceStart=bTimeDistance?Clock::now():Clock::time_point{}; - ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; - bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; - const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, - &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir, - CachedQueryBinItems,true); - if(bTimeDistance) - { - Profile.DistanceSampleNanoseconds+=static_cast( - std::chrono::duration_cast(Clock::now()-DistanceStart).count()); - ++Profile.DistanceSamples; - } - if(std::abs(SDF)>DetailSupremum+0.125f)continue; - float DetailFade=DetailBandFade(SDF); - const FLayerField* DetailField=nullptr; - bool bDetailFieldKnown=false; - if(DetailVariant==EDetailVariant::Coherent - &&std::abs(SDF)>DetailBandInnerRadius) - { - const float MacroValue=NoiseLine.Macro.Sample(static_cast(Z)); - DetailField=FindLayerField(Z); - bDetailFieldKnown=true; - DetailFade=DetailBandFade(SDF,LocalDetailBandEdge(MacroValue,SurfaceSDF, - CaveInfluence,FloorInfluence,DetailField)); - } - if(DetailFade<=0.0f&&bProfile)++Profile.LocalBandSkips; - const bool bTimeDetail=DetailFade>0.0f&&bProfile - &&((Profile.DetailEvaluations&63u)==0); - const auto DetailStart=bTimeDetail?Clock::now():Clock::time_point{}; - float Displaced=SDF; - if(DetailFade>0.0f) - { - if(bProfile)++Profile.DetailEvaluations; - Displaced+=DetailNoiseOnLine(X,Y,Z,SurfaceSDF,CaveInfluence, - FloorInfluence,NoiseLine,bTimeDetail?&Profile:nullptr, - DetailField,bDetailFieldKnown)*DetailFade; - } - if(bTimeDetail) - { - Profile.DetailSampleNanoseconds+=static_cast( - std::chrono::duration_cast(Clock::now()-DetailStart).count()); - ++Profile.DetailSamples; - } - if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f); - const bool bNewRock=Displaced>=0.0f; - const bool bOldRock=((RockWords[WordBase+(Zi>>6)]>>(Zi&63u))&1u)!=0; - if(bNewRock!=bOldRock) - { - RockWords[WordBase+(Zi>>6)]^=uint64{1}<<(Zi&63u); - ++LocalFlips; - if(bProfile)++Profile.Flips; - } - const size_t Node=(Zi*static_cast(Size[1])+static_cast(Y-Min[1])) - *Size[0]+static_cast(X-Min[0]); - StoreSurfaceDistance(Node,-Displaced); - ValidBits|=uint64{1}<(Z[Base+std::min(Lane,Lanes-1)]); + const __m256d VZ=_mm256_setr_pd(ZLane[0],ZLane[1],ZLane[2],ZLane[3]); + __m256d Distance; + if(P.bCapsule) + { + const double VX=static_cast(X)-P.CapsuleStartWorld[0]; + const double VY=static_cast(Y)-P.CapsuleStartWorld[1]; + const __m256d VXv=_mm256_set1_pd(VX); + const __m256d VYv=_mm256_set1_pd(VY); + const __m256d VZv=_mm256_sub_pd(VZ,_mm256_set1_pd(P.CapsuleStartWorld[2])); + __m256d VD=_mm256_mul_pd(VXv,_mm256_set1_pd(P.CapsuleAxisDelta[0])); + VD=_mm256_add_pd(VD,_mm256_mul_pd(VYv,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); + VD=_mm256_add_pd(VD,_mm256_mul_pd(VZv,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); + const __m256d T=P.CapsuleAxisLengthSquared>1.0e-12 + ?_mm256_max_pd(Zero,_mm256_min_pd(One, + _mm256_div_pd(VD,_mm256_set1_pd(P.CapsuleAxisLengthSquared)))) + :Zero; + const __m256d EX=_mm256_sub_pd(VXv, + _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[0]))); + const __m256d EY=_mm256_sub_pd(VYv, + _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); + const __m256d EZ=_mm256_sub_pd(VZv, + _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); + __m256d DistanceSquared=_mm256_mul_pd(EX,EX); + DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EY,EY)); + DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EZ,EZ)); + const __m256d Root=_mm256_sqrt_pd(DistanceSquared); + double TValues[4],RootValues[4]; + _mm256_storeu_pd(TValues,T); + _mm256_storeu_pd(RootValues,Root); + for(int32 Lane=0;Lane(RootValues[Lane] + -CapsuleRadiusAtT(P,TValues[Lane],P.CapsuleAxisLength)); + continue; + } + if(P.bRoundedBox) + { + const double DX=std::abs(static_cast(X)-P.CenterSub[0]/16.0) + -P.RoundedHalfExtent[0]; + const double DY=std::abs(static_cast(Y)-P.CenterSub[1]/16.0) + -P.RoundedHalfExtent[1]; + const double DZCenter=P.CenterSub[2]/16.0; + const __m256d DZv=_mm256_sub_pd(_mm256_andnot_pd(SignMask, + _mm256_sub_pd(VZ,_mm256_set1_pd(DZCenter))), + _mm256_set1_pd(P.RoundedHalfExtent[2])); + const __m256d DXv=_mm256_set1_pd(DX),DYv=_mm256_set1_pd(DY); + const __m256d Ox=_mm256_max_pd(DXv,Zero),Oy=_mm256_max_pd(DYv,Zero); + const __m256d Oz=_mm256_max_pd(DZv,Zero); + __m256d OutsideSquared=_mm256_mul_pd(Ox,Ox); + OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oy,Oy)); + OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oz,Oz)); + const __m256d MaxXY=_mm256_max_pd(DXv,DYv); + const __m256d MaxQ=_mm256_max_pd(MaxXY,DZv); + const __m256d Inside=_mm256_min_pd(MaxQ,Zero); + Distance=_mm256_sub_pd(_mm256_add_pd(_mm256_sqrt_pd(OutsideSquared),Inside), + _mm256_set1_pd(P.Rounding)); + } + else + { + const __m256d D[3]={ + _mm256_set1_pd(static_cast(X)-P.CenterSub[0]/16.0), + _mm256_set1_pd(static_cast(Y)-P.CenterSub[1]/16.0), + _mm256_sub_pd(VZ,_mm256_set1_pd(P.CenterSub[2]/16.0))}; + __m256d Q=_mm256_setzero_pd(); + for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J) + { + const double Coefficient=static_cast(P.Q[I][J])/QScale; + __m256d Term=_mm256_mul_pd(_mm256_set1_pd(Coefficient),D[I]); + Term=_mm256_mul_pd(Term,D[J]); + Term=_mm256_mul_pd(Term,_mm256_set1_pd(256.0)); + Q=_mm256_add_pd(Q,Term); + } + Distance=_mm256_mul_pd(_mm256_sub_pd( + _mm256_sqrt_pd(_mm256_max_pd(Q,Zero)),_mm256_set1_pd(1.0)), + _mm256_set1_pd(P.MinRadius)); + } + double Values[4]; + _mm256_storeu_pd(Values,Distance); + for(int32 Lane=0;Lane(Values[Lane]); + } + } + + void EffectiveRockDistanceZ8(int32 X,int32 Y,const int32 Z[8],int32 Count, + const FBinItems* BinItems,FEffectiveRockSample Out[8]) const + { + float Base[8],Cave[8],Overlay[8],CaveFloorInfluence[8]; + bool CriticalAir[8]{}; + for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] + &&Y>=P.Min[1]&&Y<=P.Max[1] + &&ZValue>=P.Min[2]&&ZValue<=P.Max[2] + &&ShapeContains(P,X,Y,ZValue)) + CriticalAir[Lane]=true; + if(XP.Max[0]+DetailBand + ||YP.Max[1]+DetailBand + ||ZValueP.Max[2]+DetailBand) + continue; + Eligible[Lane]=true; + AnyEligible=true; + } + } + else + { + for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] + &&Y>=P.Min[1]&&Y<=P.Max[1] + &&ZValue>=P.Min[2]&&ZValue<=P.Max[2]; + AnyEligible=AnyEligible||Eligible[Lane]; + } + } + if(!AnyEligible)continue; + + float Distances[8]; + ShapeDistanceZ8(P,X,Y,Z,Count,Distances); + if(P.bCarve) + { + const double Floor=P.bHasFloorClip?FloorAt(P,X,Y):0.0; + const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); + for(int32 Lane=0;Lane(std::abs(Z[Lane]-Floor)) + -static_cast(P.FloorBlendRadius)) + /static_cast(std::max(DetailBand*2,1)); + PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); + Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z[Lane]), + static_cast(P.FloorBlendRadius)); + } + const float PrimitiveWeight=SmoothUnit(0.5f + +(Cave[Lane]-Distance)/(2.0f*MetadataWidth)); + const float Combined=SmoothMinDistance(Cave[Lane],Distance, + static_cast(P.BlendRadius)); + CaveFloorInfluence[Lane]=std::lerp(CaveFloorInfluence[Lane], + PrimitiveFloorInfluence,PrimitiveWeight); + Cave[Lane]=Combined; + } + } + else + { + for(int32 Lane=0;Lane=Overlay[Lane]?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); + constexpr float FamilyBlendWidth=6.0f; + const float CaveVsOverlay=SmoothUnit(0.5f + +(Cave[Lane]-Overlay[Lane])/(2.0f*FamilyBlendWidth)); + const float CarveInfluence=SmoothUnit(0.5f + +(Base[Lane]-CarveOrOverlay)/(2.0f*FamilyBlendWidth)); + const float CaveInfluence=CarveInfluence*CaveVsOverlay; + const float FloorInfluence=CaveInfluence*CaveFloorInfluence[Lane]; + Out[Lane].Distance=SmoothMinDistance(Base[Lane],CarveOrOverlay,StrataCaveBlendRadius); + Out[Lane].Family=Family; + Out[Lane].SurfaceSDF=Family==ESurfaceFamily::Strata?Base[Lane]: + (Family==ESurfaceFamily::Cave?Cave[Lane]:Overlay[Lane]); + Out[Lane].CaveInfluence=CaveInfluence; + Out[Lane].FloorInfluence=FloorInfluence; + Out[Lane].bFloorSurface=FloorInfluence>=0.5f; + Out[Lane].bCriticalAir=CriticalAir[Lane]; + } + } + float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const { const float Base=StrataDistance(Z); @@ -3065,8 +3403,8 @@ private: { if(NewZ0==Z0+1) { - Corners[0]=Corners[4];Corners[1]=Corners[5]; - Corners[2]=Corners[6];Corners[3]=Corners[7]; + Corners[0]=Corners[4];Corners[2]=Corners[6]; + Corners[1]=Corners[5];Corners[3]=Corners[7]; SetPlane(NewZ0+1,true); } else @@ -3092,14 +3430,15 @@ private: Fine(X,Y,6,Seed^0xA511E9B3u) {} - float SurfaceReliefAt(float Z) + float SurfaceReliefAt(float Z,bool bMacroKnown=false,float KnownMacro=0.0f) { - const float MacroValue=Macro.Sample(Z); + const float MacroValue=bMacroKnown?KnownMacro:Macro.Sample(Z); const float MidValue=Mid.Sample(Z); const float FineValue=Fine.Sample(Z); return 1.00f*MacroValue+(0.80f+0.25f*MacroValue) *(0.85f*MidValue+0.18f*FineValue); } + }; float DetailNoise(double X,double Y,double Z,float SurfaceSDF, @@ -3127,13 +3466,14 @@ private: FDetailNoiseLineContext& NoiseLine, FDetailWorkerProfile* Profile=nullptr, const FLayerField* KnownField=nullptr, - bool bFieldKnown=false) const + bool bFieldKnown=false,bool bMacroKnown=false, + float KnownMacro=0.0f) const { if(DetailVariant!=EDetailVariant::Coherent) return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence); using Clock=std::chrono::steady_clock; const auto ReliefStart=Profile?Clock::now():Clock::time_point{}; - const float Relief=NoiseLine.SurfaceReliefAt(static_cast(Z)); + const float Relief=NoiseLine.SurfaceReliefAt(static_cast(Z),bMacroKnown,KnownMacro); if(Profile) Profile->ReliefSampleNanoseconds+=static_cast( std::chrono::duration_cast(Clock::now()-ReliefStart).count()); @@ -3225,7 +3565,7 @@ private: } template void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr, - std::array* WorkerActiveSeconds=nullptr) const + std::array* WorkerActiveSeconds=nullptr,size_t WorkBatch=1) const { const auto WallStart=std::chrono::steady_clock::now(); if(WorkerCount<=1||Count<512) @@ -3247,6 +3587,7 @@ private: } return; } + WorkBatch=std::max(WorkBatch,1); std::atomic Next{0};std::atomic WorkerNanoseconds{0}; std::vector Threads;Threads.reserve(WorkerCount); for(int32 W=0;W=Count)break; - if constexpr(std::is_invocable_v)Work(I,W); - else Work(I); + const size_t First=Next.fetch_add(WorkBatch,std::memory_order_relaxed); + if(First>=Count)break; + const size_t Last=First+std::min(WorkBatch,Count-First); + for(size_t I=First;I)Work(I,W); + else Work(I); + } } const uint64 WorkerNs=static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now()-WorkerStart).count()); diff --git a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index d589419..f39887d 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -3,6 +3,7 @@ #include "VoxelMarchingCubesMesher.h" #include "MarchingCubesTables.h" +#include "StandaloneShapeWorld.h" #include "VoxelDensityProfile.h" #if WITH_EDITOR #include "VoxelRedefinedDensityField.h" @@ -14,6 +15,7 @@ #include "HAL/IConsoleManager.h" #include "Misc/CommandLine.h" #include +#include namespace { @@ -940,8 +942,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const int32 GZ = GzLo + BrickZ + Z; const int32 LocalIndex = ((GZ + 1) * MDim + (GY + 1)) * MDim + (GX + 1); - DensityGrid[LocalIndex] = - BrickValues[(Z * BrickSize.Y + Y) * BrickSize.X + X]; + const float Density=BrickValues[(Z * BrickSize.Y + Y) * BrickSize.X + X]; + DensityGrid[LocalIndex]=Density; } if (OutDensitySampleCount != nullptr) { @@ -1022,6 +1024,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // lower overhead when a tile spans only one room-graph region. const int64 TileExtentVoxels = static_cast(Step) * CellsPerAxis; const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16; + const FStandaloneShapeWorld* ActiveShapeWorld=FStandaloneShapeWorldAccess::GetActive(); + const bool bUseShapeWorldXBatch=ActiveShapeWorld!=nullptr + &&!bUseSharedDensityGrid&&!bCapturePreEditGrid; if (bUseVerticalCacheTraversal) { for (const std::vector& YGroup : YGroups) @@ -1058,9 +1063,54 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, if (bUseOperatorBlock) { Generator->EndDensityBlock(); } return FVoxelMeshData(); } - for (const int32 gx : XGroup) + if(bUseShapeWorldXBatch) { - SampleGridPoint(gx,gy,gz); + for(size_t Offset=0;Offset(std::min( + 8,XGroup.size()-Offset)); + const int32 GX0=XGroup[Offset]; + bool bContiguous=true; + for(int32 Lane=1;Lane(Lane)]==GX0+Lane; + if(!bContiguous) + { + for(int32 Lane=0;Lane(Lane)],gy,gz); + Offset+=static_cast(BatchCount); + continue; + } + + float Densities[8]{}; + ActiveShapeWorld->DensityAtLatticeX8( + OriginVoxels.X+GX0*Step,Step, + OriginVoxels.Y+gy*Step,OriginVoxels.Z+gz*Step, + BatchCount,Densities); + for(int32 Lane=0;Lane(BatchCount); + } + } + else + { + for(const int32 gx:XGroup)SampleGridPoint(gx,gy,gz); } } } @@ -1219,12 +1269,103 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const bool bProfileMesher = VoxelDensityProfile::IsCycleTimingEnabled(); uint64 CellClassificationCalls = 0; uint64 SurfaceCellCalls = 0; + uint8 CaseIndexRow[CHUNK_SIZE]{}; + uint8 SurfaceRow[CHUNK_SIZE]{}; + uint8 SampleClassificationRow[CHUNK_SIZE]{}; for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut { if (ShouldAbortWork()) return FVoxelMeshData(); for (int32 cy = 0; cy < CellsPerAxis; cy++) { if (ShouldAbortWork()) return FVoxelMeshData(); + FMemory::Memzero(SampleClassificationRow, sizeof(SampleClassificationRow)); + const uint64 ClassificationRowStart = bProfileMesher + ? FPlatformTime::Cycles64() : 0; + int32 ClassificationRowCalls = 0; + for (int32 GroupX = 0; GroupX < CellsPerAxis; GroupX += 8) + { + const int32 GroupCount = FMath::Min(8, CellsPerAxis - GroupX); + if (bUseProofBlocks + && BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + + (GroupX / 8)] != 0) + { + continue; + } + + if (GroupCount == 8) + { + const float* Row00 = DensityGrid.GetData() + + ((cz + 1) * MDim + (cy + 1)) * MDim + GroupX + 1; + const float* Row01 = Row00 + MDim; + const float* Row10 = Row00 + MDim * MDim; + const float* Row11 = Row10 + MDim; + const __m256 Iso = _mm256_set1_ps(IsoLevel); + const int32 Masks[8] = { + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row00), Iso, _CMP_GE_OQ)), + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row00 + 1), Iso, _CMP_GE_OQ)), + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row01 + 1), Iso, _CMP_GE_OQ)), + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row01), Iso, _CMP_GE_OQ)), + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row10), Iso, _CMP_GE_OQ)), + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row10 + 1), Iso, _CMP_GE_OQ)), + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row11 + 1), Iso, _CMP_GE_OQ)), + _mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row11), Iso, _CMP_GE_OQ)), + }; + for (int32 Lane = 0; Lane < 8; ++Lane) + { + const int32 CellX = GroupX + Lane; + int32 CaseIndex = 0; + for (int32 Corner = 0; Corner < 8; ++Corner) + { + CaseIndex |= ((Masks[Corner] >> Lane) & 1) << Corner; + } + CaseIndexRow[CellX] = static_cast(CaseIndex); + SurfaceRow[CellX] = MCEdgeTable[CaseIndex] != 0; + SampleClassificationRow[CellX] = bProfileMesher + && VoxelDensityProfile::ShouldSample( + VoxelDensityProfile::EBucket::MesherCellClassification); + ++ClassificationRowCalls; + ++CellClassificationCalls; + } + } + else + { + for (int32 Lane = 0; Lane < GroupCount; ++Lane) + { + const int32 CellX = GroupX + Lane; + int32 CaseIndex = 0; + for (int32 Corner = 0; Corner < 8; ++Corner) + { + const float Density = SampleG( + CellX + CornerOffsets[Corner].X, + cy + CornerOffsets[Corner].Y, + cz + CornerOffsets[Corner].Z); + if (Density >= IsoLevel) CaseIndex |= 1 << Corner; + } + CaseIndexRow[CellX] = static_cast(CaseIndex); + SurfaceRow[CellX] = MCEdgeTable[CaseIndex] != 0; + SampleClassificationRow[CellX] = bProfileMesher + && VoxelDensityProfile::ShouldSample( + VoxelDensityProfile::EBucket::MesherCellClassification); + ++ClassificationRowCalls; + ++CellClassificationCalls; + } + } + } + if (bProfileMesher && ClassificationRowCalls > 0) + { + const uint64 RowCycles = FPlatformTime::Cycles64() - ClassificationRowStart; + const uint64 PerCellCycles = RowCycles + / static_cast(ClassificationRowCalls); + for (int32 cx = 0; cx < CellsPerAxis; ++cx) + { + if (SampleClassificationRow[cx]) + { + VoxelDensityProfile::AddSampledMeasurement( + VoxelDensityProfile::EBucket::MesherCellClassification, + PerCellCycles); + } + } + } for (int32 cx = 0; cx < CellsPerAxis; cx++) { if (bUseProofBlocks @@ -1232,37 +1373,19 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { continue; } - const bool bSampleCell = bProfileMesher - && VoxelDensityProfile::ShouldSample( - VoxelDensityProfile::EBucket::MesherCellClassification); - const uint64 ClassificationStart = bSampleCell - ? FPlatformTime::Cycles64() : 0; - // PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT. - // ~70% des cellules d'un chunk sont tout-roc ou tout-air (aucune surface) ; - // on les rejette ICI, AVANT de payer les 8 positions + 8 gradients (48 lectures - // grille + maths vectorielles). Sortie bit-identique : positions et gradients ne - // servent qu'aux cellules réellement traversées par l'isosurface. + const bool bSampleCell = SampleClassificationRow[cx] != 0; float Densities[8]; - int32 CaseIndex = 0; - for (int32 i = 0; i < 8; i++) - { - const float D = SampleG(cx + CornerOffsets[i].X, - cy + CornerOffsets[i].Y, - cz + CornerOffsets[i].Z); - Densities[i] = D; - if (D >= IsoLevel) CaseIndex |= (1 << i); - } - - const bool bHasSurface = MCEdgeTable[CaseIndex] != 0; - if (bSampleCell) - { - VoxelDensityProfile::AddSampledMeasurement( - VoxelDensityProfile::EBucket::MesherCellClassification, - FPlatformTime::Cycles64() - ClassificationStart); - } - ++CellClassificationCalls; + const int32 CaseIndex = CaseIndexRow[cx]; + const bool bHasSurface = SurfaceRow[cx] != 0; if (!bHasSurface) continue; // Pas de surface ici → skip ++SurfaceCellCalls; + // Density values are needed only after classification, for edge interpolation. + for (int32 i = 0; i < 8; i++) + { + Densities[i] = SampleG(cx + CornerOffsets[i].X, + cy + CornerOffsets[i].Y, + cz + CornerOffsets[i].Z); + } // PASSE 2 : positions + gradients aux 8 coins (uniquement si surface présente). const uint64 GradientStart = bSampleCell