wip(standalone): GPU field step 2 -- far-field shapes/strata/relief in shared C++/HLSL, GPU field+mesh

Seven LOD2-8 tiles: RX 7900 XTX field+meshing 0.335 ms vs CPU generateTile 15.81 ms; repeat runs
bit-identical; CPU W1/W8 44/44 hashes unchanged. Not yet a real gain: rock/air signs still come from
CPU masks for every sample. Viewer does not draw GPU tiles yet; no whole-view timing.

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 08:02:02 +02:00
1 parent d4b4377d30
commit b61ccff549
11 files changed
+2361 -9

No files matched your search

@@ -172,6 +172,57 @@ namespace VoxelForgeCore
struct FarConnectorSegment { Vec3d start, end; float radius = 0.0f; };
// Float-only upload record for the HLSL far-field evaluator. Field order matches
// VF_FieldPrimitive in Core/src/VoxelForgeFieldShared.h.
struct FarFieldPrimitiveGpu
{
std::int32_t flags = 0;
std::int32_t minVoxel[3]{}, maxVoxel[3]{};
float center[3]{}, q[9]{}, minRadius = 0.0f;
float capsuleStart[3]{}, capsuleDelta[3]{};
float capsuleAxisLengthSquared = 0.0f, capsuleAxisLength = 0.0f;
float capsuleStartRadius = 0.0f, capsuleEndRadius = 0.0f;
float profileStartT = 0.0f, profileEndT = 1.0f;
float profileStartRadius = 0.0f, profileEndRadius = 0.0f;
float radiusSlopeStart = 0.0f, radiusSlopeEnd = 0.0f;
float roundedHalfExtent[3]{}, rounding = 0.0f;
float blendRadius = 0.0f, floorBlendRadius = 0.0f;
float floorCenterZ = 0.0f, floorSlopeX = 0.0f, floorSlopeY = 0.0f;
float floorCurveOriginX = 0.0f, floorCurveOriginY = 0.0f;
float floorCurveDeltaX = 0.0f, floorCurveDeltaY = 0.0f, floorCurveLength = 0.0f;
float floorCurveStartZ = 0.0f, floorCurveEndZ = 0.0f;
float floorCurveTangentStart = 0.0f, floorCurveTangentEnd = 0.0f;
float floorCurveStartBlend = 0.0f, floorCurveEndBlend = 0.0f;
};
struct FarFieldSampleRange
{
std::uint32_t offset = 0, count = 0;
};
struct FarFieldStratumBoundary
{
float z = 0.0f, halfWidth = 8.0f;
};
struct FarFieldTileInput
{
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;
// Bit 1 means air, matching the CPU density sign rule (density >= 0).
std::vector<std::uint32_t> rockAirSigns;
// Critical route air is a subset of rockAirSigns and is always authoritative.
std::vector<std::uint32_t> protectedAir;
std::vector<FarFieldStratumBoundary> stratumBoundaries;
std::uint64_t protectedSampleCount = 0;
};
struct EditBrush
{
enum class Shape : std::uint8_t { sphere, capsule, roundedBox };
@@ -259,6 +310,8 @@ namespace VoxelForgeCore
std::string& error);
std::vector<FarConnectorSegment> farConnectorSegments() const;
std::vector<std::uint32_t> farConnectorCandidates(Vec3i minVoxel, Vec3i maxVoxel) const;
std::optional<FarFieldTileInput> exportFarFieldTile(const TileKey& key,
std::string& error) const;
bool auditDetailPassing() const;
bool auditGridStairIntegrity() const;
+192 -6
View File
@@ -6,6 +6,7 @@
#include "VoxelNoise.h"
#include "VoxelStrateManager.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "VoxelForgeFieldShared.h"
#include <algorithm>
#include <array>
@@ -326,6 +327,7 @@ public:
// Route validation is world-wide. Detail regions reuse the same authored route data and
// do not need to repeat the 5,642-point world audit for every entering tile group.
if(bValidatePassageRoutes)ValidatePassageRoutes();
RebuildSharedFieldPrimitives();
Built=true;
if(bIncludeFarWorld)BuildFarWorld();
const uint64 WordBytes=static_cast<uint64>(RockWords.size()+CandidateWords.size())*sizeof(uint64);
@@ -432,6 +434,7 @@ public:
Stats.PrimitiveCount=Primitives.size();
if(Primitives.empty())
{Error="owner shape extraction produced no rooms, tunnels, or passages";return false;}
RebuildSharedFieldPrimitives();
Built=true;
return true;
}
@@ -461,6 +464,128 @@ public:
OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end());
}
bool ExportFarFieldTileInput(const FIntVector& Origin,int32 Cells,int32 Step,
VoxelForgeCore::FarFieldTileInput& Out,std::string& Error) const
{
if(!FarBuilt||Cells<=0||Cells>32||Step<=0)
{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())
{Out.strataLo=-100000.0f;Out.strataHi=100000.0f;}
else
{Out.strataLo=static_cast<float>(LayerRanges[0].Lo);Out.strataHi=static_cast<float>(LayerRanges[0].Hi);}
Out.strataBlend=StrataCaveBlendRadius;
if(Owner&&Owner->Manager)
{
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
{
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
const float HalfWidth=static_cast<float>(std::max(8.0,
static_cast<double>(Params.BoundarySealThickness)+8.0));
const float Boundaries[2]={static_cast<float>(Slot.BottomChunkZ)*CHUNK_SIZE,
static_cast<float>(Slot.TopChunkZ+1)*CHUNK_SIZE};
for(const float Z:Boundaries)
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
Out.stratumBoundaries.push_back({Z,HalfWidth});
}
std::sort(Out.stratumBoundaries.begin(),Out.stratumBoundaries.end(),
[](const auto& A,const auto& B){return A.z<B.z;});
Out.stratumBoundaries.erase(std::unique(Out.stratumBoundaries.begin(),
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.rockAirSigns.assign((SampleCount+31u)/32u,0u);
Out.protectedAir.assign((SampleCount+31u)/32u,0u);
size_t SampleIndex=0;
for(int32 Z=-1;Z<=Cells+1;++Z)
for(int32 Y=-1;Y<=Cells+1;++Y)
for(int32 X=-1;X<=Cells+1;++X,++SampleIndex)
{
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;}
const float Density=DensityAtLattice(WX,WY,WZ);
if(Density>=0.0f)Out.rockAirSigns[SampleIndex>>5]|=1u<<(SampleIndex&31u);
if(IsInsideCriticalAir(WX,WY,WZ))
{
Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSampleCount;
}
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());
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);
}
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)
{
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]);
}
}
return true;
}
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
@@ -505,6 +630,7 @@ public:
if(FarBuilt)return;
auto Stage=std::chrono::steady_clock::now();
BuildFarParentShapes();
RebuildSharedFieldPrimitives();
Stats.FarPrimitiveSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Stage).count();
Stage=std::chrono::steady_clock::now();
BuildSpatialBins(SpatialBinSize,false);
@@ -1407,6 +1533,64 @@ private:
static std::array<double,3> Cross(const std::array<double,3>& A,const std::array<double,3>& B)
{return {A[1]*B[2]-A[2]*B[1],A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0]};}
static VF_FieldPrimitive MakeSharedFieldPrimitive(const FPrimitive& P)
{
VF_FieldPrimitive Out{};
if(P.bCapsule)Out.flags|=VF_FIELD_PRIMITIVE_CAPSULE;
if(P.bSmoothCapsuleRadius)Out.flags|=VF_FIELD_PRIMITIVE_SMOOTH_CAPSULE;
if(P.bRoundedBox)Out.flags|=VF_FIELD_PRIMITIVE_ROUNDED_BOX;
if(P.bHasFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_HAS_FLOOR;
if(P.bCurvedFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_CURVED_FLOOR;
if(P.bCarve)Out.flags|=VF_FIELD_PRIMITIVE_CARVE;
if(P.bCritical)Out.flags|=VF_FIELD_PRIMITIVE_CRITICAL;
for(int32 A=0;A<3;++A)
{
Out.minVoxel[A]=P.Min[A];Out.maxVoxel[A]=P.Max[A];
Out.center[A]=P.CenterSub[A]/16.0;
Out.capsuleStart[A]=P.CapsuleStartWorld[A];
Out.capsuleDelta[A]=P.CapsuleAxisDelta[A];
Out.roundedHalfExtent[A]=P.RoundedHalfExtent[A];
}
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)
Out.q[I*3+J]=static_cast<double>(P.Q[I][J])/QScale;
Out.minRadius=P.MinRadius;
Out.capsuleAxisLengthSquared=P.CapsuleAxisLengthSquared;
Out.capsuleAxisLength=P.CapsuleAxisLength;
Out.capsuleStartRadius=P.CapsuleStartRadius;
Out.capsuleEndRadius=P.CapsuleEndRadius;
Out.profileStartT=P.CapsuleProfileStartT;
Out.profileEndT=P.CapsuleProfileEndT;
Out.profileStartRadius=P.CapsuleProfileStartRadius;
Out.profileEndRadius=P.CapsuleProfileEndRadius;
Out.radiusSlopeStart=P.CapsuleRadiusSlopeStart;
Out.radiusSlopeEnd=P.CapsuleRadiusSlopeEnd;
Out.rounding=P.Rounding;
Out.blendRadius=P.BlendRadius;
Out.floorBlendRadius=P.FloorBlendRadius;
Out.floorCenterZ=P.FloorCenterZ;
Out.floorSlopeX=P.FloorSlopeX;
Out.floorSlopeY=P.FloorSlopeY;
Out.floorCurveOriginX=P.FloorCurveOriginX;
Out.floorCurveOriginY=P.FloorCurveOriginY;
Out.floorCurveDeltaX=P.FloorCurveDeltaX;
Out.floorCurveDeltaY=P.FloorCurveDeltaY;
Out.floorCurveLength=P.FloorCurveLength;
Out.floorCurveStartZ=P.FloorCurveStartZ;
Out.floorCurveEndZ=P.FloorCurveEndZ;
Out.floorCurveTangentStart=P.FloorCurveTangentStart;
Out.floorCurveTangentEnd=P.FloorCurveTangentEnd;
Out.floorCurveStartBlend=P.FloorCurveStartBlend;
Out.floorCurveEndBlend=P.FloorCurveEndBlend;
return Out;
}
void RebuildSharedFieldPrimitives()
{
SharedFieldPrimitives.clear();SharedFieldPrimitives.reserve(Primitives.size());
for(const FPrimitive& P:Primitives)
SharedFieldPrimitives.push_back(MakeSharedFieldPrimitive(P));
}
void AddEllipsoid(const FVector& Center,const std::array<double,3>& Radii,
const std::array<double,3>& Axis0={1,0,0},
const std::array<double,3>& Axis1={0,1,0},
@@ -3633,6 +3817,7 @@ private:
for(uint32 Id:*QueryItems)
{
const FPrimitive& P=Primitives[Id];
const VF_FieldPrimitive& Shared=SharedFieldPrimitives[Id];
if(P.bCarve)
{
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
@@ -3642,22 +3827,22 @@ private:
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
float Distance=ShapeDistance(P,X,Y,Z);
float Distance=VF_FieldPrimitiveDistance(Shared,X,Y,Z);
if(P.bHasFloorClip)
{
const double Floor=FloorAt(P,X,Y);
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
const double Floor=VF_FieldPrimitiveFloorAt(Shared,X,Y);
Distance=VF_FieldSmoothMax(Distance,static_cast<float>(Floor-Z),
static_cast<float>(P.FloorBlendRadius));
}
Cave=SmoothMinDistance(Cave,Distance,static_cast<float>(P.BlendRadius));
Cave=VF_FieldSmoothMin(Cave,Distance,static_cast<float>(P.BlendRadius));
}
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
&&Z>=P.Min[2]&&Z<=P.Max[2])
Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z));
Overlay=std::max(Overlay,-VF_FieldPrimitiveDistance(Shared,X,Y,Z));
}
}
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
return SmoothMinDistance(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
return VF_FieldSmoothMin(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
}
struct FOwnerNoiseLineContext
@@ -4094,6 +4279,7 @@ private:
std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields;
std::vector<FPrimitive> Primitives;
std::vector<VF_FieldPrimitive> SharedFieldPrimitives;
std::vector<FFarConnectorSegment> FarConnectorSegments;
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> FarConnectorSpatialBins;
std::vector<FEditBrush> EditBrushes;
+18
View File
@@ -733,6 +733,24 @@ namespace VoxelForgeCore
return result;
}
std::optional<FarFieldTileInput> World::exportFarFieldTile(const TileKey& key,
std::string& error) const
{
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt())
{error="world must be compiled before exporting a far field tile";return std::nullopt;}
if(key.lod<impl_->configuration.fullResolutionLodCount||key.lod>8)
{error="far field export accepts only coarse LOD tiles";return std::nullopt;}
if(!impl_->shape->HasFarWorld())
{error="world must include the far parent/connector shapes before export";return std::nullopt;}
const int32 extent=CHUNK_SIZE<<key.lod;
const int32 cells=std::max(1,impl_->owner->CoarseTileCells);
const int32 step=std::max(1,extent/cells);
FarFieldTileInput result;
if(!impl_->shape->ExportFarFieldTileInput(ToNative(key).OriginVoxels(),cells,step,result,error))
return std::nullopt;
return result;
}
bool World::auditDetailPassing() const { return impl_&&impl_->shape&&impl_->shape->IsDetailAuditPassing(); }
bool World::auditGridStairIntegrity() const
{ return impl_&&impl_->shape&&impl_->shape->AuditGridStairIntegrity(); }
+178 -3
View File
@@ -7,22 +7,182 @@
#define VF_FIELD_INLINE
typedef int VF_FieldI32;
typedef uint VF_FieldU32;
typedef float VF_FieldReal;
#define VF_FIELD_CAST_I32(v) int(v)
#define VF_FIELD_CAST_U32(v) uint(v)
#define VF_FIELD_CAST_F32(v) float(v)
#define VF_FIELD_FLOOR(v) floor(v)
#define VF_FIELD_ABS(v) abs(v)
#define VF_FIELD_SQRT(v) sqrt(v)
#define VF_FIELD_MIN(a,b) min(a,b)
#define VF_FIELD_MAX(a,b) max(a,b)
#define VF_FIELD_CLAMP(v,a,b) clamp(v,a,b)
#define VF_FIELD_LERP(a,b,t) lerp(a,b,t)
#else
#include <algorithm>
#include <cmath>
#include <cstdint>
#define VF_FIELD_INLINE inline
typedef std::int32_t VF_FieldI32;
typedef std::uint32_t VF_FieldU32;
typedef double VF_FieldReal;
#define VF_FIELD_CAST_I32(v) static_cast<VF_FieldI32>(v)
#define VF_FIELD_CAST_U32(v) static_cast<VF_FieldU32>(v)
#define VF_FIELD_CAST_F32(v) static_cast<float>(v)
#define VF_FIELD_FLOOR(v) std::floor(v)
#define VF_FIELD_ABS(v) std::abs(v)
#define VF_FIELD_SQRT(v) std::sqrt(v)
#define VF_FIELD_MIN(a,b) std::min(a,b)
#define VF_FIELD_MAX(a,b) std::max(a,b)
#define VF_FIELD_CLAMP(v,a,b) std::clamp(v,a,b)
#define VF_FIELD_LERP(a,b,t) std::lerp(a,b,t)
#endif
// High-level far-field primitive data. The CPU instance uses double intermediates to retain the
// reference path's precision; HLSL instantiates the same fields and routines as float. The packed
// upload record is a separate float-only API type with the same semantic members.
struct VF_FieldPrimitive
{
VF_FieldI32 flags;
VF_FieldI32 minVoxel[3], maxVoxel[3];
VF_FieldReal center[3], q[9], minRadius;
VF_FieldReal capsuleStart[3], capsuleDelta[3];
VF_FieldReal capsuleAxisLengthSquared, capsuleAxisLength;
VF_FieldReal capsuleStartRadius, capsuleEndRadius;
VF_FieldReal profileStartT, profileEndT, profileStartRadius, profileEndRadius;
VF_FieldReal radiusSlopeStart, radiusSlopeEnd;
VF_FieldReal roundedHalfExtent[3], rounding;
VF_FieldReal blendRadius, floorBlendRadius;
VF_FieldReal floorCenterZ, floorSlopeX, floorSlopeY;
VF_FieldReal floorCurveOriginX, floorCurveOriginY;
VF_FieldReal floorCurveDeltaX, floorCurveDeltaY, floorCurveLength;
VF_FieldReal floorCurveStartZ, floorCurveEndZ;
VF_FieldReal floorCurveTangentStart, floorCurveTangentEnd;
VF_FieldReal floorCurveStartBlend, floorCurveEndBlend;
};
static const VF_FieldI32 VF_FIELD_PRIMITIVE_CAPSULE = 1;
static const VF_FieldI32 VF_FIELD_PRIMITIVE_SMOOTH_CAPSULE = 2;
static const VF_FieldI32 VF_FIELD_PRIMITIVE_ROUNDED_BOX = 4;
static const VF_FieldI32 VF_FIELD_PRIMITIVE_HAS_FLOOR = 8;
static const VF_FieldI32 VF_FIELD_PRIMITIVE_CURVED_FLOOR = 16;
static const VF_FieldI32 VF_FIELD_PRIMITIVE_CARVE = 32;
static const VF_FieldI32 VF_FIELD_PRIMITIVE_CRITICAL = 64;
VF_FIELD_INLINE VF_FieldReal VF_FieldEvaluateFloorProfile(VF_FieldReal a, VF_FieldReal b,
VF_FieldReal tangentA, VF_FieldReal tangentB, VF_FieldReal length,
VF_FieldReal startBlend, VF_FieldReal endBlend, VF_FieldReal s)
{
const VF_FieldReal secant = (b - a) / length;
if (s < 0.0) return a + tangentA * s;
if (s > length) return b + tangentB * (s - length);
VF_FieldReal result = VF_FIELD_LERP(a, b, s / length);
if (startBlend > 0.0 && s < startBlend)
{
const VF_FieldReal t = s / startBlend;
const VF_FieldReal correction = (tangentA - secant) * startBlend
* (t - 2.0 * t * t + t * t * t);
result += correction;
}
if (endBlend > 0.0 && length - s < endBlend)
{
const VF_FieldReal t = (length - s) / endBlend;
const VF_FieldReal correction = (tangentB - secant) * endBlend
* (t - 2.0 * t * t + t * t * t);
result -= correction;
}
return result;
}
VF_FIELD_INLINE VF_FieldReal VF_FieldPrimitiveFloorAt(VF_FieldPrimitive p,
VF_FieldReal x, VF_FieldReal y)
{
if ((p.flags & VF_FIELD_PRIMITIVE_CURVED_FLOOR) != 0)
{
const VF_FieldReal t = ((x - p.floorCurveOriginX) * p.floorCurveDeltaX
+ (y - p.floorCurveOriginY) * p.floorCurveDeltaY)
/ (p.floorCurveLength * p.floorCurveLength);
return VF_FieldEvaluateFloorProfile(p.floorCurveStartZ, p.floorCurveEndZ,
p.floorCurveTangentStart, p.floorCurveTangentEnd, p.floorCurveLength,
p.floorCurveStartBlend, p.floorCurveEndBlend, t * p.floorCurveLength);
}
return p.floorCenterZ + p.floorSlopeX * (x - p.center[0])
+ p.floorSlopeY * (y - p.center[1]);
}
VF_FIELD_INLINE float VF_FieldCapsuleRadiusAtT(VF_FieldPrimitive p,
VF_FieldReal t, VF_FieldReal axisLength)
{
t = VF_FIELD_CLAMP(t, 0.0, 1.0);
if ((p.flags & VF_FIELD_PRIMITIVE_SMOOTH_CAPSULE) == 0)
return VF_FIELD_CAST_F32(p.capsuleStartRadius
+ (p.capsuleEndRadius - p.capsuleStartRadius) * t);
const VF_FieldReal t0 = p.profileStartT, t1 = p.profileEndT;
if (t < t0)
return VF_FIELD_CAST_F32(p.profileStartRadius
+ p.radiusSlopeStart * axisLength * (t - t0));
if (t > t1)
return VF_FIELD_CAST_F32(p.profileEndRadius
+ p.radiusSlopeEnd * axisLength * (t - t1));
const VF_FieldReal spanT = VF_FIELD_MAX(1.0e-9, t1 - t0);
const VF_FieldReal u = VF_FIELD_CLAMP((t - t0) / spanT, 0.0, 1.0);
const VF_FieldReal u2 = u * u, u3 = u2 * u;
const VF_FieldReal spanDistance = axisLength * spanT;
const VF_FieldReal h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
const VF_FieldReal h10 = u3 - 2.0 * u2 + u;
const VF_FieldReal h01 = -2.0 * u3 + 3.0 * u2;
const VF_FieldReal h11 = u3 - u2;
return VF_FIELD_CAST_F32(h00 * p.profileStartRadius
+ h10 * spanDistance * p.radiusSlopeStart
+ h01 * p.profileEndRadius
+ h11 * spanDistance * p.radiusSlopeEnd);
}
VF_FIELD_INLINE float VF_FieldPrimitiveDistance(VF_FieldPrimitive p,
VF_FieldReal x, VF_FieldReal y, VF_FieldReal z)
{
if ((p.flags & VF_FIELD_PRIMITIVE_CAPSULE) != 0)
{
const VF_FieldReal position[3] = {x, y, z};
VF_FieldReal v[3], d[3], vd = 0.0;
for (VF_FieldI32 axis = 0; axis < 3; ++axis)
{
v[axis] = position[axis] - p.capsuleStart[axis];
d[axis] = p.capsuleDelta[axis];
vd += v[axis] * d[axis];
}
const VF_FieldReal d2 = p.capsuleAxisLengthSquared;
const VF_FieldReal t = d2 > 1.0e-12 ? VF_FIELD_CLAMP(vd / d2, 0.0, 1.0) : 0.0;
VF_FieldReal distanceSquared = 0.0;
for (VF_FieldI32 axis = 0; axis < 3; ++axis)
{
const VF_FieldReal e = v[axis] - t * d[axis];
distanceSquared += e * e;
}
const float radius = VF_FieldCapsuleRadiusAtT(p, t, p.capsuleAxisLength);
return VF_FIELD_CAST_F32(VF_FIELD_SQRT(distanceSquared) - radius);
}
if ((p.flags & VF_FIELD_PRIMITIVE_ROUNDED_BOX) != 0)
{
const VF_FieldReal d[3] = {
VF_FIELD_ABS(x - p.center[0]) - p.roundedHalfExtent[0],
VF_FIELD_ABS(y - p.center[1]) - p.roundedHalfExtent[1],
VF_FIELD_ABS(z - p.center[2]) - p.roundedHalfExtent[2]};
const VF_FieldReal ox = VF_FIELD_MAX(d[0], 0.0);
const VF_FieldReal oy = VF_FIELD_MAX(d[1], 0.0);
const VF_FieldReal oz = VF_FIELD_MAX(d[2], 0.0);
const VF_FieldReal outside = VF_FIELD_SQRT(ox * ox + oy * oy + oz * oz);
const VF_FieldReal inside = VF_FIELD_MIN(VF_FIELD_MAX(d[0], VF_FIELD_MAX(d[1], d[2])), 0.0);
return VF_FIELD_CAST_F32(outside + inside - p.rounding);
}
const VF_FieldReal d[3] = {x - p.center[0], y - p.center[1], z - p.center[2]};
VF_FieldReal q = 0.0;
for (VF_FieldI32 i = 0; i < 3; ++i)
for (VF_FieldI32 j = 0; j < 3; ++j)
q += p.q[i * 3 + j] * d[i] * d[j] * 256.0;
return VF_FIELD_CAST_F32((VF_FIELD_SQRT(VF_FIELD_MAX(0.0, q)) - 1.0) * p.minRadius);
}
VF_FIELD_INLINE VF_FieldU32 VF_FieldMix(VF_FieldU32 x)
{
x ^= x >> 16;
@@ -94,7 +254,7 @@ VF_FIELD_INLINE float VF_FieldSmoothUnit(float value)
#if defined(VF_FIELD_HLSL)
value = clamp(value, 0.0f, 1.0f);
#else
value = value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value);
value = std::clamp(value, 0.0f, 1.0f);
#endif
return value * value * (3.0f - 2.0f * value);
}
@@ -102,7 +262,11 @@ VF_FIELD_INLINE float VF_FieldSmoothUnit(float value)
VF_FIELD_INLINE float VF_FieldSmoothMin(float a, float b, float width)
{
if (width <= 0.0f)
#if defined(VF_FIELD_HLSL)
return a < b ? a : b;
#else
return std::min(a, b);
#endif
const float delta = a - b;
#if defined(VF_FIELD_HLSL)
const float absolute = abs(delta);
@@ -110,12 +274,17 @@ VF_FIELD_INLINE float VF_FieldSmoothMin(float a, float b, float width)
const float h = max(width - absolute, 0.0f) / width;
#else
const float absolute = std::abs(delta);
const float minimum = a < b ? a : b;
const float h = (width - absolute) > 0.0f ? (width - absolute) / width : 0.0f;
const float minimum = std::min(a, b);
const float h = std::max(width - absolute, 0.0f) / width;
#endif
return minimum - (h * h * h) * (width / 6.0f);
}
VF_FIELD_INLINE float VF_FieldSmoothMax(float a, float b, float width)
{
return -VF_FieldSmoothMin(-a, -b, width);
}
// CPU and GPU use the same edge-root rule. The optional midpoint is the exact analytic
// field evaluation used by the reference LOD0 mesher for roots close to an edge midpoint.
VF_FIELD_INLINE float VF_FieldRefineRoot(float d1, float d2, float midpoint,
@@ -171,3 +340,9 @@ VF_FIELD_INLINE float VF_FieldRefineRoot(float d1, float d2, float midpoint,
#undef VF_FIELD_CAST_U32
#undef VF_FIELD_CAST_F32
#undef VF_FIELD_FLOOR
#undef VF_FIELD_ABS
#undef VF_FIELD_SQRT
#undef VF_FIELD_MIN
#undef VF_FIELD_MAX
#undef VF_FIELD_CLAMP
#undef VF_FIELD_LERP
@@ -0,0 +1,105 @@
[CmdletBinding()]
param([switch]$SkipCore)
$ErrorActionPreference = 'Stop'
$HostProcess = Get-Process -Id $PID
$HostProcess.PriorityClass = 'BelowNormal'
$HostProcess.ProcessorAffinity = [IntPtr]0xFF00
$Standalone = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$BuildDir = Join-Path $Standalone 'Build\VoxelForgeGpuFieldPort'
$WrapperDir = Join-Path $BuildDir 'wrappers'
$BinDir = Join-Path $Standalone 'bin'
$Output = Join-Path $BinDir 'VoxelForgeGpuFieldPort.exe'
$CoreLib = Join-Path $BuildDir 'VoxelForgeGpuFieldPortCore.lib'
$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat'
if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC setup script missing: $VcVars" }
[IO.Directory]::CreateDirectory($BuildDir) | Out-Null
[IO.Directory]::CreateDirectory($WrapperDir) | Out-Null
[IO.Directory]::CreateDirectory($BinDir) | Out-Null
$Manifest = Join-Path $Standalone 'Core\core_sources.txt'
$Sources = @(Get-Content -LiteralPath $Manifest | ForEach-Object { $_.Trim() } |
Where-Object { $_ -and -not $_.StartsWith('#') })
$CompileSources = [Collections.Generic.List[string]]::new()
foreach ($SourceName in $Sources) {
$Source = [IO.Path]::GetFullPath((Join-Path $Standalone $SourceName))
if (-not $Source.StartsWith($Standalone, [StringComparison]::OrdinalIgnoreCase)) {
throw "Core source escapes Standalone: $SourceName"
}
if ($SourceName.Replace('\','/') -eq 'Core/src/VoxelForgeCore.cpp') {
$CompileSources.Add($Source)
continue
}
$WrapperName = 'gpu_vfcore_' + ($SourceName.Replace('\','/') -replace '[^A-Za-z0-9]+','_')
$WrapperPath = Join-Path $WrapperDir ($WrapperName + '.cpp')
$SourceInclude = $Source.Replace('\','/')
$WrapperText = @"
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "$SourceInclude"
}
"@
[IO.File]::WriteAllText($WrapperPath, $WrapperText, [Text.UTF8Encoding]::new($false))
$CompileSources.Add($WrapperPath)
}
$IncludeArgs = @(
('/I"{0}"' -f (Join-Path $Standalone 'Core\include')),
('/I"{0}"' -f (Join-Path $Standalone 'Core\src')),
('/I"{0}"' -f (Join-Path $Standalone 'Compat')),
('/I"{0}"' -f (Join-Path $Standalone 'Source')),
('/I"{0}"' -f (Join-Path $Standalone 'Source\VoxelForge\Public')),
('/I"{0}"' -f (Join-Path $Standalone 'Source\VoxelForge\Private'))
)
$Common = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/MP8','/D_CRT_SECURE_NO_WARNINGS') + $IncludeArgs
$CoreRsp = Join-Path $BuildDir 'field_core_compile.rsp'
[IO.File]::WriteAllLines($CoreRsp, $Common + @('/c') + @($CompileSources | ForEach-Object { '"{0}"' -f $_ }), [Text.UTF8Encoding]::new($false))
$ObjectNames = @($CompileSources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) })
$LibRsp = Join-Path $BuildDir 'field_core_library.rsp'
[IO.File]::WriteAllLines($LibRsp, @('/nologo',('/OUT:"{0}"' -f $CoreLib)) + $ObjectNames, [Text.UTF8Encoding]::new($false))
$RunnerRsp = Join-Path $BuildDir 'field_runner_link.rsp'
$Runner = Join-Path $PSScriptRoot 'GpuFieldPort.cpp'
$RunnerRspLines = $Common + @(
('/Fe"{0}"' -f $Output),
('"{0}"' -f $Runner),
('"{0}"' -f $CoreLib),
'/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib','psapi.lib'
)
[IO.File]::WriteAllLines($RunnerRsp, $RunnerRspLines, [Text.UTF8Encoding]::new($false))
Push-Location $BuildDir
try {
$BuildCommands = [Collections.Generic.List[string]]::new()
if (-not $SkipCore) {
$BuildCommands.Add(('call "{0}" >nul && cl.exe @"{1}"' -f $VcVars, $CoreRsp))
$BuildCommands.Add(('call "{0}" >nul && lib.exe @"{1}"' -f $VcVars, $LibRsp))
}
$BuildCommands.Add(('call "{0}" >nul && cl.exe @"{1}"' -f $VcVars, $RunnerRsp))
foreach ($BuildCommand in $BuildCommands) {
$attempt = 0
do {
& $env:ComSpec /d /s /c $BuildCommand
$code = $LASTEXITCODE
if ($code -eq -1073741502 -and $attempt -eq 0) {
Write-Host '0xC0000142 encountered; waiting 60 seconds before the single permitted retry.'
Start-Sleep -Seconds 60
$attempt++
continue
}
if ($code -eq -1073741502) { throw '0xC0000142 repeated after the single retry; stopping.' }
if ($code -ne 0) { throw "GPU port build command failed with exit code $code" }
break
} while ($true)
}
if (-not $SkipCore) {
& (Join-Path $Standalone 'Core\CheckCoreSymbols.ps1') -LibraryPath $CoreLib
if ($LASTEXITCODE -ne 0) { throw "GPU port Core symbol check failed with exit code $LASTEXITCODE" }
}
if (-not (Test-Path -LiteralPath $Output -PathType Leaf)) { throw "GPU runner missing: $Output" }
} finally {
Pop-Location
}
Write-Host "Built $CoreLib"
Write-Host "Built $Output"
@@ -0,0 +1,142 @@
[CmdletBinding()]
param(
[string]$OutputName = 'VoxelForgeGpuFieldViewer.exe',
[ValidateSet('All','Core')][string]$Target = 'All'
)
$ErrorActionPreference = 'Stop'
$BuildHostProcess = Get-Process -Id $PID
$BuildHostProcess.PriorityClass = 'BelowNormal'
$BuildHostProcess.ProcessorAffinity = [IntPtr]0xFF00
$Root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$BuildDir = Join-Path $Root 'Build\VoxelForgeGpuFieldViewer'
$BinDir = Join-Path $Root 'bin'
[IO.Directory]::CreateDirectory($BuildDir) | Out-Null
[IO.Directory]::CreateDirectory($BinDir) | Out-Null
if ($Target -eq 'All' -and ([IO.Path]::GetFileName($OutputName) -ne $OutputName -or [IO.Path]::GetExtension($OutputName) -ne '.exe')) {
throw "OutputName must be an executable filename, not a path: $OutputName"
}
$OutputPath = Join-Path $BinDir $OutputName
$CoreLibrary = Join-Path $BuildDir 'VoxelForgeGpuFieldViewerCore.lib'
$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat'
if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script not found: $VcVars" }
function Invoke-FieldViewerBuildCommand([string]$CommandLine,[string]$Label) {
$BuildRetry = 0
do {
& $env:ComSpec /d /s /c $CommandLine
$BuildExit = $LASTEXITCODE
if ($BuildExit -eq -1073741502 -and $BuildRetry -eq 0) {
Write-Host '0xC0000142 encountered; waiting 60 seconds before the single permitted retry.'
Start-Sleep -Seconds 60
$BuildRetry++
continue
}
if ($BuildExit -eq -1073741502) { throw "0xC0000142 repeated after one retry during $Label; stopping." }
if ($BuildExit -ne 0) { throw "$Label failed with exit code $BuildExit" }
break
} while ($true)
}
$SourceManifest = Join-Path $Root 'Core\core_sources.txt'
if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) { throw "Core source manifest not found: $SourceManifest" }
$CoreSourceNames = @(Get-Content -LiteralPath $SourceManifest | ForEach-Object { $_.Trim() } |
Where-Object { $_ -and -not $_.StartsWith('#') })
if ($CoreSourceNames.Count -eq 0) { throw "Core source manifest is empty: $SourceManifest" }
$CoreSources = @($CoreSourceNames | ForEach-Object {
$SourcePath = [IO.Path]::GetFullPath((Join-Path $Root $_))
if (-not $SourcePath.StartsWith($Root,[StringComparison]::OrdinalIgnoreCase)) {
throw "Core source path escapes Standalone: $_"
}
$SourcePath
})
foreach ($Source in $CoreSources) {
if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Required core source is missing: $Source" }
}
# Keep the canonical source manifest shared with UBT, but compile every copied implementation
# source through a generated one-source shim that places its Unreal-shaped internals in a private
# namespace. The public API implementation owns its boundary and is compiled directly.
$WrapperDir = Join-Path $BuildDir 'wrappers'
[IO.Directory]::CreateDirectory($WrapperDir) | Out-Null
$CompileSources = [Collections.Generic.List[string]]::new()
foreach ($Index in 0..($CoreSources.Count-1)) {
$ManifestName = $CoreSourceNames[$Index].Replace('\','/')
if ($ManifestName -eq 'Core/src/VoxelForgeCore.cpp') {
$CompileSources.Add($CoreSources[$Index])
continue
}
$WrapperName = 'vfcore_' + ($ManifestName -replace '[^A-Za-z0-9]+','_')
$WrapperPath = Join-Path $WrapperDir ($WrapperName + '.cpp')
$SourceInclude = $CoreSources[$Index].Replace('\','/')
$WrapperText = @"
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "$SourceInclude"
}
"@
[IO.File]::WriteAllText($WrapperPath,$WrapperText,[Text.UTF8Encoding]::new($false))
$CompileSources.Add($WrapperPath)
}
$IncludeArgs = @(
('/I"{0}"' -f (Join-Path $Root 'Core\include')),
('/I"{0}"' -f (Join-Path $Root 'Core\src')),
('/I"{0}"' -f (Join-Path $Root 'Compat')),
('/I"{0}"' -f (Join-Path $Root 'Source')),
('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Public')),
('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private'))
)
$CommonFlags = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/MP8','/D_CRT_SECURE_NO_WARNINGS') + $IncludeArgs
$CoreRsp = Join-Path $BuildDir 'core-compile.rsp'
$CoreRspLines = $CommonFlags + @('/c') + @($CompileSources | ForEach-Object { '"{0}"' -f $_ })
[IO.File]::WriteAllLines($CoreRsp,$CoreRspLines,[Text.UTF8Encoding]::new($false))
$Objects = @($CompileSources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) })
$LibRsp = Join-Path $BuildDir 'core-library.rsp'
[IO.File]::WriteAllLines($LibRsp,@('/nologo',('/OUT:"{0}"' -f $CoreLibrary)) + $Objects,[Text.UTF8Encoding]::new($false))
$LegacySources = @()
$LegacyObjects = @()
$LegacyRsp = Join-Path $BuildDir 'viewer-reference-compile.rsp'
if ($Target -eq 'All') {
for ($Index=0; $Index -lt $CoreSources.Count; ++$Index) {
if ($CoreSourceNames[$Index].Replace('\','/') -eq 'Core/src/VoxelForgeCore.cpp') { continue }
$LegacySources += $CoreSources[$Index]
}
$LegacyObjects = @($LegacySources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) })
$LegacyRspLines = $CommonFlags + @('/c') + @($LegacySources | ForEach-Object { '"{0}"' -f $_ })
[IO.File]::WriteAllLines($LegacyRsp,$LegacyRspLines,[Text.UTF8Encoding]::new($false))
}
$ViewerRsp = Join-Path $BuildDir 'viewer-link.rsp'
$ViewerRspLines = $CommonFlags + @('/Fe"{0}"' -f $OutputPath) + $LegacyObjects + @(
('"{0}"' -f (Join-Path $Root 'Source\StandaloneViewer.cpp')),
('"{0}"' -f $CoreLibrary),
'/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib'
)
[IO.File]::WriteAllLines($ViewerRsp,$ViewerRspLines,[Text.UTF8Encoding]::new($false))
Push-Location $BuildDir
try {
# One cl process with /MP8 bounds compiler work to eight processes; MSVC, lib.exe, and the
# viewer link inherit BelowNormal priority and the 0xFF00 process affinity from this host.
$CommandLine = 'call "{0}" >nul && cl.exe @"{1}" && lib.exe @"{2}"' -f $VcVars,$CoreRsp,$LibRsp
Invoke-FieldViewerBuildCommand $CommandLine 'VoxelForgeCore compile/library step'
if (-not (Test-Path -LiteralPath $CoreLibrary -PathType Leaf)) { throw "Core library was not produced: $CoreLibrary" }
$SymbolCheck = Join-Path $Root 'Core\CheckCoreSymbols.ps1'
& $SymbolCheck -LibraryPath $CoreLibrary
if ($Target -eq 'All') {
# The standalone viewer retains a global compatibility build only for legacy comparison
# and audits. Those objects are linked into the viewer executable, never into Core.lib.
$CommandLine = 'call "{0}" >nul && cl.exe @"{1}" && cl.exe @"{2}"' -f $VcVars,$LegacyRsp,$ViewerRsp
Invoke-FieldViewerBuildCommand $CommandLine 'standalone viewer compile/link step'
}
if ($Target -eq 'All' -and -not (Test-Path -LiteralPath $OutputPath -PathType Leaf)) { throw "Viewer was not produced: $OutputPath" }
} finally {
Pop-Location
}
Write-Host "Built $CoreLibrary"
if ($Target -eq 'All') { Write-Host "Built $OutputPath" }
@@ -0,0 +1,50 @@
[CmdletBinding()]
param(
[string]$ReferenceLog = (Join-Path $PSScriptRoot '..\speed_round\gpu_field_head_source_w8.stdout.log'),
[string]$CandidateLog = (Join-Path $PSScriptRoot '..\speed_round\gpu_field_shared_far_w8.stdout.log')
)
$ErrorActionPreference = 'Stop'
$HashPattern = '\[(StreamFullDetail|StreamFullClipmap|StreamLevel)\].*?(?:lod=(\d+).*?)?density_hash=(0x[0-9a-fA-F]+) mesh_hash=(0x[0-9a-fA-F]+) payload_hash=(0x[0-9a-fA-F]+) topology_hash=(0x[0-9a-fA-F]+)'
function Read-Hashes([string]$Path) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Hash log not found: $Path" }
$Hashes = @{}
foreach ($Line in Get-Content -LiteralPath $Path) {
if ($Line -match $HashPattern) {
$Kind = $Matches[1]
$Key = if ($Kind -eq 'StreamLevel') { "lod$($Matches[2])" } else { $Kind }
$Hashes[$Key] = @($Matches[3], $Matches[4], $Matches[5], $Matches[6])
}
}
return $Hashes
}
$Reference = Read-Hashes $ReferenceLog
$Candidate = Read-Hashes $CandidateLog
$Differences = 0
$Fields = 0
foreach ($Key in $Reference.Keys) {
if (-not $Candidate.ContainsKey($Key)) {
Write-Output "MISSING candidate=$Key"
$Differences += 4
continue
}
for ($Index = 0; $Index -lt 4; ++$Index) {
++$Fields
if ($Reference[$Key][$Index] -ne $Candidate[$Key][$Index]) {
++$Differences
Write-Output "DIFF key=$Key field=$Index reference=$($Reference[$Key][$Index]) candidate=$($Candidate[$Key][$Index])"
}
}
}
foreach ($Key in $Candidate.Keys) {
if (-not $Reference.ContainsKey($Key)) {
Write-Output "EXTRA candidate=$Key"
$Differences += 4
}
}
$Status = if ($Differences -eq 0 -and $Fields -eq 44 -and $Reference.Count -eq 11 -and $Candidate.Count -eq 11) { 'PASS' } else { 'FAIL' }
Write-Output "[FarCpuHashCompare] status=$Status hash_fields=$Fields differences=$Differences reference_keys=$($Reference.Count) candidate_keys=$($Candidate.Count)"
if ($Status -ne 'PASS') { exit 1 }
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,69 @@
# GPU field port, step 2
## Result
This is a **far-field tile prototype**, not the requested viewer port. It now evaluates coarse shape and strata distances in an HLSL compute shader, then runs the existing GPU marching-cubes path. The CPU still compiles the world and exports selected tile records, candidate shape references, and exact rock/air and protected-air sign words. The CPU sign words drive the cell cases; the GPU does not choose topology.
The field math shared so far is in [VoxelForgeFieldShared.h](../../../Core/src/VoxelForgeFieldShared.h). Its C++/HLSL subset contains analytic ellipsoid, capsule, tapered/smooth capsule, and rounded-box distances; flat and profiled floors; smooth min/max; integer lattice hashing; value noise and the surface-relief sum; and edge-root refinement. The CPU far `CoarseRockDistance` path now calls the shared primitive-distance, floor, and blend functions. The GPU `EvaluateFarFieldCS` uses the same primitive-distance, floor, and blend source for its coarse shapes and strata. The fine LOD0/1 field, partition-of-unity detail relief, and its evaluator remain CPU-only. Although the shared relief helper exists, this far shader does not apply relief: the current canonical coarse field has no fine relief term. Strata shell repair geometry and connector shell patches are not generated by this port.
The current upload record also contains the exact per-sample candidate ranges/IDs and CPU sign words needed to preserve decisions. `FarFieldTileInput` exports strata-boundary records, but the current shader only consumes the coarse lower/upper stratum heights and blend width; it does not yet use the boundary list to emit shells.
## CPU reference hashes
The current shared-field build produced the same 44 density, mesh, payload, and topology hash fields as the fresh `/MP8` build from the clean `566d1f3` CPU source (`d4b4377` parent). [CompareFarCpuHashes.ps1](CompareFarCpuHashes.ps1) reports **44/44 fields equal** for the retained W8 logs. This comparison uses a fresh build from the current CPU source as the reference. The older retained `VoxelForgeRound11Final.exe` hash log is stale and does not match a fresh build of that source.
The input and comparison logs are [baseline](../speed_round/gpu_field_head_source_w8.stdout.log) and [shared field](../speed_round/gpu_field_shared_far_w8.stdout.log). No canonical LOD0/1 evaluator was redirected through float GPU math.
## Tile measurements
The run selected one surface-intersecting 16³-cell tile from each LOD2 through LOD8, 6,859 lattice samples per tile. The GPU was an AMD Radeon RX 7900 XTX. WARP was Microsoft’s D3D11 software device at feature level 11_0. These are seven representative tiles, not all far tiles in the view.
| Measurement across the 7 selected tiles | RX 7900 XTX | WARP | CPU reference |
| --- | ---: | ---: | ---: |
| Field evaluation timestamps | 0.1062 ms | 6.9466 ms | included in `generateTile` below |
| Field + GPU meshing timestamps | 0.3349 ms | 75.2699 ms | — |
| Full D3D wall time including upload, dispatch, and readback | 9.5323 ms summed | 136.3023 ms summed | — |
| `World::generateTile` wall time, sequential sum | — | — | 15.8055 ms |
| CPU shape/sign export and upload preparation, sequential sum | — | — | 5.8304 ms |
The measured 0.3349 ms is compute-only field plus meshing across these seven probes. The seven-tile D3D wall total includes per-tile setup and readback and is not a viewer frame cost. The CPU API time is for the same seven keys, generated sequentially by the probe, not the full-view eight-worker total. On a 90 Hz display the full frame budget is 11.11 ms; these seven kernel dispatches account for 0.335 ms (3.0% of that budget), or fit within a 0.5 ms compute slice by themselves. This says nothing about scheduling all 1,266 LOD2+ tiles, viewer draw cost, or a steady-state frame because neither is integrated.
The current CPU-only W8 clipmap benchmark took 0.691 s cold for 2,226 tiles in the baseline-source build. There is **no cold whole-view measurement with GPU far plus CPU near** yet, and no GPU chunk-crossing measurement in this step.
## Agreement and repeatability
Across all 7 tiles (48,013 lattice samples), GPU-vs-CPU and WARP-vs-CPU had zero sign differences. The largest absolute field-value difference was `8.3923e-5` voxels on both adapters; per-tile mean absolute error was `5.98e-8` to `1.384e-6` voxels. Each GPU and WARP cell case was derived from the CPU’s exact sign mask, so there were zero cell case differences by construction. RX repeat runs had zero differing vertex-component bits, cases, or indirect-draw arguments on every tested tile.
The vertex metric is the symmetric mean/max of the two directed nearest-vertex distances (CPU-to-GPU and GPU-to-CPU); it is not a continuous-surface Hausdorff distance. Per tile:
| LOD | RX field max error / WARP max error (vox) | RX/CPU vertex mean / max (cm) | WARP/CPU vertex mean / max (cm) | RX/WARP vertex mean / max (cm) | Case diffs RX / WARP |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 2 | 8.3923e-5 / 8.3923e-5 | 7.8943e-5 / 0.0029297 | 7.7662e-5 / 0.0029297 | 1.5237e-5 / 0.00097656 | 0 / 0 |
| 3 | 9.5367e-6 / 9.5367e-6 | 7.0617e-5 / 0.00097656 | 7.5598e-5 / 0.00097656 | 6.8048e-6 / 0.00024414 | 0 / 0 |
| 4 | 1.1444e-5 / 9.5367e-6 | 1.3605e-4 / 0.0097656 | 1.3279e-4 / 0.0097656 | 1.8673e-5 / 0.0019531 | 0 / 0 |
| 5 | 1.5736e-5 / 1.5259e-5 | 1.8690e-4 / 0.0078125 | 2.4954e-4 / 0.0078125 | 1.4741e-4 / 0.0078125 | 0 / 0 |
| 6 | 3.0518e-5 / 3.0518e-5 | 8.7221e-5 / 0.0078125 | 5.6992e-5 / 0.0078125 | 5.0486e-5 / 0.0039063 | 0 / 0 |
| 7 | 3.0518e-5 / 3.0518e-5 | 1.0709e-4 / 0.015625 | 7.8328e-5 / 0.015625 | 7.7681e-5 / 0.015625 | 0 / 0 |
| 8 | 2.2888e-5 / 2.2888e-5 | 1.5299e-4 / 0.03125 | 1.3583e-4 / 0.03125 | 5.6220e-5 / 0.015625 | 0 / 0 |
Across tiles, the mean of the per-tile means was `0.0001171 cm` (about `1.17 µm`) for RX-vs-CPU and `0.0001152 cm` for WARP-vs-CPU. The largest per-tile maximum was `0.03125 cm` (0.3125 mm) for either CPU comparison. RX-vs-WARP mean-of-tile-means was `0.0000532 cm`, with a maximum of `0.015625 cm`. Their float outputs are not bit-identical: the field math differs slightly on the two implementations, while their sign masks and classified topology agree.
No selected tile contained a protected-air sample or critical cell. Therefore these probes do not establish route-critical or thin-wall behavior; the route-critical sample count was zero, thin features were not audited, and the required “zero differences on protected/thin features” result remains unverified. The GPU far-survival audit against shell-repaired meshes was not run.
The separate 4,096-point shared relief-helper probe was bit-identical between C++ and RX/WARP in this run. That verifies the helper on those points; it is not an end-to-end relief-field comparison because the far shader does not apply relief.
## GTX 1060-class estimate
Using the earlier first-port report’s nominal 61.4 TFLOPS RX 7900 XTX figure and the owner-provided 4.4 TFLOPS for the GTX 1060 class gives a peak-FP32 ratio of about 14:1. A direct shader-only scaling of the current seven-tile dispatch sum gives roughly **4.7 ms for field plus meshing across these seven probes** (about 0.67 ms per probe), of which the field-stage share is about 1.5 ms total. This is a rough extrapolation, not a measurement or a whole-view estimate. Pascal’s integer throughput, the field’s divergent candidate loops, clocks, and driver overhead make simple FP32 scaling especially uncertain.
## Missing deliverables
- The viewer does not yet draw the new tile buffers directly. The harness reads back GPU data for comparison.
- There is no whole-view GPU-far/CPU-near cold timing or scheduled per-frame whole-far slice measurement.
- Six fixed-view and four remote-view CPU-vs-GPU captures are not available.
- The route-critical/thin-feature GPU test and far-survival audit against GPU meshes are not available.
- GPU far shell repairs/connectors, the strata boundary list, and the relief term are not in the shader path; LOD0/1 detail relief has not been ported.
## Reproduction
The isolated build scripts are [BuildGpuFieldPort.ps1](BuildGpuFieldPort.ps1) and [BuildGpuFieldViewer.ps1](BuildGpuFieldViewer.ps1); the probe runner and final log are [RunGpuFieldPort.ps1](RunGpuFieldPort.ps1) and [far_field_lod2_8_fixed.stdout.log](far_field_lod2_8_fixed.stdout.log). Builds use `/MP8` and the host scripts and runner set BelowNormal priority with affinity `0xFF00`. The run reported peak process working set of 440.49 MiB and private memory of 611.45 MiB. The largest per-tile D3D buffer allocation was 3.135 MiB. No `0xC0000142` occurred. All build and result artifacts are under `Standalone\` with new output names; nothing was committed, pushed, or stashed.
@@ -0,0 +1,41 @@
[CmdletBinding()]
param([string]$LogStem='far_field_lod2_8')
$ErrorActionPreference='Stop'
if([IO.Path]::GetFileName($LogStem)-ne$LogStem){throw 'LogStem must be a filename stem.'}
$HostProcess=Get-Process -Id $PID
$HostProcess.PriorityClass='BelowNormal'
$HostProcess.ProcessorAffinity=[IntPtr]0xFF00
$StandaloneRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$ExecutablePath=Join-Path $StandaloneRoot 'bin\VoxelForgeGpuFieldPort.exe'
$StdoutPath=Join-Path $PSScriptRoot ($LogStem+'.stdout.log')
$StderrPath=Join-Path $PSScriptRoot ($LogStem+'.stderr.log')
if(-not(Test-Path -LiteralPath $ExecutablePath -PathType Leaf)){throw "GPU field runner not found: $ExecutablePath"}
for($Attempt=0;$Attempt -lt 2;$Attempt++)
{
if($Attempt -gt 0)
{
Write-Host '0xC0000142 encountered; waiting 60 seconds before the single permitted retry.'
Start-Sleep -Seconds 60
}
Remove-Item -LiteralPath $StdoutPath,$StderrPath -Force -ErrorAction SilentlyContinue
$Child=Start-Process -FilePath $ExecutablePath -ArgumentList @() -WorkingDirectory $StandaloneRoot `
-WindowStyle Hidden -PassThru -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$PeakWorkingSet=0L;$PeakPrivateMemory=0L
while(-not$Child.HasExited)
{
$Child.Refresh()
if($Child.HasExited){break}
$Child.PriorityClass='BelowNormal'
$Child.ProcessorAffinity=[IntPtr]0xFF00
$PeakWorkingSet=[Math]::Max($PeakWorkingSet,$Child.WorkingSet64)
$PeakPrivateMemory=[Math]::Max($PeakPrivateMemory,$Child.PrivateMemorySize64)
Start-Sleep -Milliseconds 50
}
$Child.WaitForExit();$Child.Refresh()
if($Child.ExitCode -eq -1073741502 -and $Attempt -eq 0){continue}
if($Child.ExitCode -eq -1073741502){throw '0xC0000142 repeated after its single retry; stopping.'}
Write-Output "RUN_EXIT pid=$($Child.Id) priority=BelowNormal affinity=0xFF00 peak_working_set_gb=$([math]::Round($PeakWorkingSet/1GB,2)) peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) exit_code=$($Child.ExitCode) stdout=$StdoutPath stderr=$StderrPath"
exit $Child.ExitCode
}
@@ -126,6 +126,73 @@ RWStructuredBuffer<uint> CellCases : register(u0);
RWStructuredBuffer<uint> CellLocalOffsets : register(u1);
RWStructuredBuffer<uint> GroupVertexCounts : register(u2);
struct FarFieldSampleRange { uint offset; uint count; };
StructuredBuffer<VF_FieldPrimitive> FarShapes : register(t10);
StructuredBuffer<FarFieldSampleRange> FarSampleShapeRanges : register(t11);
StructuredBuffer<uint> FarSampleShapeIds : register(t12);
StructuredBuffer<uint> FarRockAirSigns : register(t13);
StructuredBuffer<uint> FarProtectedAir : register(t14);
RWStructuredBuffer<float> FarFieldDensity : register(u7);
cbuffer FarFieldConstants : register(b1)
{
float FarStrataLo;
float FarStrataHi;
float FarStrataBlend;
uint FarSampleCount;
};
[numthreads(64,1,1)]
void EvaluateFarFieldCS(uint3 dispatchId : SV_DispatchThreadID)
{
const uint index = dispatchId.x;
if (index >= FarSampleCount) return;
const uint x = index % SampleDim;
const uint y = (index / SampleDim) % SampleDim;
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];
float cave = 100000.0f;
float overlay = -100000.0f;
[loop]
for (uint item = 0; item < range.count; ++item)
{
const VF_FieldPrimitive shape = FarShapes[FarSampleShapeIds[range.offset + item]];
if ((shape.flags & VF_FIELD_PRIMITIVE_CARVE) != 0)
{
if (position.x < shape.minVoxel[0] - 6 || position.x > shape.maxVoxel[0] + 6
|| position.y < shape.minVoxel[1] - 6 || position.y > shape.maxVoxel[1] + 6
|| position.z < shape.minVoxel[2] - 6 || position.z > shape.maxVoxel[2] + 6)
continue;
float distance = VF_FieldPrimitiveDistance(shape,
position.x, position.y, position.z);
if ((shape.flags & VF_FIELD_PRIMITIVE_HAS_FLOOR) != 0)
{
const float floorZ = VF_FieldPrimitiveFloorAt(shape, position.x, position.y);
distance = VF_FieldSmoothMax(distance, floorZ - position.z,
shape.floorBlendRadius);
}
cave = VF_FieldSmoothMin(cave, distance, shape.blendRadius);
}
else if (position.x >= shape.minVoxel[0] && position.x <= shape.maxVoxel[0]
&& position.y >= shape.minVoxel[1] && position.y <= shape.maxVoxel[1]
&& position.z >= shape.minVoxel[2] && position.z <= shape.maxVoxel[2])
{
overlay = max(overlay, -VF_FieldPrimitiveDistance(shape,
position.x, position.y, position.z));
}
}
const float base = min(position.z - FarStrataLo, FarStrataHi - position.z);
float rockDistance = VF_FieldSmoothMin(base, max(cave, overlay), FarStrataBlend);
const uint protectedAir = (FarProtectedAir[index >> 5] >> (index & 31)) & 1;
if (protectedAir != 0) rockDistance = min(rockDistance, -0.0625f);
const float rawDensity = -rockDistance;
const uint air = (FarRockAirSigns[index >> 5] >> (index & 31)) & 1;
const float magnitude = max(abs(rawDensity), 0.05f);
FarFieldDensity[index] = air != 0 ? magnitude : -magnitude;
}
groupshared uint ScanValues[256];
[numthreads(256,1,1)]