wip(standalone): GPU step 4 -- batched far generation, connector panels on GPU; far survival passes

Up to 32 tiles per deferred command list, one polled event per batch, no per-tile waits; CPU
upload/enqueue 723 -> 46 ms. GPU far meshes pass far survival (7,200 strata boundaries, 10,976
connectors, 0 missing/holes). But hybrid cold 4.36 s vs CPU 2.29 s: whole-tile GPU timestamps sum to
646 ms (0.58 ms/tile; step 3's 0.03 ms covered part of the work only), divergent per-thread shape
loops; CPU far-input export 1.6 worker-s. CPU 44/44 hashes unchanged.

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 11:01:03 +02:00
1 parent 782f195bdb
commit 247682a8e0
18 files changed
+1519 -191

No files matched your search

@@ -205,6 +205,20 @@ namespace VoxelForgeCore
float z = 0.0f, halfWidth = 8.0f;
};
// Sparse connector repair panels for the GPU far renderer. Each record describes
// the tile-clipped axial interval and output ranges; signs and source radii are
// evaluated by the GPU from the far shape field.
struct FarFieldConnectorPanel
{
float start[3]{}, radius = 0.0f;
float end[3]{}, tMin = 0.0f;
float tMax = 1.0f;
std::uint32_t ringCount = 0, ringOffset = 0, vertexOffset = 0;
std::uint32_t axialSampleStart = 0, axialSampleCount = 0;
std::uint32_t padding[2]{};
};
static_assert(sizeof(FarFieldConnectorPanel) == 64);
struct FarFieldTileInput
{
Vec3i origin{};
@@ -222,6 +236,8 @@ namespace VoxelForgeCore
std::vector<std::uint32_t> protectedSolid;
std::vector<std::uint32_t> protectedThin;
std::vector<FarFieldStratumBoundary> stratumBoundaries;
std::vector<FarFieldConnectorPanel> connectorPanels;
std::uint32_t connectorRingCount = 0, connectorVertexCount = 0;
std::uint64_t protectedSampleCount = 0;
std::uint64_t protectedSolidSampleCount = 0;
std::uint64_t protectedSealSampleCount = 0;
+85 -19
View File
@@ -471,7 +471,6 @@ 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<std::vector<uint32>> PerSample(SampleCount);
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;
if(LayerRanges.empty())
@@ -505,6 +504,76 @@ public:
Out.protectedSolid.assign((SampleCount+31u)/32u,0u);
Out.protectedThin.assign((SampleCount+31u)/32u,0u);
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
// sufficient resolution and the CPU path emits no panel geometry.
if(Cells*Step>=(CHUNK_SIZE<<6))
{
std::vector<uint32> ConnectorCandidates;
FindFarConnectorCandidates(
FIntVector(Origin.X-64,Origin.Y-64,Origin.Z-64),
FIntVector(Origin.X+Cells*Step+64,Origin.Y+Cells*Step+64,
Origin.Z+Cells*Step+64),ConnectorCandidates);
const double TileMin[3]={static_cast<double>(Origin.X),static_cast<double>(Origin.Y),
static_cast<double>(Origin.Z)};
const double TileMax[3]={TileMin[0]+Cells*Step,TileMin[1]+Cells*Step,
TileMin[2]+Cells*Step};
for(const uint32 ConnectorIndex:ConnectorCandidates)
{
if(ConnectorIndex>=Connectors.size())continue;
const FFarConnectorSegment& Connector=Connectors[ConnectorIndex];
const double Start[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
const double Delta[3]={Connector.End.X-Connector.Start.X,
Connector.End.Y-Connector.Start.Y,Connector.End.Z-Connector.Start.Z};
double TMin=0.0,TMax=1.0;bool Intersects=true;
for(int32 Axis=0;Axis<3;++Axis)
{
if(std::abs(Delta[Axis])<1.0e-9)
{if(Start[Axis]<TileMin[Axis]||Start[Axis]>TileMax[Axis])Intersects=false;continue;}
double A=(TileMin[Axis]-Start[Axis])/Delta[Axis];
double B=(TileMax[Axis]-Start[Axis])/Delta[Axis];
if(A>B)std::swap(A,B);
TMin=std::max(TMin,A);TMax=std::min(TMax,B);
if(TMin>TMax){Intersects=false;break;}
}
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);
const uint32 FirstSample=static_cast<uint32>(std::clamp(FirstValue,0.0,
static_cast<double>(AxialSampleCount)));
const uint32 SampleCount=LastValue>=FirstValue?static_cast<uint32>(std::max(0.0,
std::min(LastValue,static_cast<double>(AxialSampleCount-1))-FirstValue+1.0)):0;
const uint32 RingCount=SampleCount+2;
const uint64 VertexCount=static_cast<uint64>(RingCount-1)*24u;
if(Out.connectorVertexCount+VertexCount>std::numeric_limits<uint32>::max())
{Error="far connector panel output exceeds the 32-bit tile limit";return false;}
VoxelForgeCore::FarFieldConnectorPanel Panel{};
for(int32 Axis=0;Axis<3;++Axis)
{Panel.start[Axis]=static_cast<float>(Start[Axis]);Panel.end[Axis]=static_cast<float>(Start[Axis]+Delta[Axis]);}
Panel.radius=Connector.Radius;Panel.tMin=static_cast<float>(TMin);Panel.tMax=static_cast<float>(TMax);
Panel.ringCount=RingCount;Panel.ringOffset=Out.connectorRingCount;
Panel.vertexOffset=Out.connectorVertexCount;
Panel.axialSampleStart=FirstSample;Panel.axialSampleCount=AxialSampleCount;
Out.connectorRingCount+=Panel.ringCount;Out.connectorVertexCount+=static_cast<uint32>(VertexCount);
Out.connectorPanels.push_back(Panel);
}
}
size_t SampleIndex=0;
for(int32 Z=-1;Z<=Cells+1;++Z)
for(int32 Y=-1;Y<=Cells+1;++Y)
@@ -561,9 +630,17 @@ public:
}
}
const auto Found=SpatialBins.find(PointBin(WX,WY,WZ));
if(Found==SpatialBins.end())continue;
auto& Ids=PerSample[SampleIndex];
Ids.assign(Found->second.begin(),Found->second.end());
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;
}
@@ -607,22 +684,11 @@ public:
G.floorCurveEndBlend=static_cast<float>(S.floorCurveEndBlend);
Out.shapes.push_back(G);
}
uint64 CandidateCount=0;
for(const auto& Ids:PerSample)CandidateCount+=Ids.size();
if(CandidateCount>std::numeric_limits<uint32>::max())
{Error="far tile candidate list exceeds the GPU 32-bit offset limit";return false;}
Out.sampleShapeIds.reserve(static_cast<size_t>(CandidateCount));
for(size_t I=0;I<PerSample.size();++I)
for(uint32& Id:Out.sampleShapeIds)
{
const auto& Ids=PerSample[I];
Out.sampleShapeRanges[I]={static_cast<uint32>(Out.sampleShapeIds.size()),
static_cast<uint32>(Ids.size())};
for(const uint32 Id:Ids)
{
if(Id>=Remap.size()||Remap[Id]==std::numeric_limits<uint32>::max())
{Error="far tile shape remap is inconsistent";return false;}
Out.sampleShapeIds.push_back(Remap[Id]);
}
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;
}