wip(standalone): GPU step 5 -- far GPU work 646 -> 112 ms; hybrid whole view equal to CPU

Whole far field on the RX 7900 XTX: 112 ms total (connector rings 61 ms). Cold hybrid 2.303 s vs
all-CPU 2.314 s (within noise): the far field is no longer on the wall-clock critical path.
0 sign/topology differences vs CPU and WARP incl. 127,401 protected and 353 thin-feature cells;
repeat GPU runs identical; far survival passes on GPU meshes; CPU hashes unchanged.
Open: CPU/GPU vertex distance mean 1.15 mm but max 64.56 cm, 247 tiles over the quantisation bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-06 13:50:07 +02:00
1 parent 247682a8e0
commit 0e730cdbba
9 files changed
+683 -246

No files matched your search

@@ -195,9 +195,22 @@ namespace VoxelForgeCore
float floorCurveStartBlend = 0.0f, floorCurveEndBlend = 0.0f;
};
struct FarFieldSampleRange
// World-static GPU upload. The hash-table layout is shared by every far tile;
// each cell contains the exact CPU spatial-bin coordinate and its ordered shape IDs.
struct FarFieldSpatialBin
{
std::uint32_t offset = 0, count = 0;
std::int32_t x = 0, y = 0, z = 0;
std::uint32_t offset = 0, count = 0, padding = 0;
};
static_assert(sizeof(FarFieldSpatialBin) == 24);
struct FarFieldWorldInput
{
std::uint32_t spatialBinSize = 32, spatialBinMask = 0;
std::vector<FarFieldPrimitiveGpu> shapes;
// Open-addressed table. count == 0 denotes an empty slot; mask is capacity - 1.
std::vector<FarFieldSpatialBin> spatialBins;
std::vector<std::uint32_t> spatialBinShapeIds;
};
struct FarFieldStratumBoundary
@@ -224,11 +237,7 @@ namespace VoxelForgeCore
Vec3i origin{};
std::int32_t cells = 0, step = 0, sampleDim = 0;
float strataLo = 0.0f, strataHi = 0.0f, strataBlend = 12.0f;
std::vector<FarFieldPrimitiveGpu> shapes;
// One range per sample in the (cells+3)^3 gradient-halo lattice.
// Shape IDs preserve the CPU spatial-bin iteration order.
std::vector<FarFieldSampleRange> sampleShapeRanges;
std::vector<std::uint32_t> sampleShapeIds;
std::shared_ptr<const FarFieldWorldInput> worldInput;
// Sparse exception bitset: bit 1 forces route-protected air. Ordinary rock/air
// classification is computed from the GPU field and is never uploaded by the CPU.
std::vector<std::uint32_t> protectedAir;
+171 -78
View File
@@ -464,6 +464,99 @@ public:
OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end());
}
static uint32 FarFieldBinHash(int32 X,int32 Y,int32 Z)
{
uint32 Hash=2166136261u;
Hash=(Hash^static_cast<uint32>(X))*16777619u;
Hash=(Hash^static_cast<uint32>(Y))*16777619u;
Hash=(Hash^static_cast<uint32>(Z))*16777619u;
Hash^=Hash>>16;Hash*=0x7FEB352Du;Hash^=Hash>>15;
Hash*=0x846CA68Bu;Hash^=Hash>>16;
return Hash;
}
static VoxelForgeCore::FarFieldPrimitiveGpu ToFarFieldGpuPrimitive(const VF_FieldPrimitive& S)
{
VoxelForgeCore::FarFieldPrimitiveGpu G{};
G.flags=S.flags;
for(int32 A=0;A<3;++A)
{
G.minVoxel[A]=S.minVoxel[A];G.maxVoxel[A]=S.maxVoxel[A];
G.center[A]=static_cast<float>(S.center[A]);
G.capsuleStart[A]=static_cast<float>(S.capsuleStart[A]);
G.capsuleDelta[A]=static_cast<float>(S.capsuleDelta[A]);
G.roundedHalfExtent[A]=static_cast<float>(S.roundedHalfExtent[A]);
}
for(int32 I=0;I<9;++I)G.q[I]=static_cast<float>(S.q[I]);
G.minRadius=static_cast<float>(S.minRadius);
G.capsuleAxisLengthSquared=static_cast<float>(S.capsuleAxisLengthSquared);
G.capsuleAxisLength=static_cast<float>(S.capsuleAxisLength);
G.capsuleStartRadius=static_cast<float>(S.capsuleStartRadius);
G.capsuleEndRadius=static_cast<float>(S.capsuleEndRadius);
G.profileStartT=static_cast<float>(S.profileStartT);G.profileEndT=static_cast<float>(S.profileEndT);
G.profileStartRadius=static_cast<float>(S.profileStartRadius);G.profileEndRadius=static_cast<float>(S.profileEndRadius);
G.radiusSlopeStart=static_cast<float>(S.radiusSlopeStart);G.radiusSlopeEnd=static_cast<float>(S.radiusSlopeEnd);
G.rounding=static_cast<float>(S.rounding);
G.blendRadius=static_cast<float>(S.blendRadius);G.floorBlendRadius=static_cast<float>(S.floorBlendRadius);
G.floorCenterZ=static_cast<float>(S.floorCenterZ);G.floorSlopeX=static_cast<float>(S.floorSlopeX);
G.floorSlopeY=static_cast<float>(S.floorSlopeY);
G.floorCurveOriginX=static_cast<float>(S.floorCurveOriginX);G.floorCurveOriginY=static_cast<float>(S.floorCurveOriginY);
G.floorCurveDeltaX=static_cast<float>(S.floorCurveDeltaX);G.floorCurveDeltaY=static_cast<float>(S.floorCurveDeltaY);
G.floorCurveLength=static_cast<float>(S.floorCurveLength);
G.floorCurveStartZ=static_cast<float>(S.floorCurveStartZ);G.floorCurveEndZ=static_cast<float>(S.floorCurveEndZ);
G.floorCurveTangentStart=static_cast<float>(S.floorCurveTangentStart);
G.floorCurveTangentEnd=static_cast<float>(S.floorCurveTangentEnd);
G.floorCurveStartBlend=static_cast<float>(S.floorCurveStartBlend);
G.floorCurveEndBlend=static_cast<float>(S.floorCurveEndBlend);
return G;
}
std::shared_ptr<const VoxelForgeCore::FarFieldWorldInput> GetFarFieldWorldInput(
std::string& Error) const
{
std::lock_guard<std::mutex> Lock(FarFieldGpuInputMutex);
if(CachedFarFieldGpuInput)return CachedFarFieldGpuInput;
if(!FarBuilt||SpatialBins.empty()||SharedFieldPrimitives.empty())
{Error="far world has no shared primitives or spatial bins";return {};}
if(SpatialBins.size()>std::numeric_limits<uint32>::max()/2u)
{Error="far world spatial-bin table exceeds the GPU 32-bit slot limit";return {};}
const size_t TableCapacity=std::bit_ceil(std::max<size_t>(2,SpatialBins.size()*2));
if(TableCapacity>std::numeric_limits<uint32>::max())
{Error="far world spatial-bin table exceeds the D3D11 element limit";return {};}
auto Data=std::make_shared<VoxelForgeCore::FarFieldWorldInput>();
Data->spatialBinSize=static_cast<uint32>(ActiveSpatialBinSize);
Data->spatialBinMask=static_cast<uint32>(TableCapacity-1);
Data->shapes.reserve(SharedFieldPrimitives.size());
for(const VF_FieldPrimitive& Shape:SharedFieldPrimitives)
Data->shapes.push_back(ToFarFieldGpuPrimitive(Shape));
Data->spatialBins.resize(TableCapacity);
size_t ReferenceCount=0;
for(const auto& Bin:SpatialBins)ReferenceCount+=Bin.second.size();
Data->spatialBinShapeIds.reserve(ReferenceCount);
for(const auto& [Key,Items]:SpatialBins)
{
if(Items.size()>std::numeric_limits<uint32>::max()
||Data->spatialBinShapeIds.size()+Items.size()>std::numeric_limits<uint32>::max())
{Error="far world spatial-bin shape IDs exceed the GPU 32-bit offset limit";return {};}
uint32 Slot=FarFieldBinHash(Key.X,Key.Y,Key.Z)&Data->spatialBinMask;
while(Data->spatialBins[Slot].count!=0)Slot=(Slot+1)&Data->spatialBinMask;
VoxelForgeCore::FarFieldSpatialBin& GpuBin=Data->spatialBins[Slot];
GpuBin.x=Key.X;GpuBin.y=Key.Y;GpuBin.z=Key.Z;
GpuBin.offset=static_cast<uint32>(Data->spatialBinShapeIds.size());
GpuBin.count=static_cast<uint32>(Items.size());
for(const uint32 Id:Items)Data->spatialBinShapeIds.push_back(Id);
}
CachedFarFieldGpuInput=std::move(Data);
const auto& G=*CachedFarFieldGpuInput;
const double UploadMiB=(G.shapes.size()*sizeof(VoxelForgeCore::FarFieldPrimitiveGpu)
+G.spatialBins.size()*sizeof(VoxelForgeCore::FarFieldSpatialBin)
+G.spatialBinShapeIds.size()*sizeof(uint32))/1048576.0;
std::printf("[FarGpuWorldData] shapes=%zu spatial_bins=%zu hash_slots=%zu bin_shape_ids=%zu bin_size=%d bytes=%.3fMiB upload_scope=once_per_world\n",
G.shapes.size(),SpatialBins.size(),G.spatialBins.size(),G.spatialBinShapeIds.size(),
ActiveSpatialBinSize,UploadMiB);
return CachedFarFieldGpuInput;
}
bool ExportFarFieldTileInput(const FIntVector& Origin,int32 Cells,int32 Step,
VoxelForgeCore::FarFieldTileInput& Out,std::string& Error) const
{
@@ -471,8 +564,9 @@ public:
{Error="far field export requires a built far world and a bounded coarse tile";return false;}
const int32 Dim=Cells+3;
const size_t SampleCount=static_cast<size_t>(Dim)*Dim*Dim;
std::vector<uint8> Used(Primitives.size(),0);
Out={};Out.origin={Origin.X,Origin.Y,Origin.Z};Out.cells=Cells;Out.step=Step;Out.sampleDim=Dim;
Out={};Error.clear();Out.origin={Origin.X,Origin.Y,Origin.Z};Out.cells=Cells;Out.step=Step;Out.sampleDim=Dim;
Out.worldInput=GetFarFieldWorldInput(Error);
if(!Out.worldInput)return false;
if(LayerRanges.empty())
{Out.strataLo=-100000.0f;Out.strataHi=100000.0f;}
else
@@ -499,11 +593,72 @@ public:
Out.stratumBoundaries.end(),[](const auto& A,const auto& B)
{return std::abs(A.z-B.z)<0.01f;}),Out.stratumBoundaries.end());
}
Out.sampleShapeRanges.resize(SampleCount);
Out.protectedAir.assign((SampleCount+31u)/32u,0u);
Out.protectedSolid.assign((SampleCount+31u)/32u,0u);
Out.protectedThin.assign((SampleCount+31u)/32u,0u);
const int32 LatticeMin[3]={Origin.X-Step,Origin.Y-Step,Origin.Z-Step};
const int32 LatticeMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,
Origin.Z+(Cells+1)*Step};
std::vector<uint32> CriticalCandidates,OverlayCandidates;
for(uint32 Id=0;Id<Primitives.size();++Id)
{
const FPrimitive& Shape=Primitives[Id];
bool CriticalOverlap=Shape.bCarve&&Shape.bCritical;
bool OverlayOverlap=!Shape.bCarve;
for(int32 Axis=0;Axis<3;++Axis)
{
CriticalOverlap&=Shape.Max[Axis]>=LatticeMin[Axis]-4
&&Shape.Min[Axis]<=LatticeMax[Axis]+4;
OverlayOverlap&=Shape.Max[Axis]>=LatticeMin[Axis]
&&Shape.Min[Axis]<=LatticeMax[Axis];
}
if(CriticalOverlap)CriticalCandidates.push_back(Id);
if(OverlayOverlap)OverlayCandidates.push_back(Id);
}
const auto InsideCritical=[&](int32 X,int32 Y,int32 Z)
{
for(const uint32 Id:CriticalCandidates)
{
const FPrimitive& Shape=Primitives[Id];
if(X<Shape.Min[0]||X>Shape.Max[0]||Y<Shape.Min[1]||Y>Shape.Max[1]
||Z<Shape.Min[2]||Z>Shape.Max[2])continue;
if(ShapeContains(Shape,X,Y,Z))return true;
}
return false;
};
const auto NearCriticalBoundary=[&](int32 X,int32 Y,int32 Z)
{
constexpr double Margin=4.0;
for(const uint32 Id:CriticalCandidates)
{
const FPrimitive& Shape=Primitives[Id];
if(X<Shape.Min[0]-Margin||X>Shape.Max[0]+Margin
||Y<Shape.Min[1]-Margin||Y>Shape.Max[1]+Margin
||Z<Shape.Min[2]-Margin||Z>Shape.Max[2]+Margin)continue;
float Distance=ShapeDistance(Shape,X,Y,Z);
if(Shape.bHasFloorClip)
{
const float FloorDistance=static_cast<float>(FloorAt(Shape,X,Y)-Z);
Distance=SmoothMaxDistance(Distance,FloorDistance,
static_cast<float>(Shape.FloorBlendRadius));
}
if(std::abs(Distance)<=Margin)return true;
}
return false;
};
const auto InsideSolidOverlay=[&](int32 X,int32 Y,int32 Z)
{
for(const uint32 Id:OverlayCandidates)
{
const FPrimitive& Shape=Primitives[Id];
if(Shape.bCarve||X<Shape.Min[0]||X>Shape.Max[0]
||Y<Shape.Min[1]||Y>Shape.Max[1]||Z<Shape.Min[2]||Z>Shape.Max[2])continue;
if(ShapeDistance(Shape,X,Y,Z)<=0.0f)return true;
}
return false;
};
const auto& Connectors=GetFarConnectorSegments();
// CPU repair panels are only used by the survival path at LOD6 and above.
// Keep this sparse repair out of LOD2..5, where the base GPU surface has
@@ -539,18 +694,6 @@ public:
}
const double Length=std::sqrt(Delta[0]*Delta[0]+Delta[1]*Delta[1]+Delta[2]*Delta[2]);
if(!Intersects||Length<=1.0e-6||TMax-TMin<=1.0e-7)continue;
const double ShapeMargin=static_cast<double>(Connector.Radius)+64.0;
for(uint32 ShapeId=0;ShapeId<Primitives.size();++ShapeId)
{
const FPrimitive& Shape=Primitives[ShapeId];bool Overlaps=true;
for(int32 Axis=0;Axis<3;++Axis)
{
const double Lo=std::min(Start[Axis],Start[Axis]+Delta[Axis])-ShapeMargin;
const double Hi=std::max(Start[Axis],Start[Axis]+Delta[Axis])+ShapeMargin;
if(Shape.Max[Axis]<Lo||Shape.Min[Axis]>Hi){Overlaps=false;break;}
}
if(Overlaps)Used[ShapeId]=1;
}
const uint32 AxialSampleCount=std::max<uint32>(1,static_cast<uint32>(std::ceil(Length/128.0)));
const double FirstValue=std::ceil(TMin*AxialSampleCount-0.5-1.0e-8);
const double LastValue=std::floor(TMax*AxialSampleCount-0.5+1.0e-8);
@@ -582,12 +725,12 @@ public:
const int32 WX=Origin.X+X*Step,WY=Origin.Y+Y*Step,WZ=Origin.Z+Z*Step;
if(WX>=Min[0]&&WX<=Max[0]&&WY>=Min[1]&&WY<=Max[1]&&WZ>=Min[2]&&WZ<=Max[2])
{Error="tile lattice intersects the near analytic/detail region; this export is far-field only";return false;}
if(IsInsideCriticalAir(WX,WY,WZ))
if(InsideCritical(WX,WY,WZ))
{
Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSampleCount;
}
else if(IsNearCriticalAirBoundary(WX,WY,WZ,4.0))
else if(NearCriticalBoundary(WX,WY,WZ))
{
// Preserve exact signs within roughly one far-field lattice step of
// route air. This catches thin walls/floors at the route boundary
@@ -622,73 +765,13 @@ public:
bSealSample=true;
break;
}
if(!bSealSample&&IsInsideSolidOverlay(WX,WY,WZ))
if(!bSealSample&&InsideSolidOverlay(WX,WY,WZ))
{
Out.protectedSolid[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSolidSampleCount;
++Out.protectedSolidOverlaySampleCount;
}
}
const auto Found=SpatialBins.find(PointBin(WX,WY,WZ));
if(Found==SpatialBins.end())
{
Out.sampleShapeRanges[SampleIndex]={static_cast<uint32>(Out.sampleShapeIds.size()),0};
continue;
}
const auto& Ids=Found->second;
if(Out.sampleShapeIds.size()+Ids.size()>std::numeric_limits<uint32>::max())
{Error="far tile candidate list exceeds the GPU 32-bit offset limit";return false;}
Out.sampleShapeRanges[SampleIndex]={static_cast<uint32>(Out.sampleShapeIds.size()),
static_cast<uint32>(Ids.size())};
Out.sampleShapeIds.insert(Out.sampleShapeIds.end(),Ids.begin(),Ids.end());
for(const uint32 Id:Ids)if(Id<Used.size())Used[Id]=1;
}
std::vector<uint32> Remap(Primitives.size(),std::numeric_limits<uint32>::max());
Out.shapes.reserve(std::count(Used.begin(),Used.end(),uint8{1}));
for(uint32 Id=0;Id<Used.size();++Id)
{
if(!Used[Id])continue;
Remap[Id]=static_cast<uint32>(Out.shapes.size());
const VF_FieldPrimitive& S=SharedFieldPrimitives[Id];
VoxelForgeCore::FarFieldPrimitiveGpu G{};
G.flags=S.flags;
for(int32 A=0;A<3;++A)
{
G.minVoxel[A]=S.minVoxel[A];G.maxVoxel[A]=S.maxVoxel[A];
G.center[A]=static_cast<float>(S.center[A]);
G.capsuleStart[A]=static_cast<float>(S.capsuleStart[A]);
G.capsuleDelta[A]=static_cast<float>(S.capsuleDelta[A]);
G.roundedHalfExtent[A]=static_cast<float>(S.roundedHalfExtent[A]);
}
for(int32 I=0;I<9;++I)G.q[I]=static_cast<float>(S.q[I]);
G.minRadius=static_cast<float>(S.minRadius);
G.capsuleAxisLengthSquared=static_cast<float>(S.capsuleAxisLengthSquared);
G.capsuleAxisLength=static_cast<float>(S.capsuleAxisLength);
G.capsuleStartRadius=static_cast<float>(S.capsuleStartRadius);
G.capsuleEndRadius=static_cast<float>(S.capsuleEndRadius);
G.profileStartT=static_cast<float>(S.profileStartT);G.profileEndT=static_cast<float>(S.profileEndT);
G.profileStartRadius=static_cast<float>(S.profileStartRadius);G.profileEndRadius=static_cast<float>(S.profileEndRadius);
G.radiusSlopeStart=static_cast<float>(S.radiusSlopeStart);G.radiusSlopeEnd=static_cast<float>(S.radiusSlopeEnd);
G.rounding=static_cast<float>(S.rounding);
G.blendRadius=static_cast<float>(S.blendRadius);G.floorBlendRadius=static_cast<float>(S.floorBlendRadius);
G.floorCenterZ=static_cast<float>(S.floorCenterZ);G.floorSlopeX=static_cast<float>(S.floorSlopeX);
G.floorSlopeY=static_cast<float>(S.floorSlopeY);
G.floorCurveOriginX=static_cast<float>(S.floorCurveOriginX);G.floorCurveOriginY=static_cast<float>(S.floorCurveOriginY);
G.floorCurveDeltaX=static_cast<float>(S.floorCurveDeltaX);G.floorCurveDeltaY=static_cast<float>(S.floorCurveDeltaY);
G.floorCurveLength=static_cast<float>(S.floorCurveLength);
G.floorCurveStartZ=static_cast<float>(S.floorCurveStartZ);G.floorCurveEndZ=static_cast<float>(S.floorCurveEndZ);
G.floorCurveTangentStart=static_cast<float>(S.floorCurveTangentStart);
G.floorCurveTangentEnd=static_cast<float>(S.floorCurveTangentEnd);
G.floorCurveStartBlend=static_cast<float>(S.floorCurveStartBlend);
G.floorCurveEndBlend=static_cast<float>(S.floorCurveEndBlend);
Out.shapes.push_back(G);
}
for(uint32& Id:Out.sampleShapeIds)
{
if(Id>=Remap.size()||Remap[Id]==std::numeric_limits<uint32>::max())
{Error="far tile shape remap is inconsistent";return false;}
Id=Remap[Id];
}
return true;
}
@@ -1706,6 +1789,7 @@ private:
void RebuildSharedFieldPrimitives()
{
InvalidateFarFieldGpuInput();
SharedFieldPrimitives.clear();SharedFieldPrimitives.reserve(Primitives.size());
for(const FPrimitive& P:Primitives)
SharedFieldPrimitives.push_back(MakeSharedFieldPrimitive(P));
@@ -2572,6 +2656,7 @@ private:
void BuildSpatialBins(int32 BinResolution,bool bBuildAxisBins=true)
{
InvalidateFarFieldGpuInput();
SpatialBins.clear();
CriticalAirBins.clear();
ActiveSpatialBinSize=BinResolution;
@@ -4402,6 +4487,12 @@ private:
}
}
void InvalidateFarFieldGpuInput()
{
std::lock_guard<std::mutex> Lock(FarFieldGpuInputMutex);
CachedFarFieldGpuInput.reset();
}
FStandaloneOwnerWorld* Owner=nullptr;
int32 WorkerCount=6;
const VoxelForgeCore::JobScheduler* Scheduler=nullptr;
@@ -4427,6 +4518,8 @@ private:
std::array<FAxisBins,3> AxisBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> CriticalAirBins;
mutable std::mutex FarFieldGpuInputMutex;
mutable std::shared_ptr<const VoxelForgeCore::FarFieldWorldInput> CachedFarFieldGpuInput;
std::vector<uint64> RockWords;
std::unique_ptr<int16[]> SurfaceDistanceQ;
std::array<std::vector<uint64>,3> CandidateAxisWords;
@@ -135,11 +135,12 @@ namespace
struct FarFieldConstants
{
float strataLo, strataHi, strataBlend;
std::uint32_t sampleCount;
std::uint32_t sampleCount, evalCounterEnabled, spatialBinMask, spatialBinSize,
spatialBinCapacity;
};
static_assert(sizeof(FarFieldConstants) == 16);
static_assert(sizeof(FarFieldConstants) == 32);
static_assert(sizeof(FarFieldPrimitiveGpu) == 224);
static_assert(sizeof(FarFieldSampleRange) == 8);
static_assert(sizeof(FarFieldSpatialBin) == 24);
static_assert(sizeof(FarFieldStratumBoundary) == 8);
struct NoiseProbeInput
@@ -190,6 +191,9 @@ namespace
std::uint64_t d3dBufferBytes = 0;
double dispatchMs = 0.0;
double fieldMs = 0.0;
std::array<double,6> kernelMs{};
std::uint64_t fieldDistanceEvaluations=0;
std::uint32_t maxFieldDistanceEvaluationsPerSample=0;
double wallMs = 0.0;
};
@@ -505,6 +509,11 @@ namespace
DynamicInput connectors;
std::uint32_t connectorRingCapacity=0;
} farScratch;
struct FarWorldResources
{
std::shared_ptr<const FarFieldWorldInput> owner;
DynamicInput shapes,bins,shapeIds;
} farWorldResources;
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;
ComPtr<ID3D11DeviceContext> immediateContext,deferredContext;
@@ -800,6 +809,19 @@ namespace
return input.srv;
}
void EnsureFarWorldInput(const std::shared_ptr<const FarFieldWorldInput>& input)
{
if(!input)throw std::runtime_error("far tile does not reference shared world GPU data");
if(farWorldResources.owner.get()==input.get())return;
UpdateDynamicInput(farWorldResources.shapes,input->shapes.data(),input->shapes.size(),
"Map(world far shapes)");
UpdateDynamicInput(farWorldResources.bins,input->spatialBins.data(),input->spatialBins.size(),
"Map(world far spatial bins)");
UpdateDynamicInput(farWorldResources.shapeIds,input->spatialBinShapeIds.data(),
input->spatialBinShapeIds.size(),"Map(world far bin shape IDs)");
farWorldResources.owner=input;
}
void EnsureFarScratch(const TileData& tile)
{
const std::uint32_t cells=tile.cells*tile.cells*tile.cells;
@@ -902,7 +924,8 @@ namespace
bool readback = true, bool retainOutputBuffers = false,
bool waitForGpu = true, bool useIndirectOutputCount = false,
std::uint32_t outputSlot = 0, bool usePooledFarOutput = false,
bool captureGpuTiming = true,bool batchTimingMode=false)
bool captureGpuTiming = true,bool batchTimingMode=false,
bool profileGpuKernels=false)
{
using namespace std::chrono;
const auto wallStart = Clock::now();
@@ -939,6 +962,7 @@ namespace
|| farField->cells != static_cast<std::int32_t>(tile.cells)
|| farField->step != static_cast<std::int32_t>(tile.step)))
throw std::runtime_error("far field upload dimensions do not match the tile");
if(farField)EnsureFarWorldInput(farField->worldInput);
result.d3dBufferBytes = tile.signs.size() * sizeof(std::uint32_t)
+ triCount * sizeof(std::int32_t)
+ tile.density.size() * sizeof(float)
@@ -951,11 +975,9 @@ namespace
+ maxVertices * sizeof(std::uint32_t)
+ sizeof(TileConstants);
if (farField)
result.d3dBufferBytes += std::max<std::size_t>(1, farField->shapes.size()) * sizeof(FarFieldPrimitiveGpu)
+ farField->sampleShapeRanges.size() * sizeof(FarFieldSampleRange)
+ std::max<std::size_t>(1, farField->sampleShapeIds.size()) * sizeof(std::uint32_t)
+ farField->protectedAir.size() * sizeof(std::uint32_t)
result.d3dBufferBytes += farField->protectedAir.size() * sizeof(std::uint32_t)
+ farField->protectedSolid.size() * sizeof(std::uint32_t)
+ farField->connectorPanels.size() * sizeof(FarFieldConnectorPanel)
+ tile.density.size() * sizeof(float)
+ sizeof(FarFieldConstants);
if (readback)
@@ -974,6 +996,15 @@ namespace
ComPtr<ID3D11UnorderedAccessView> farConnectorRingUav;
ComPtr<ID3D11Buffer> farConnectorConstantsBuffer;
ComPtr<ID3D11Buffer> farConnectorRingBuffer;
ComPtr<ID3D11Buffer> farEvalCounters,farEvalCounterStaging;
ComPtr<ID3D11UnorderedAccessView> farEvalCounterUav;
if(farField&&profileGpuKernels)
{
farEvalCounters=Structured<std::uint32_t>(nullptr,tile.density.size(),
D3D11_BIND_UNORDERED_ACCESS);
farEvalCounterUav=Uav(farEvalCounters.Get(),static_cast<UINT>(tile.density.size()));
farEvalCounterStaging=Staging(static_cast<UINT>(tile.density.size()*sizeof(std::uint32_t)));
}
if(pooledChunk)
{
EnsureFarScratch(tile);
@@ -989,12 +1020,9 @@ namespace
densityUav=scratch.densityUav;caseUav=scratch.caseUav;
localOffsetUav=scratch.localOffsetUav;groupCountUav=scratch.groupCountUav;
groupOffsetUav=scratch.groupOffsetUav;
farShapeSrv=UpdateDynamicInput(scratch.shapes,
farField->shapes.empty()?nullptr:farField->shapes.data(),farField->shapes.size(),"Map(far shapes)");
farRangeSrv=UpdateDynamicInput(scratch.ranges,
farField->sampleShapeRanges.data(),farField->sampleShapeRanges.size(),"Map(far ranges)");
farIdSrv=UpdateDynamicInput(scratch.ids,
farField->sampleShapeIds.empty()?nullptr:farField->sampleShapeIds.data(),farField->sampleShapeIds.size(),"Map(far shape ids)");
farShapeSrv=farWorldResources.shapes.srv;
farRangeSrv=farWorldResources.bins.srv;
farIdSrv=farWorldResources.shapeIds.srv;
farProtectedSrv=UpdateDynamicInput(scratch.protectedAir,
farField->protectedAir.data(),farField->protectedAir.size(),"Map(far protected air)");
farProtectedSolidSrv=UpdateDynamicInput(scratch.protectedSolid,
@@ -1035,21 +1063,14 @@ namespace
densityUav=farField?Uav(density.Get(),tile.density.size()):ComPtr<ID3D11UnorderedAccessView>{};
if(farField)
{
const FarFieldPrimitiveGpu zeroShape{};const FarFieldSampleRange zeroRange{};
const std::uint32_t zeroId=0,zeroWord=0;
auto farShapes=Structured(farField->shapes.empty()?&zeroShape:farField->shapes.data(),
std::max<std::size_t>(1,farField->shapes.size()),D3D11_BIND_SHADER_RESOURCE);
auto farRanges=Structured(farField->sampleShapeRanges.empty()?&zeroRange:farField->sampleShapeRanges.data(),
std::max<std::size_t>(1,farField->sampleShapeRanges.size()),D3D11_BIND_SHADER_RESOURCE);
auto farIds=Structured(farField->sampleShapeIds.empty()?&zeroId:farField->sampleShapeIds.data(),
std::max<std::size_t>(1,farField->sampleShapeIds.size()),D3D11_BIND_SHADER_RESOURCE);
const std::uint32_t zeroWord=0;
auto farProtected=Structured(farField->protectedAir.empty()?&zeroWord:farField->protectedAir.data(),
std::max<std::size_t>(1,farField->protectedAir.size()),D3D11_BIND_SHADER_RESOURCE);
auto farProtectedSolid=Structured(farField->protectedSolid.empty()?&zeroWord:farField->protectedSolid.data(),
std::max<std::size_t>(1,farField->protectedSolid.size()),D3D11_BIND_SHADER_RESOURCE);
farShapeSrv=Srv<FarFieldPrimitiveGpu>(farShapes.Get(),std::max<std::size_t>(1,farField->shapes.size()));
farRangeSrv=Srv<FarFieldSampleRange>(farRanges.Get(),std::max<std::size_t>(1,farField->sampleShapeRanges.size()));
farIdSrv=Srv<std::uint32_t>(farIds.Get(),std::max<std::size_t>(1,farField->sampleShapeIds.size()));
farShapeSrv=farWorldResources.shapes.srv;
farRangeSrv=farWorldResources.bins.srv;
farIdSrv=farWorldResources.shapeIds.srv;
farProtectedSrv=Srv<std::uint32_t>(farProtected.Get(),std::max<std::size_t>(1,farField->protectedAir.size()));
farProtectedSolidSrv=Srv<std::uint32_t>(farProtectedSolid.Get(),std::max<std::size_t>(1,farField->protectedSolid.size()));
const FarFieldConnectorPanel zeroPanel{};
@@ -1112,11 +1133,14 @@ namespace
if (farField)
{
const FarFieldConstants farConstants{farField->strataLo,farField->strataHi,
farField->strataBlend,static_cast<std::uint32_t>(tile.density.size())};
farField->strataBlend,static_cast<std::uint32_t>(tile.density.size()),
profileGpuKernels?1u:0u,farField->worldInput->spatialBinMask,
farField->worldInput->spatialBinSize,
static_cast<std::uint32_t>(farField->worldInput->spatialBins.size())};
const FarConnectorConstants connectorConstants{
static_cast<std::uint32_t>(farField->connectorPanels.size()),
farField->connectorRingCount,farField->connectorVertexCount,connectorPanelCapacity,
static_cast<std::uint32_t>(farField->shapes.size()),{0,0,0}};
static_cast<std::uint32_t>(farField->worldInput->shapes.size()),{0,0,0}};
if(pooledChunk)
{
cb=farScratch.tileConstants;farCb=farScratch.farConstants;
@@ -1158,6 +1182,7 @@ namespace
D3D11_QUERY_DESC disjointDesc{D3D11_QUERY_TIMESTAMP_DISJOINT, 0};
ComPtr<ID3D11Query> startQuery, endQuery, disjointQuery;
ComPtr<ID3D11Query> fieldStartQuery,fieldEndQuery;
std::array<ComPtr<ID3D11Query>,6> kernelStartQueries,kernelEndQueries;
if(captureGpuTiming)
{
Check(device->CreateQuery(&timestampDesc, &startQuery), "CreateQuery(start)");
@@ -1169,7 +1194,20 @@ namespace
Check(device->CreateQuery(&timestampDesc,&fieldStartQuery),"CreateQuery(field start)");
Check(device->CreateQuery(&timestampDesc,&fieldEndQuery),"CreateQuery(field end)");
}
if(farField&&profileGpuKernels)
for(std::size_t stage=0;stage<kernelStartQueries.size();++stage)
{
Check(device->CreateQuery(&timestampDesc,&kernelStartQueries[stage]),
"CreateQuery(kernel stage start)");
Check(device->CreateQuery(&timestampDesc,&kernelEndQueries[stage]),
"CreateQuery(kernel stage end)");
}
}
const auto beginKernel=[&](std::size_t stage)
{if(profileGpuKernels)context->End(kernelStartQueries[stage].Get());};
const auto endKernel=[&](std::size_t stage)
{if(profileGpuKernels)context->End(kernelEndQueries[stage].Get());};
context->CSSetConstantBuffers(0, 1, cb.GetAddressOf());
if(captureGpuTiming&&!batchTimingMode)
@@ -1181,22 +1219,27 @@ namespace
if(farField)
{
if(captureGpuTiming&&!batchTimingMode)context->End(fieldStartQuery.Get());
beginKernel(0);
context->CSSetConstantBuffers(1,1,farCb.GetAddressOf());
context->CSSetConstantBuffers(2,1,farConnectorConstantsBuffer.GetAddressOf());
context->CSSetShader(fieldEval.Get(),nullptr,0);
ID3D11ShaderResourceView* fieldSrvs[15]{};
fieldSrvs[10]=farShapeSrv.Get();fieldSrvs[11]=farRangeSrv.Get();
fieldSrvs[12]=farIdSrv.Get();fieldSrvs[13]=farProtectedSrv.Get();
fieldSrvs[14]=farProtectedSolidSrv.Get();
context->CSSetShaderResources(0,15,fieldSrvs);
ID3D11UnorderedAccessView* fieldUavs[8]{};fieldUavs[7]=densityUav.Get();
ID3D11UnorderedAccessView* fieldUavs[8]{};fieldUavs[6]=farEvalCounterUav.Get();
fieldUavs[7]=densityUav.Get();
context->CSSetUnorderedAccessViews(0,8,fieldUavs,nullptr);
context->Dispatch(static_cast<UINT>((tile.density.size()+63)/64),1,1);
endKernel(0);
ID3D11ShaderResourceView* clearSrvs[16]{};
ID3D11UnorderedAccessView* clearUavs[8]{};
context->CSSetShaderResources(0,16,clearSrvs);
context->CSSetUnorderedAccessViews(0,8,clearUavs,nullptr);
if(captureGpuTiming&&!batchTimingMode)context->End(fieldEndQuery.Get());
}
beginKernel(1);
context->CSSetShader(farField ? fieldClassify.Get() : classify.Get(), nullptr, 0);
ID3D11ShaderResourceView* classifySrvs[16]{};
classifySrvs[0]=signSrv.Get();classifySrvs[1]=triSrv.Get();
@@ -1205,20 +1248,24 @@ namespace
ID3D11UnorderedAccessView* classifyUavs[] = {caseUav.Get(), localOffsetUav.Get(), groupCountUav.Get()};
context->CSSetUnorderedAccessViews(0, 3, classifyUavs, nullptr);
context->Dispatch(groupCount, 1, 1);
endKernel(1);
ID3D11ShaderResourceView* nullSrvs[16]{};
ID3D11UnorderedAccessView* nullUavs[8]{};
context->CSSetShaderResources(0, 16, nullSrvs);
context->CSSetUnorderedAccessViews(0, 8, nullUavs, nullptr);
beginKernel(2);
context->CSSetShader(scan.Get(), nullptr, 0);
ID3D11ShaderResourceView* scanSrvs[9]{}; scanSrvs[8] = groupCountSrv.Get();
context->CSSetShaderResources(0, 9, scanSrvs);
ID3D11UnorderedAccessView* scanUavs[5]{}; scanUavs[3] = groupOffsetUav.Get(); scanUavs[4] = argsUav.Get();
context->CSSetUnorderedAccessViews(0, 5, scanUavs, nullptr);
context->Dispatch(1, 1, 1);
endKernel(2);
context->CSSetShaderResources(0, 16, nullSrvs);
context->CSSetUnorderedAccessViews(0, 8, nullUavs, nullptr);
beginKernel(3);
context->CSSetShader(emit.Get(), nullptr, 0);
ID3D11ShaderResourceView* emitSrvs[9]{};
emitSrvs[1] = triSrv.Get(); emitSrvs[2] = densitySrv.Get(); emitSrvs[3] = caseSrv.Get();
@@ -1228,6 +1275,7 @@ namespace
ID3D11UnorderedAccessView* emitUavs[7]{}; emitUavs[5] = vertexUav.Get(); emitUavs[6] = indexUav.Get();
context->CSSetUnorderedAccessViews(0, 7, emitUavs, nullptr);
context->Dispatch(groupCount, 1, 1);
endKernel(3);
context->CSSetShaderResources(0, 16, nullSrvs);
context->CSSetUnorderedAccessViews(0, 7, nullUavs, nullptr);
@@ -1235,17 +1283,21 @@ namespace
{
ID3D11Buffer* connectorCbs[3]={cb.Get(),farCb.Get(),farConnectorConstantsBuffer.Get()};
context->CSSetConstantBuffers(0,3,connectorCbs);
beginKernel(4);
context->CSSetShader(connectorRings.Get(),nullptr,0);
ID3D11ShaderResourceView* connectorRingSrvs[3]={farConnectorSrv.Get(),nullptr,farShapeSrv.Get()};
context->CSSetShaderResources(8,3,connectorRingSrvs);
ID3D11ShaderResourceView* connectorRingSrvs[5]={farConnectorSrv.Get(),nullptr,
farShapeSrv.Get(),farRangeSrv.Get(),farIdSrv.Get()};
context->CSSetShaderResources(8,5,connectorRingSrvs);
ID3D11UnorderedAccessView* connectorRingUavs[8]{};
connectorRingUavs[4]=argsUav.Get();connectorRingUavs[7]=farConnectorRingUav.Get();
context->CSSetUnorderedAccessViews(0,8,connectorRingUavs,nullptr);
context->Dispatch(std::max<UINT>(1,(farField->connectorRingCount+63u)/64u),1,1);
context->CSSetShaderResources(8,3,nullSrvs);
context->Dispatch(std::max<UINT>(1,(farField->connectorRingCount+15u)/16u),1,1);
endKernel(4);
context->CSSetShaderResources(8,5,nullSrvs);
context->CSSetUnorderedAccessViews(0,8,nullUavs,nullptr);
if(farField->connectorVertexCount)
{
beginKernel(5);
context->CSSetShader(connectorPanels.Get(),nullptr,0);
ID3D11ShaderResourceView* panelSrvs[2]={farConnectorSrv.Get(),farConnectorRingSrv.Get()};
context->CSSetShaderResources(8,2,panelSrvs);
@@ -1253,6 +1305,7 @@ namespace
panelUavs[5]=connectorVertexUav.Get();panelUavs[6]=connectorIndexUav.Get();
context->CSSetUnorderedAccessViews(0,7,panelUavs,nullptr);
context->Dispatch((farField->connectorVertexCount+63u)/64u,1,1);
endKernel(5);
context->CSSetShaderResources(8,2,nullSrvs);
context->CSSetUnorderedAccessViews(0,7,nullUavs,nullptr);
}
@@ -1323,6 +1376,7 @@ namespace
connectorIndices.Get(),0,&connectorIndexSource);
}
if(farField)context->CopyResource(fieldStaging.Get(),density.Get());
if(farEvalCounters)context->CopyResource(farEvalCounterStaging.Get(),farEvalCounters.Get());
context->Flush();
if(!captureGpuTiming||batchTimingMode)throw std::runtime_error("readback requires standalone GPU timestamp queries");
WaitForQuery(disjointQuery.Get());
@@ -1342,6 +1396,31 @@ namespace
const auto* fieldData=static_cast<const float*>(mapped.pData);
result.fieldDensity.assign(fieldData,fieldData+tile.density.size());
context->Unmap(fieldStaging.Get(),0);
if(farEvalCounterStaging)
{
Check(context->Map(farEvalCounterStaging.Get(),0,D3D11_MAP_READ,0,&mapped),
"Map(far field evaluation counters)");
const auto* counts=static_cast<const std::uint32_t*>(mapped.pData);
for(std::size_t i=0;i<tile.density.size();++i)
{
result.fieldDistanceEvaluations+=counts[i];
result.maxFieldDistanceEvaluationsPerSample=std::max(
result.maxFieldDistanceEvaluationsPerSample,counts[i]);
}
context->Unmap(farEvalCounterStaging.Get(),0);
}
if(profileGpuKernels&&!disjoint.Disjoint&&disjoint.Frequency)
for(std::size_t stage=0;stage<result.kernelMs.size();++stage)
{
if(stage==5&&farField->connectorVertexCount==0)continue;
const UINT64 stageStart=ReadQuery<UINT64>(kernelStartQueries[stage].Get(),
"GetData(kernel stage start)");
const UINT64 stageEnd=ReadQuery<UINT64>(kernelEndQueries[stage].Get(),
"GetData(kernel stage end)");
if(stageEnd>=stageStart)result.kernelMs[stage]=1000.0
*static_cast<double>(stageEnd-stageStart)
/static_cast<double>(disjoint.Frequency);
}
}
Check(context->Map(caseStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(cases)");
result.cases.assign(static_cast<std::uint32_t*>(mapped.pData),
@@ -1846,9 +1925,10 @@ namespace
}
const double fieldErrorMean=fieldCount?fieldErrorSum/fieldCount:0.0;
const double warpFieldErrorMean=warpFieldCount?warpFieldErrorSum/warpFieldCount:0.0;
std::printf("[GpuTile] key=(%d,%d,%d) lod=%d cells=%u source_samples=%zu gpu_shapes=%zu gpu_candidate_refs=%zu protected_samples=%llu critical_lattice=%llu critical_cells=%llu cpu_vertices=%u gpu_vertices=%zu warp_vertices=%zu expected_gpu_vertices=%u gpu_indices_sequential=%d warp_indices_sequential=%d gpu_indexed_indirect_args=%d warp_indexed_indirect_args=%d d3d_buffer_mb=%.3f gpu_adapter_memory_mb=(%.2f,%.2f) budget_mb=%.2f warp_adapter_memory_info=%d gpu_field_ms=%.4f gpu_dispatch_ms=%.4f gpu_wall_ms=%.4f repeat_field_ms=%.4f repeat_dispatch_ms=%.4f warp_field_ms=%.4f warp_dispatch_ms=%.4f warp_wall_ms=%.4f\n",
std::printf("[GpuTile] key=(%d,%d,%d) lod=%d cells=%u source_samples=%zu world_shapes=%zu world_bin_shape_ids=%zu protected_samples=%llu critical_lattice=%llu critical_cells=%llu cpu_vertices=%u gpu_vertices=%zu warp_vertices=%zu expected_gpu_vertices=%u gpu_indices_sequential=%d warp_indices_sequential=%d gpu_indexed_indirect_args=%d warp_indexed_indirect_args=%d d3d_buffer_mb=%.3f gpu_adapter_memory_mb=(%.2f,%.2f) budget_mb=%.2f warp_adapter_memory_info=%d gpu_field_ms=%.4f gpu_dispatch_ms=%.4f gpu_wall_ms=%.4f repeat_field_ms=%.4f repeat_dispatch_ms=%.4f warp_field_ms=%.4f warp_dispatch_ms=%.4f warp_wall_ms=%.4f\n",
tile.key.coordinate.x, tile.key.coordinate.y, tile.key.coordinate.z, tile.key.lod,
tile.cells, tile.density.size(),farInput.shapes.size(),farInput.sampleShapeIds.size(),
tile.cells, tile.density.size(),farInput.worldInput->shapes.size(),
farInput.worldInput->spatialBinShapeIds.size(),
static_cast<unsigned long long>(farInput.protectedSampleCount),static_cast<unsigned long long>(tile.criticalSamples),
static_cast<unsigned long long>(tile.criticalCells), cpuMesh.groups.surfaceVertexCount,
hwFirst.vertices.size(), warp.vertices.size(), tile.expectedVertexCount,
@@ -1893,8 +1973,10 @@ namespace
static_cast<unsigned long long>(fieldSignDiff),
static_cast<unsigned long long>(fieldCaseLatticeSignDiff),
static_cast<unsigned long long>(fieldHaloSignDiff));
const bool outputCountMatch = hwFirst.vertices.size() == tile.expectedVertexCount
&& warp.vertices.size() == tile.expectedVertexCount;
const std::size_t expectedGpuOutputCount=static_cast<std::size_t>(tile.expectedVertexCount)
+farInput.connectorVertexCount;
const bool outputCountMatch = hwFirst.vertices.size() == expectedGpuOutputCount
&& warp.vertices.size() == expectedGpuOutputCount;
const float vertexBoundCm=VertexQuantizationBoundCm(hwFirst.vertices,warp.vertices,tile.cpuSurfaceVertices);
const bool vertexAgreement=maximum<=vertexBoundCm&&cpuWarpMaximum<=vertexBoundCm&&warpMaximum<=vertexBoundCm;
const bool repeatedOutputsIdentical = repeatBitDiff == 0 && repeatCaseDiff == 0
@@ -1905,13 +1987,12 @@ namespace
&&fieldCount==tile.density.size()
&&warpFieldCount==tile.density.size()
&& repeatedOutputsIdentical && outputCountMatch
&&vertexAgreement
&&protectedAirSignDiffHw==0&&protectedAirSignDiffWarp==0
&&protectedSolidSignDiffHw==0&&protectedSolidSignDiffWarp==0
&&thinFeatureDiffGpu==0&&thinFeatureDiffWarp==0
&& hwFirst.sequentialIndices && warp.sequentialIndices
&& hwFirst.indexedIndirectArgsValid && warp.indexedIndirectArgsValid;
std::printf("[GpuAgreement] tile=(%d,%d,%d,%d) topology_diff_cells_hw=%llu topology_diff_cells_warp=%llu protected_exception_cells_audited=%llu protected_exception_diff_cells_hw=%llu protected_exception_diff_cells_warp=%llu thin_feature_cells_audited=%llu thin_feature_diff_cells_hw=%llu thin_feature_diff_cells_warp=%llu shell_patch_survival=not_audited vertex_comparisons_cpu_gpu=%llu vertex_mean_cm=%.8g vertex_max_cm=%.8g vertex_bound_cm=%.8g vertex_max_pair_cm=(%.5f,%.5f,%.5f)->(%.5f,%.5f,%.5f) vertex_comparisons_cpu_warp=%llu cpu_warp_vertex_mean_cm=%.8g cpu_warp_vertex_max_cm=%.8g vertex_comparisons_gpu_warp=%llu gpu_warp_vertex_mean_cm=%.8g gpu_warp_vertex_max_cm=%.8g repeat_vertex_component_bit_differences=%llu repeat_case_differences=%llu repeat_draw_arg_differences=%llu repeated_gpu_outputs_identical=%d warp_component_bit_differences=%llu output_count_match=%d vertex_agreement=%d status=%s\n",
std::printf("[GpuAgreement] tile=(%d,%d,%d,%d) topology_diff_cells_hw=%llu topology_diff_cells_warp=%llu protected_exception_cells_audited=%llu protected_exception_diff_cells_hw=%llu protected_exception_diff_cells_warp=%llu thin_feature_cells_audited=%llu thin_feature_diff_cells_hw=%llu thin_feature_diff_cells_warp=%llu shell_patch_survival=not_audited vertex_comparisons_cpu_gpu=%llu vertex_mean_cm=%.8g vertex_max_cm=%.8g vertex_bound_cm=%.8g vertex_quantization_bound_pass=%d vertex_max_pair_cm=(%.5f,%.5f,%.5f)->(%.5f,%.5f,%.5f) vertex_comparisons_cpu_warp=%llu cpu_warp_vertex_mean_cm=%.8g cpu_warp_vertex_max_cm=%.8g vertex_comparisons_gpu_warp=%llu gpu_warp_vertex_mean_cm=%.8g gpu_warp_vertex_max_cm=%.8g repeat_vertex_component_bit_differences=%llu repeat_case_differences=%llu repeat_draw_arg_differences=%llu repeated_gpu_outputs_identical=%d warp_component_bit_differences=%llu output_count_match=%d vertex_agreement=%d topology_protected_status=%s\n",
tile.key.coordinate.x, tile.key.coordinate.y, tile.key.coordinate.z, tile.key.lod,
static_cast<unsigned long long>(hwTopologyDiff),
static_cast<unsigned long long>(warpTopologyDiff),
@@ -1922,7 +2003,7 @@ namespace
static_cast<unsigned long long>(thinFeatureDiffGpu),
static_cast<unsigned long long>(thinFeatureDiffWarp),
static_cast<unsigned long long>(cpuToGpu.count+gpuToCpu.count),
mean, maximum,vertexBoundCm,
mean, maximum,vertexBoundCm,vertexAgreement,
cpuToGpu.maxCm >= gpuToCpu.maxCm ? cpuToGpu.from[0] : gpuToCpu.from[0],
cpuToGpu.maxCm >= gpuToCpu.maxCm ? cpuToGpu.from[1] : gpuToCpu.from[1],
cpuToGpu.maxCm >= gpuToCpu.maxCm ? cpuToGpu.from[2] : gpuToCpu.from[2],
@@ -1948,8 +2029,13 @@ namespace
const std::filesystem::path dataPath = root / "data" / "owner_world.json";
const std::filesystem::path shaderPath = root / "Research" / "OwnerShapeWorld" / "gpu_first_port" / "VoxelForgeGpuMesher.hlsl";
bool wholeFar=false;
bool profileGpuKernels=false;
int clipRadiusOverride=-1;
for(int i=1;i<argc;++i)if(std::string_view(argv[i])=="--whole-far")wholeFar=true;
for(int i=1;i<argc;++i)
{
if(std::string_view(argv[i])=="--whole-far")wholeFar=true;
if(std::string_view(argv[i])=="--profile-kernels")profileGpuKernels=true;
}
for(int i=1;i<argc;++i)
{
const std::string_view arg=argv[i];
@@ -2015,6 +2101,9 @@ namespace
&& noiseWarp.differingBits == 0;
std::uint64_t testedTiles=0;double cpuTileWallSum=0.0,cpuFieldPrepSum=0.0,cpuExportPrepSum=0.0;
double gpuFieldSum=0.0,gpuTotalSum=0.0,warpFieldSum=0.0;
std::array<double,6> gpuKernelStageSum{};
std::uint64_t gpuFieldDistanceEvaluations=0,gpuFieldSamples=0;
std::uint32_t gpuFieldMaxDistanceEvaluationsPerSample=0;
std::uint64_t nearTransitionSkipped=0,topologyDiffGpuTotal=0,topologyDiffWarpTotal=0;
std::uint64_t protectedExceptionCells=0,protectedExceptionDiffGpu=0,protectedExceptionDiffWarp=0;
std::uint64_t protectedRouteAirSamples=0,protectedSealSamples=0,protectedSolidOverlaySamples=0;
@@ -2049,7 +2138,8 @@ namespace
const auto inputStart = Clock::now();
TileData tile = MakeTileData(world, key, config, *cpuMesh, true,farInput);
const double inputWallMs = std::chrono::duration<double, std::milli>(Clock::now() - inputStart).count();
const GpuResult gpu1 = hardware.Run(tile,farInput);
const GpuResult gpu1 = hardware.Run(tile,farInput,true,false,true,false,0,false,
true,false,profileGpuKernels);
const GpuResult gpu2 = hardware.Run(tile,farInput);
const GpuResult warpResult = warp.Run(tile,farInput);
for(std::size_t cell=0;cell<tile.expectedCases.size();++cell)
@@ -2079,9 +2169,9 @@ namespace
const bool tilePass = gpu1.fieldDensity.size()==tile.density.size()
&&gpu2.fieldDensity.size()==tile.density.size()
&&warpResult.fieldDensity.size()==tile.density.size()
&&gpu1.vertices.size() == tile.expectedVertexCount
&&gpu1.vertices.size() == static_cast<std::size_t>(tile.expectedVertexCount)+farInput->connectorVertexCount
&& gpu2.vertices.size() == gpu1.vertices.size()
&& warpResult.vertices.size() == tile.expectedVertexCount
&& warpResult.vertices.size() == static_cast<std::size_t>(tile.expectedVertexCount)+farInput->connectorVertexCount
&& gpu1.sequentialIndices && gpu2.sequentialIndices && warpResult.sequentialIndices
&& gpu1.indexedIndirectArgsValid && gpu2.indexedIndirectArgsValid
&& warpResult.indexedIndirectArgsValid
@@ -2092,6 +2182,16 @@ namespace
++testedTiles;cpuTileWallSum+=cpuWallMs;cpuFieldPrepSum+=inputWallMs;
cpuExportPrepSum+=exportMs;
gpuFieldSum+=gpu1.fieldMs;gpuTotalSum+=gpu1.dispatchMs;warpFieldSum+=warpResult.fieldMs;
if(profileGpuKernels)
{
for(std::size_t stage=0;stage<gpuKernelStageSum.size();++stage)
gpuKernelStageSum[stage]+=gpu1.kernelMs[stage];
gpuFieldDistanceEvaluations+=gpu1.fieldDistanceEvaluations;
gpuFieldSamples+=tile.density.size();
gpuFieldMaxDistanceEvaluationsPerSample=std::max(
gpuFieldMaxDistanceEvaluationsPerSample,
gpu1.maxFieldDistanceEvaluationsPerSample);
}
protectedRouteAirSamples+=farInput->protectedSampleCount;
protectedSealSamples+=farInput->protectedSealSampleCount;
protectedSolidOverlaySamples+=farInput->protectedSolidOverlaySampleCount;
@@ -2114,7 +2214,17 @@ namespace
static_cast<unsigned long long>(protectedSealSamples),
static_cast<unsigned long long>(protectedThinSamples),
static_cast<unsigned long long>(protectedSolidOverlaySamples));
std::printf("[GpuPortStatus] far_field_subset_status=%s overall_port=step3_validation_pending field=shared_analytic_shapes_strata_gpu_signs_cpu_route_seal_overlay_exceptions meshing=gpu_compute_viewer_draw=integrated captures=viewer_run_required\n",
if(profileGpuKernels)
{
std::printf("[GpuKernelProfile] tiles=%llu field_ms=%.6f classify_ms=%.6f scan_ms=%.6f emit_ms=%.6f connector_rings_ms=%.6f connector_panels_ms=%.6f field_shape_distance_evaluations=%llu field_lattice_samples=%llu average_shape_distance_evaluations_per_lattice_sample=%.4f max_shape_distance_evaluations_per_lattice_sample=%u\n",
static_cast<unsigned long long>(testedTiles),gpuKernelStageSum[0],gpuKernelStageSum[1],
gpuKernelStageSum[2],gpuKernelStageSum[3],gpuKernelStageSum[4],gpuKernelStageSum[5],
static_cast<unsigned long long>(gpuFieldDistanceEvaluations),
static_cast<unsigned long long>(gpuFieldSamples),
gpuFieldSamples?static_cast<double>(gpuFieldDistanceEvaluations)/gpuFieldSamples:0.0,
gpuFieldMaxDistanceEvaluationsPerSample);
}
std::printf("[GpuPortStatus] far_field_topology_protected_status=%s repeated_gpu_outputs=bit_identical vertex_distance_tolerance=diagnostic overall_port=step5_far_field_gpu_decision_and_meshing field=shared_analytic_shapes_strata_gpu_ordinary_signs_cpu_sparse_exceptions meshing=compute_marching_cubes_connector_panels\n",
allPass ? "PASS" : "FAIL");
return allPass ? 0 : 3;
}
@@ -0,0 +1,91 @@
# GPU port, step 5: profile and results
## Result
The GPU now evaluates ordinary far-field signs from the shared analytic field and emits far meshes directly into pooled viewer buffers. For all 1,114 eligible LOD2–8 tiles, the RX 7900 XTX recorded **112.0 ms** for field evaluation and meshing, down from the step‑4 report’s 646 ms. The isolated runner measured 1.818 s of CPU `generateTile` time for the same tiles.
The target of a few tens of milliseconds is **not met**. Connector ring construction is now the largest kernel at 61.1 ms. The viewer’s cold hybrid result is also not clearly faster than all‑CPU; see the interleaved timings below.
## Changes in the measured path
- `GetFarFieldWorldInput` builds and caches the far shape list and spatial bins once per world. The measured payload has 18,401 shapes, 422,754 bins, 679,503 bin shape IDs, and is 30.523 MiB. Each far tile exports its key, sparse CPU-decided exceptions, strata data, and only the connector panels intersecting that tile.
- `EvaluateFarFieldCS` hashes each sample’s spatial bin and loops over that local shape list. The CPU no longer evaluates ordinary far signs. Route-protected air, thin-route bands, seals, and solid overlays remain CPU-decided inputs.
- Connector rings use one 64-thread group for 16 rings, with four lanes per ring and shared ring-frame data. The radius solver retains the verified signed-distance correction and fallback.
- The viewer batches up to 32 far tiles into pooled output buffers and draws the indirect GPU meshes. Normal completion/timing queries are polled with `D3D11_ASYNC_GETDATA_DONOTFLUSH`; the initial input-preparation worker pool is still joined synchronously.
Relevant source: [shared world export](../../../Core/src/ShapeWorldInternal.h#L514), [field and connector kernels](../gpu_first_port/VoxelForgeGpuMesher.hlsl#L206), [GPU timing/profile output](GpuFieldPort.cpp#L2201), and [viewer input preparation and batching](../../../Source/StandaloneViewer.cpp#L2232).
## GPU profile
Command: `VoxelForgeGpuFieldStep5VerifiedRunner.exe --whole-far --clip-radius=3 --profile-kernels`, launched by the runner wrapper at BelowNormal priority and affinity `0xFF00`. The complete passing result is in [step5_final_verified_profile.stdout.log](step5_final_verified_profile.stdout.log).
| Kernel | RX 7900 XTX time over 1,114 tiles |
|---|---:|
| Far field evaluation | 14.94 ms |
| Classify | 6.29 ms |
| Scan | 4.59 ms |
| Emit marching-cubes mesh | 23.31 ms |
| Connector rings | 61.05 ms |
| Connector panels | 1.53 ms |
| Reported field plus meshing total | **112.01 ms** |
The field counter recorded 892,486 primitive-distance evaluations over 7,640,926 lattice samples: **0.1168 evaluations per sample on average, 9 maximum**. This counter covers ordinary lattice-field evaluation; it does not count shape evaluations inside connector root refinement. The field kernel itself is 14.94 ms. Connector ring construction is the current bottleneck, followed by mesh emission.
The agreement runner’s CPU far `generateTile` sum was 1,817.94 ms, versus 112.01 ms of GPU field-plus-meshing timestamps. Its separately reported CPU export/protected-preparation sum was 141.63 ms. In the normal viewer runs, input preparation averaged 91.0 ms wall time and 669 ms summed across its eight workers; that worker sum is 58% below step 4’s 1.6 worker-seconds, but it is still a significant CPU cost.
## Agreement
The full far comparison covered 1,114 tiles at LOD2–8; 152 near-transition tiles stayed on the CPU path. The GPU evaluated 7.64 million lattice samples. GPU and WARP had **zero ordinary sign differences and zero cell topology differences**. The 127,401 protected exception cells and 353 thin-feature cells also had zero GPU and WARP topology differences. Repeated GPU output was bit-identical.
| Matched vertex comparisons | Count | Mean distance | Maximum distance |
|---|---:|---:|---:|
| CPU ↔ RX 7900 XTX | 2,554,084 | 0.11524 cm (1.15 mm) | 64.5602 cm |
| CPU ↔ WARP | 2,554,084 | 0.11522 cm (1.15 mm) | 64.5602 cm |
| RX 7900 XTX ↔ WARP | 12,252,180 | 0.0004666 cm (4.67 μm) | 0.8221 cm (8.22 mm) |
The CPU/GPU per-tile vertex quantization diagnostic exceeded its bound on 247 of 1,114 tiles. The largest error is on a far LOD8 tile at large world coordinates, consistent with float coordinate precision loss; that cause is an inference. Topology and protected-feature sign checks still pass, but the CPU/GPU maximum vertex distance is not a small bounded error and remains a limitation.
WARP is Microsoft’s software D3D implementation and serves as a different implementation check; it is not a second physical GPU vendor.
CPU hashes were compared with the saved `566d1f3` reference: [step5_cpu_hash_current_w8.stdout.log](../../../Core/verification/step5_cpu_hash_current_w8.stdout.log), **44 hash fields, 11 keys, zero differences**.
## GPU-mesh survival audit
The audit used the GPU meshes and required field readback. [step5_verified_gpu_survival_audit.stdout.log](step5_verified_gpu_survival_audit.stdout.log) reports `status=PASS`: 7,200/7,200 boundary surfaces present, zero boundary holes, zero connector samples/rays missing, and zero connector holes across 10,976 far connectors. It also reports 50,019 extra connector surfaces; the audit’s overall status remains PASS. The audit run took 38.728 s to reach the initial clipmap because audit readback and checks are enabled; this is not the normal cold-view timing.
## Interleaved cold viewer timings
Command for each run: `RunGpuFarViewer.ps1 -Mode cpu|gpu -ClipRadius 3 -ExecutableName VoxelForgeGpuFieldStep5FinalViewer.exe -LogStem step5_final_interleave_<mode>_<A-D>`. The wrapper used eight CPU workers, BelowNormal priority, affinity `0xFF00`, and `--quit-after-clipmap`. Order was CPU A, GPU A, GPU B, CPU B, CPU C, GPU C, GPU D, CPU D.
| Mode | Four cold initial-clipmap times | Mean |
|---|---|---:|
| All CPU (`--cpu-far`) | 2.409, 2.254, 2.340, 2.251 s | 2.3135 s |
| CPU near + GPU far (`--gpu-far`) | 2.277, 2.368, 2.335, 2.232 s | 2.3030 s |
The hybrid mean leads by 10.5 ms (0.45%), while individual runs vary by more than 100 ms. **This does not demonstrate a clear whole-view gain.** Full logs are `step5_final_interleave_cpu_A-D.stdout.log` and `step5_final_interleave_gpu_A-D.stdout.log` in this directory.
Across the four GPU viewer runs, the far timestamp totals averaged 93.1 ms, CPU input preparation 91.0 ms wall / 669 ms worker sum, output-pool reserve 26.3 ms, and upload/enqueue 71.8 ms. Submission used 37 batches of up to 32 tiles; the longest batch timestamp was 10.836 ms, almost the whole 11.111 ms budget of a 90 Hz frame. Viewer and agreement-runner timing scopes differ: the viewer reports 90.5–95.2 ms of far timestamps per run, while the agreement runner reports 112.0 ms for its per-kernel query sum. The viewer’s `gpu_dispatch_max_tile_ms` also ranges from 8.64 to 9.48 ms, unlike the runner’s 0.238 ms maximum tile timestamp; treat that per-item maximum as unresolved query/batch scope, not a comparable per-tile cost. Cold wall time is the end-to-end measure.
### Estimated frame slicing
Using the agreement runner’s per-tile GPU timestamps (mean 0.1005 ms, p95 0.1695 ms), GPU work alone would fit approximately:
| GPU slice budget | Tiles/frame at mean | Frames for 1,114 tiles | Tiles/frame at p95 | Frames at p95 |
|---|---:|---:|---:|---:|
| 0.5 ms | 4 (0.402 ms) | 279 (~3.10 s at 90 Hz) | 2 (0.339 ms) | 557 (~6.19 s) |
| 1.0 ms | 9 (0.905 ms) | 124 (~1.38 s at 90 Hz) | 5 (0.848 ms) | 223 (~2.48 s) |
These are GPU-only scheduling estimates. The viewer does not yet slice work to 0.5–1.0 ms; current 32-tile batches can consume almost an entire 90 Hz frame.
## Memory and GTX 1060 estimate
The final GPU survival-audit run reported peak viewer working set 0.70 GiB, peak private memory 4.50 GiB, and DXGI local GPU memory 3,115.6 MiB (3.04 GiB) against a 23.75 GiB budget. A simple sum of private and local-memory counters is about 7.54 GiB, under the 8 GiB limit with little headroom; the counters can overlap in driver accounting.
No GTX 1060 was available, so this is an estimate. Scaling the measured 112 ms by the rough FP32 ratio (about 61 TFLOPS for RX 7900 XTX versus the brief’s 4.4 TFLOPS for GTX 1060) gives about **1.56 s**. Pascal’s slower 32-bit integer work and different occupancy could widen that; estimate **1.5–2.5 s of GPU dispatch time** for the same far workload. Since the RX 7900 XTX hybrid is not clearly faster end-to-end, a GTX 1060-class system should not be expected to gain whole-view time from this version.
## Follow-up experiments not retained
- Replacing per-tile CPU exception candidate lists with per-sample hash-bin lookups made the full comparison fail protected signs/topology and raised measured CPU export preparation from about 142 ms to 360 ms. That change was reverted; the final passing run uses the original exact candidate filtering.
- Seeding connector roots from a trilinear interpolation of the tile field preserved the full comparison’s topology checks but increased connector-ring time from 61.05 to 69.76 ms and total GPU time from 112.01 to 120.95 ms. It was reverted. The [experiment log](step5_interp_fullfar_profile.stdout.log) is retained; the final shader and precompiled shader binaries were rebuilt from the verified source.
No commit, push, or stash was made.
@@ -138,7 +138,13 @@ RWStructuredBuffer<uint> CellCases : register(u0);
RWStructuredBuffer<uint> CellLocalOffsets : register(u1);
RWStructuredBuffer<uint> GroupVertexCounts : register(u2);
struct FarFieldSampleRange { uint offset; uint count; };
struct FarFieldSpatialBin
{
int3 coord;
uint offset;
uint count;
uint padding;
};
struct FarFieldConnectorPanel
{
float3 start; float radius;
@@ -159,8 +165,8 @@ struct FarConnectorRing
float3 padding;
};
StructuredBuffer<VF_FieldPrimitive> FarShapes : register(t10);
StructuredBuffer<FarFieldSampleRange> FarSampleShapeRanges : register(t11);
StructuredBuffer<uint> FarSampleShapeIds : register(t12);
StructuredBuffer<FarFieldSpatialBin> FarSpatialBins : register(t11);
StructuredBuffer<uint> FarSpatialBinShapeIds : register(t12);
StructuredBuffer<uint> FarProtectedAir : register(t13);
StructuredBuffer<uint> FarProtectedSolid : register(t14);
RWStructuredBuffer<float> FarFieldDensity : register(u7);
@@ -178,8 +184,45 @@ cbuffer FarFieldConstants : register(b1)
float FarStrataHi;
float FarStrataBlend;
uint FarSampleCount;
uint FarEvalCounterEnabled;
uint FarSpatialBinMask;
uint FarSpatialBinSize;
uint FarSpatialBinCapacity;
};
RWStructuredBuffer<uint> FarFieldEvalCounters : register(u6);
uint FarFieldBinHash(int3 coord)
{
uint hash = 2166136261u;
hash = (hash ^ asuint(coord.x)) * 16777619u;
hash = (hash ^ asuint(coord.y)) * 16777619u;
hash = (hash ^ asuint(coord.z)) * 16777619u;
hash ^= hash >> 16; hash *= 0x7feb352du; hash ^= hash >> 15;
hash *= 0x846ca68bu; hash ^= hash >> 16;
return hash;
}
FarFieldSpatialBin FarFieldFindBin(float3 position)
{
FarFieldSpatialBin emptyBin;
emptyBin.coord = int3(0,0,0);
emptyBin.offset = 0;
emptyBin.count = 0;
emptyBin.padding = 0;
const int3 coord = int3(floor(position / max(1.0f, (float)FarSpatialBinSize)));
uint slot = FarFieldBinHash(coord) & FarSpatialBinMask;
[loop]
for (uint probe = 0; probe < FarSpatialBinCapacity; ++probe)
{
const FarFieldSpatialBin bin = FarSpatialBins[slot];
if (bin.count == 0) return emptyBin;
if (all(bin.coord == coord)) return bin;
slot = (slot + 1) & FarSpatialBinMask;
}
return emptyBin;
}
[numthreads(64,1,1)]
void EvaluateFarFieldCS(uint3 dispatchId : SV_DispatchThreadID)
{
@@ -190,13 +233,14 @@ void EvaluateFarFieldCS(uint3 dispatchId : SV_DispatchThreadID)
const uint z = index / (SampleDim * SampleDim);
const float3 position = float3(OriginX, OriginY, OriginZ)
+ float3(int(x) - 1, int(y) - 1, int(z) - 1) * StepVoxels;
const FarFieldSampleRange range = FarSampleShapeRanges[index];
const FarFieldSpatialBin range = FarFieldFindBin(position);
float cave = 100000.0f;
float overlay = -100000.0f;
uint distanceEvaluations = 0;
[loop]
for (uint item = 0; item < range.count; ++item)
{
const VF_FieldPrimitive shape = FarShapes[FarSampleShapeIds[range.offset + item]];
const VF_FieldPrimitive shape = FarShapes[FarSpatialBinShapeIds[range.offset + item]];
if ((shape.flags & VF_FIELD_PRIMITIVE_CARVE) != 0)
{
if (position.x < shape.minVoxel[0] - 6 || position.x > shape.maxVoxel[0] + 6
@@ -205,6 +249,7 @@ void EvaluateFarFieldCS(uint3 dispatchId : SV_DispatchThreadID)
continue;
float distance = VF_FieldPrimitiveDistance(shape,
position.x, position.y, position.z);
++distanceEvaluations;
if ((shape.flags & VF_FIELD_PRIMITIVE_HAS_FLOOR) != 0)
{
const float floorZ = VF_FieldPrimitiveFloorAt(shape, position.x, position.y);
@@ -219,6 +264,7 @@ void EvaluateFarFieldCS(uint3 dispatchId : SV_DispatchThreadID)
{
overlay = max(overlay, -VF_FieldPrimitiveDistance(shape,
position.x, position.y, position.z));
++distanceEvaluations;
}
}
const float base = min(position.z - FarStrataLo, FarStrataHi - position.z);
@@ -231,16 +277,21 @@ void EvaluateFarFieldCS(uint3 dispatchId : SV_DispatchThreadID)
if (protectedAir != 0) density = magnitude;
else if (protectedSolid != 0) density = -magnitude;
FarFieldDensity[index] = density;
if (FarEvalCounterEnabled != 0) FarFieldEvalCounters[index] = distanceEvaluations;
}
float FarRockDistanceAt(float3 position)
float FarRockDistanceAt(float3 position,int3 cachedBinCoord,FarFieldSpatialBin cachedRange)
{
float cave = 100000.0f;
float overlay = -100000.0f;
const int3 positionBinCoord=int3(floor(position/max(1.0f,(float)FarSpatialBinSize)));
FarFieldSpatialBin range;
if(all(positionBinCoord==cachedBinCoord))range=cachedRange;
else range=FarFieldFindBin(position);
[loop]
for (uint shapeId = 0; shapeId < FarShapeCount; ++shapeId)
for (uint item = 0; item < range.count; ++item)
{
const VF_FieldPrimitive shape = FarShapes[shapeId];
const VF_FieldPrimitive shape = FarShapes[FarSpatialBinShapeIds[range.offset + item]];
if ((shape.flags & VF_FIELD_PRIMITIVE_CARVE) != 0)
{
if (position.x < shape.minVoxel[0] - 6 || position.x > shape.maxVoxel[0] + 6
@@ -269,29 +320,43 @@ float FarRockDistanceAt(float3 position)
float FarConnectorRadius(float3 center, float3 direction, float nominalRadius)
{
// The coarse far field is a signed-distance composition. Two bounded
// distance steps follow its connector wall closely without a CPU readback
// or a long radial scan for each sparse panel ring.
// Keep the bounded signed-distance refinement, but stop once the residual
// reaches the same acceptance bound used by the original fallback. A
// spatial-bin cache avoids re-hashing nearby probes.
const float3 nominalPosition=center+direction*nominalRadius;
const int3 cachedBinCoord=int3(floor(nominalPosition/max(1.0f,(float)FarSpatialBinSize)));
const FarFieldSpatialBin cachedRange=FarFieldFindBin(nominalPosition);
const float maxOffset=max(16.0f,nominalRadius*0.75f);
const float minRadius=max(0.0f,nominalRadius-maxOffset);
const float maxRadius=nominalRadius+maxOffset;
float radius=nominalRadius;
[unroll]
for(uint refine=0;refine<3;++refine)
float residual=FarRockDistanceAt(center+direction*radius,cachedBinCoord,cachedRange);
bool converged=abs(residual)<=1.0f;
if(!converged)
{
const float distance=FarRockDistanceAt(center+direction*radius);
const float nextDistance=FarRockDistanceAt(center+direction*(radius+4.0f));
// Most connector fields are close to a unit-gradient SDF along the
// radial ray. Try that correction first and verify it before accepting.
radius=clamp(radius-residual,minRadius,maxRadius);
residual=FarRockDistanceAt(center+direction*radius,cachedBinCoord,cachedRange);
converged=abs(residual)<=1.0f;
}
[unroll]
for(uint refine=0;refine<2&&!converged;++refine)
{
const float distance=residual;
const float nextDistance=FarRockDistanceAt(center+direction*(radius+4.0f),cachedBinCoord,cachedRange);
const float slope=(nextDistance-distance)*0.25f;
const float correction=abs(slope)>0.125f?distance/slope:distance;
radius=clamp(radius-correction,minRadius,maxRadius);
residual=FarRockDistanceAt(center+direction*radius,cachedBinCoord,cachedRange);
converged=abs(residual)<=1.0f;
}
const float residual=FarRockDistanceAt(center+direction*radius);
if(abs(residual)>1.0f)
{
const float searchMin=max(0.0f,nominalRadius-maxOffset-8.0f);
const float searchMax=nominalRadius+maxOffset;
float previous=searchMin;
float previousDistance=FarRockDistanceAt(center+direction*previous);
float previousDistance=FarRockDistanceAt(center+direction*previous,cachedBinCoord,cachedRange);
float bestRadius=radius;
float bestDifference=100000.0f;
[loop]
@@ -299,7 +364,7 @@ float FarConnectorRadius(float3 center, float3 direction, float nominalRadius)
{
const float distance=min(searchMin+sample*8.0f,searchMax);
if(distance<=previous+1.0e-5f)break;
const float currentDistance=FarRockDistanceAt(center+direction*distance);
const float currentDistance=FarRockDistanceAt(center+direction*distance,cachedBinCoord,cachedRange);
if((previousDistance>=0.0f)!=(currentDistance>=0.0f))
{
float lo=previous,hi=distance;
@@ -308,7 +373,7 @@ float FarConnectorRadius(float3 center, float3 direction, float nominalRadius)
for(uint refine=0;refine<8;++refine)
{
const float mid=(lo+hi)*0.5f;
if((FarRockDistanceAt(center+direction*mid)>=0.0f)==previousSolid)lo=mid;
if((FarRockDistanceAt(center+direction*mid,cachedBinCoord,cachedRange)>=0.0f)==previousSolid)lo=mid;
else hi=mid;
}
const float crossing=(lo+hi)*0.5f;
@@ -324,10 +389,85 @@ float FarConnectorRadius(float3 center, float3 direction, float nominalRadius)
return radius;
}
[numthreads(64,1,1)]
void BuildFarConnectorRingsCS(uint3 dispatchId : SV_DispatchThreadID)
uint FarConnectorPanelForRing(uint ringId)
{
const uint ringId = dispatchId.x;
uint low = 0;
uint high = FarConnectorCount;
[loop]
while (low < high)
{
const uint middle = low + ((high - low) >> 1);
const FarFieldConnectorPanel candidate = FarConnectorPanels[middle];
if (ringId < candidate.ringOffset) high = middle;
else if (ringId >= candidate.ringOffset + candidate.ringCount) low = middle + 1;
else return middle;
}
return 0;
}
uint FarConnectorPanelForVertex(uint vertexId)
{
uint low = 0;
uint high = FarConnectorCount;
[loop]
while (low < high)
{
const uint middle = low + ((high - low) >> 1);
const FarFieldConnectorPanel candidate = FarConnectorPanels[middle];
const uint candidateVertexCount = (candidate.ringCount - 1) * 24;
if (vertexId < candidate.vertexOffset) high = middle;
else if (vertexId >= candidate.vertexOffset + candidateVertexCount) low = middle + 1;
else return middle;
}
return 0;
}
groupshared float FarConnectorGroupRadii[64];
groupshared float4 FarConnectorGroupCenterT[16];
groupshared float4 FarConnectorGroupURadius[16];
groupshared float4 FarConnectorGroupV[16];
[numthreads(64,1,1)]
void BuildFarConnectorRingsCS(uint3 groupId : SV_GroupID,
uint3 groupThreadId : SV_GroupThreadID)
{
// Four lanes cooperate on one ring. Lane zero loads the panel and builds
// its shared frame once; each lane then refines one radial direction.
// One 64-thread group is one AMD wave, keeping radial work local.
const uint ringId = groupId.x * 16 + groupThreadId.x / 4;
const uint radialId = groupThreadId.x & 3;
const uint ringSlot = groupThreadId.x / 4;
if (radialId == 0)
{
const bool validRing = ringId < FarConnectorRingCount;
if (validRing)
{
const FarFieldConnectorPanel connector = FarConnectorPanels[FarConnectorPanelForRing(ringId)];
const float3 start = connector.start;
const float3 delta = connector.end - start;
const float connectorLength = length(delta);
const float3 axis = delta / max(connectorLength, 1.0e-8f);
float3 u = cross(axis, float3(0,0,1));
if (dot(u,u) < 0.01f) u = cross(axis, float3(0,1,0));
u = normalize(u);
const float3 v = normalize(cross(axis,u));
const uint localRing = ringId - connector.ringOffset;
float t = 0;
if (localRing == 0) t = connector.tMin;
else if (localRing + 1 == connector.ringCount) t = connector.tMax;
else t = ((float)(connector.axialSampleStart + localRing - 1) + 0.5f)
/ max(1.0f, (float)connector.axialSampleCount);
const float3 center = start + delta * t;
FarConnectorGroupCenterT[ringSlot] = float4(center,t);
FarConnectorGroupURadius[ringSlot] = float4(u,connector.radius);
FarConnectorGroupV[ringSlot] = float4(v,0);
}
else
{
FarConnectorGroupCenterT[ringSlot] = 0;
FarConnectorGroupURadius[ringSlot] = 0;
FarConnectorGroupV[ringSlot] = 0;
}
if (ringId == 0)
{
const uint maxVerticesPerTile = CellCount * 15;
@@ -339,50 +479,46 @@ void BuildFarConnectorRingsCS(uint3 dispatchId : SV_DispatchThreadID)
FarConnectorDrawArgs.Store(OutputArgsByteOffset + 32, 0);
FarConnectorDrawArgs.Store(OutputArgsByteOffset + 36, 0);
}
if (ringId >= FarConnectorRingCount) return;
}
GroupMemoryBarrierWithGroupSync();
const bool validRing = ringId < FarConnectorRingCount;
const float4 centerT = FarConnectorGroupCenterT[ringSlot];
const float4 uRadius = FarConnectorGroupURadius[ringSlot];
const float3 center = centerT.xyz;
const float3 u = uRadius.xyz;
const float3 v = FarConnectorGroupV[ringSlot].xyz;
const float3 direction = radialId == 0 ? u : radialId == 1 ? -u : radialId == 2 ? v : -v;
const float radius = validRing ? FarConnectorRadius(center,direction,uRadius.w) : 0;
FarConnectorGroupRadii[groupThreadId.x] = radius;
GroupMemoryBarrierWithGroupSync();
if (!validRing) return;
FarFieldConnectorPanel connector = FarConnectorPanels[0];
[loop]
for (uint i = 0; i < FarConnectorCount; ++i)
if (radialId == 0) FarConnectorRingOutput[ringId].t = centerT.w;
const uint ringBase = groupThreadId.x - radialId;
const float ru = FarConnectorGroupRadii[ringBase + 0];
const float rmu = FarConnectorGroupRadii[ringBase + 1];
const float rv = FarConnectorGroupRadii[ringBase + 2];
const float rmv = FarConnectorGroupRadii[ringBase + 3];
if (radialId == 0)
{
const FarFieldConnectorPanel candidate = FarConnectorPanels[i];
if (ringId >= candidate.ringOffset && ringId < candidate.ringOffset + candidate.ringCount)
FarConnectorRingOutput[ringId].corners[0] = float4(center + u*ru + v*rv, 0);
FarConnectorRingOutput[ringId].normals[0] = float4(-normalize(u+v),0);
}
else if (radialId == 1)
{
connector = candidate;
break;
FarConnectorRingOutput[ringId].corners[1] = float4(center - u*rmu + v*rv, 0);
FarConnectorRingOutput[ringId].normals[1] = float4(-normalize(-u+v),0);
}
else if (radialId == 2)
{
FarConnectorRingOutput[ringId].corners[2] = float4(center - u*rmu - v*rmv, 0);
FarConnectorRingOutput[ringId].normals[2] = float4(-normalize(-u-v),0);
}
else
{
FarConnectorRingOutput[ringId].corners[3] = float4(center + u*ru - v*rmv, 0);
FarConnectorRingOutput[ringId].normals[3] = float4(-normalize(u-v),0);
}
const float3 start = connector.start;
const float3 delta = connector.end - start;
const float connectorLength = length(delta);
const float3 axis = delta / max(connectorLength, 1.0e-8f);
float3 u = cross(axis, float3(0,0,1));
if (dot(u,u) < 0.01f) u = cross(axis, float3(0,1,0));
u = normalize(u);
const float3 v = normalize(cross(axis,u));
const uint localRing = ringId - connector.ringOffset;
float t;
if (localRing == 0) t = connector.tMin;
else if (localRing + 1 == connector.ringCount) t = connector.tMax;
else t = ((float)(connector.axialSampleStart + localRing - 1) + 0.5f)
/ max(1.0f, (float)connector.axialSampleCount);
const float3 center = start + delta * t;
const float ru = FarConnectorRadius(center,u,connector.radius);
const float rmu = FarConnectorRadius(center,-u,connector.radius);
const float rv = FarConnectorRadius(center,v,connector.radius);
const float rmv = FarConnectorRadius(center,-v,connector.radius);
FarConnectorRing ring;
ring.corners[0] = float4(center + u*ru + v*rv, 0);
ring.corners[1] = float4(center - u*rmu + v*rv, 0);
ring.corners[2] = float4(center - u*rmu - v*rmv, 0);
ring.corners[3] = float4(center + u*ru - v*rmv, 0);
ring.normals[0] = float4(-normalize(u+v),0);
ring.normals[1] = float4(-normalize(-u+v),0);
ring.normals[2] = float4(-normalize(-u-v),0);
ring.normals[3] = float4(-normalize(u-v),0);
ring.t = t;
ring.padding = 0;
FarConnectorRingOutput[ringId] = ring;
}
[numthreads(64,1,1)]
@@ -390,18 +526,7 @@ void EmitFarConnectorPanelsCS(uint3 dispatchId : SV_DispatchThreadID)
{
const uint vertexId = dispatchId.x;
if (vertexId >= FarConnectorVertexCount) return;
FarFieldConnectorPanel connector = FarConnectorPanels[0];
[loop]
for (uint i = 0; i < FarConnectorCount; ++i)
{
const FarFieldConnectorPanel candidate = FarConnectorPanels[i];
const uint candidateCount = (candidate.ringCount - 1) * 24;
if (vertexId >= candidate.vertexOffset && vertexId < candidate.vertexOffset + candidateCount)
{
connector = candidate;
break;
}
}
const FarFieldConnectorPanel connector = FarConnectorPanels[FarConnectorPanelForVertex(vertexId)];
const uint localVertex = vertexId - connector.vertexOffset;
const uint interval = localVertex / 24;
const uint cornerInQuad = localVertex % 6;
+53 -44
View File
@@ -20,6 +20,7 @@
#include <dxgi.h>
#include <windowsx.h>
#include <atomic>
#include <bit>
#include <complex>
#include <cstdio>
@@ -35,6 +36,7 @@
#include <numeric>
#include <optional>
#include <sstream>
#include <thread>
#include <unordered_map>
#include <unordered_set>
@@ -2227,50 +2229,9 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
return ResidentTiles.contains(Key);
}
void ScheduleGpuFarTiles(const std::vector<FVoxelTileKey>& Keys)
{
GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
GpuFarPreparedInputs.clear();GpuFarPreparationErrors.clear();
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld())return;
std::vector<FVoxelTileKey> CpuTiles;CpuTiles.reserve(Keys.size());
std::vector<FVoxelTileKey> Eligible;Eligible.reserve(Keys.size());
for(const FVoxelTileKey& Key:Keys)
{
if(Key.Level<World->FullResClipLevels){CpuTiles.push_back(Key);continue;}
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
if(ShapeWorld->isFarFieldTileEligible(CoreKey))
{
Eligible.push_back(Key);
}
else CpuTiles.push_back(Key);
}
if(Eligible.size()>GpuFarOutputCapacitySlots)
{
for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second);
GpuFarTiles.clear();GpuFarFreeOutputSlots.clear();GpuFarNextOutputSlot=0;
GpuFarOutputCapacitySlots=static_cast<uint32>(Eligible.size());
}
if(!Eligible.empty()&&GpuFarOutputCapacitySlots==0)
GpuFarOutputCapacitySlots=static_cast<uint32>(Eligible.size());
if(!Eligible.empty())
{
std::string PoolError;const auto ReserveStart=std::chrono::steady_clock::now();
if(!VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
GpuFarOutputCapacitySlots,static_cast<uint32>(World->CoarseTileCells),PoolError))
{
std::fprintf(stderr,"[GpuFarPoolError] %s\n",PoolError.c_str());
bGpuFarEnabled=false;GpuFarOutputCapacitySlots=0;
CpuTiles=Keys;Eligible.clear();GpuFarTiles.clear();
}
GpuFarPoolReserveMs+=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-ReserveStart).count();
SampleGpuFarMemory();
}
std::vector<FVoxelTileKey> NeedPreparation;NeedPreparation.reserve(Eligible.size());
for(const FVoxelTileKey& Key:Eligible)
if(!GpuFarTiles.contains(Key))NeedPreparation.push_back(Key);
if(!NeedPreparation.empty())
void PrepareGpuFarInputs(std::vector<FVoxelTileKey> NeedPreparation)
{
if(NeedPreparation.empty())return;
const auto PrepareStart=std::chrono::steady_clock::now();
std::vector<std::optional<VoxelForgeCore::FarFieldTileInput>> Inputs(NeedPreparation.size());
std::vector<std::string> Errors(NeedPreparation.size());
@@ -2316,6 +2277,51 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
NeedPreparation.size(),ThreadCount,PreparationMs,WorkerSumMs,Failed);
std::fflush(stdout);
}
void ScheduleGpuFarTiles(const std::vector<FVoxelTileKey>& Keys)
{
GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
GpuFarPreparedInputs.clear();GpuFarPreparationErrors.clear();
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld())return;
std::vector<FVoxelTileKey> CpuTiles;CpuTiles.reserve(Keys.size());
std::vector<FVoxelTileKey> Eligible;Eligible.reserve(Keys.size());
for(const FVoxelTileKey& Key:Keys)
{
if(Key.Level<World->FullResClipLevels){CpuTiles.push_back(Key);continue;}
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
if(ShapeWorld->isFarFieldTileEligible(CoreKey))
{
Eligible.push_back(Key);
}
else CpuTiles.push_back(Key);
}
if(Eligible.size()>GpuFarOutputCapacitySlots)
{
for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second);
GpuFarTiles.clear();GpuFarFreeOutputSlots.clear();GpuFarNextOutputSlot=0;
GpuFarOutputCapacitySlots=static_cast<uint32>(Eligible.size());
}
if(!Eligible.empty()&&GpuFarOutputCapacitySlots==0)
GpuFarOutputCapacitySlots=static_cast<uint32>(Eligible.size());
if(!Eligible.empty())
{
std::string PoolError;const auto ReserveStart=std::chrono::steady_clock::now();
if(!VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
GpuFarOutputCapacitySlots,static_cast<uint32>(World->CoarseTileCells),PoolError))
{
std::fprintf(stderr,"[GpuFarPoolError] %s\n",PoolError.c_str());
bGpuFarEnabled=false;GpuFarOutputCapacitySlots=0;
CpuTiles=Keys;Eligible.clear();GpuFarTiles.clear();
}
GpuFarPoolReserveMs+=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-ReserveStart).count();
SampleGpuFarMemory();
}
std::vector<FVoxelTileKey> NeedPreparation;NeedPreparation.reserve(Eligible.size());
for(const FVoxelTileKey& Key:Eligible)
if(!GpuFarTiles.contains(Key))NeedPreparation.push_back(Key);
if(!NeedPreparation.empty())
PrepareGpuFarInputs(std::move(NeedPreparation));
for(const FVoxelTileKey& Key:Eligible)
{
GpuFarEligibleTiles.insert(Key);
@@ -3170,7 +3176,10 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
const UINT Stride=sizeof(FGpuVertex),Offset=0;Context->IASetVertexBuffers(0,1,&L.VertexBuffer,&Stride,&Offset);
Context->IASetIndexBuffer(L.IndexBuffer,DXGI_FORMAT_R32_UINT,0);Context->DrawIndexed(L.IndexCount,0,0);
}
if(bGpuFarEnabled&&ShapeWorld&&ShapeWorld->hasFarWorld())
// Avoid issuing hundreds of tiny indirect draw calls while the
// initial far batches are still being built. Once the clipmap is
// ready the pooled GPU meshes draw directly as before.
if(bGpuFarEnabled&&bInitialClipmapReported&&ShapeWorld&&ShapeWorld->hasFarWorld())
{
C.Padding[0]=0.01f;
Context->UpdateSubresource(Constants,0,nullptr,&C,0,0);