From b61ccff549bd1272b8913d7150ca38f3fb6f12c9 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 6 Oct 2026 08:02:02 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- .../include/VoxelForgeCore/VoxelForgeCore.h | 53 + Standalone/Core/src/ShapeWorldInternal.h | 198 ++- Standalone/Core/src/VoxelForgeCore.cpp | 18 + Standalone/Core/src/VoxelForgeFieldShared.h | 181 ++- .../gpu_field_port/BuildGpuFieldPort.ps1 | 105 ++ .../gpu_field_port/BuildGpuFieldViewer.ps1 | 142 ++ .../gpu_field_port/CompareFarCpuHashes.ps1 | 50 + .../gpu_field_port/GpuFieldPort.cpp | 1446 +++++++++++++++++ .../OwnerShapeWorld/gpu_field_port/REPORT.md | 69 + .../gpu_field_port/RunGpuFieldPort.ps1 | 41 + .../gpu_first_port/VoxelForgeGpuMesher.hlsl | 67 + 11 files changed, 2361 insertions(+), 9 deletions(-) create mode 100644 Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldPort.ps1 create mode 100644 Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldViewer.ps1 create mode 100644 Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareFarCpuHashes.ps1 create mode 100644 Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp create mode 100644 Standalone/Research/OwnerShapeWorld/gpu_field_port/REPORT.md create mode 100644 Standalone/Research/OwnerShapeWorld/gpu_field_port/RunGpuFieldPort.ps1 diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 61ba896..42a8019 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -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 shapes; + // One range per sample in the (cells+3)^3 gradient-halo lattice. + // Shape IDs preserve the CPU spatial-bin iteration order. + std::vector sampleShapeRanges; + std::vector sampleShapeIds; + // Bit 1 means air, matching the CPU density sign rule (density >= 0). + std::vector rockAirSigns; + // Critical route air is a subset of rockAirSigns and is always authoritative. + std::vector protectedAir; + std::vector 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 farConnectorSegments() const; std::vector farConnectorCandidates(Vec3i minVoxel, Vec3i maxVoxel) const; + std::optional exportFarFieldTile(const TileKey& key, + std::string& error) const; bool auditDetailPassing() const; bool auditGridStairIntegrity() const; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index eaa1898..19d2e43 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -6,6 +6,7 @@ #include "VoxelNoise.h" #include "VoxelStrateManager.h" #include "VoxelForgeCore/VoxelForgeCore.h" +#include "VoxelForgeFieldShared.h" #include #include @@ -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(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(Dim)*Dim*Dim; + std::vector> PerSample(SampleCount); + std::vector 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(LayerRanges[0].Lo);Out.strataHi=static_cast(LayerRanges[0].Hi);} + Out.strataBlend=StrataCaveBlendRadius; + if(Owner&&Owner->Manager) + { + const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast(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(std::max(8.0, + static_cast(Params.BoundarySealThickness)+8.0)); + const float Boundaries[2]={static_cast(Slot.BottomChunkZ)*CHUNK_SIZE, + static_cast(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=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 Remap(Primitives.size(),std::numeric_limits::max()); + Out.shapes.reserve(std::count(Used.begin(),Used.end(),uint8{1})); + for(uint32 Id=0;Id(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(S.center[A]); + G.capsuleStart[A]=static_cast(S.capsuleStart[A]); + G.capsuleDelta[A]=static_cast(S.capsuleDelta[A]); + G.roundedHalfExtent[A]=static_cast(S.roundedHalfExtent[A]); + } + for(int32 I=0;I<9;++I)G.q[I]=static_cast(S.q[I]); + G.minRadius=static_cast(S.minRadius); + G.capsuleAxisLengthSquared=static_cast(S.capsuleAxisLengthSquared); + G.capsuleAxisLength=static_cast(S.capsuleAxisLength); + G.capsuleStartRadius=static_cast(S.capsuleStartRadius); + G.capsuleEndRadius=static_cast(S.capsuleEndRadius); + G.profileStartT=static_cast(S.profileStartT);G.profileEndT=static_cast(S.profileEndT); + G.profileStartRadius=static_cast(S.profileStartRadius);G.profileEndRadius=static_cast(S.profileEndRadius); + G.radiusSlopeStart=static_cast(S.radiusSlopeStart);G.radiusSlopeEnd=static_cast(S.radiusSlopeEnd); + G.rounding=static_cast(S.rounding); + G.blendRadius=static_cast(S.blendRadius);G.floorBlendRadius=static_cast(S.floorBlendRadius); + G.floorCenterZ=static_cast(S.floorCenterZ);G.floorSlopeX=static_cast(S.floorSlopeX); + G.floorSlopeY=static_cast(S.floorSlopeY); + G.floorCurveOriginX=static_cast(S.floorCurveOriginX);G.floorCurveOriginY=static_cast(S.floorCurveOriginY); + G.floorCurveDeltaX=static_cast(S.floorCurveDeltaX);G.floorCurveDeltaY=static_cast(S.floorCurveDeltaY); + G.floorCurveLength=static_cast(S.floorCurveLength); + G.floorCurveStartZ=static_cast(S.floorCurveStartZ);G.floorCurveEndZ=static_cast(S.floorCurveEndZ); + G.floorCurveTangentStart=static_cast(S.floorCurveTangentStart); + G.floorCurveTangentEnd=static_cast(S.floorCurveTangentEnd); + G.floorCurveStartBlend=static_cast(S.floorCurveStartBlend); + G.floorCurveEndBlend=static_cast(S.floorCurveEndBlend); + Out.shapes.push_back(G); + } + uint64 CandidateCount=0; + for(const auto& Ids:PerSample)CandidateCount+=Ids.size(); + if(CandidateCount>std::numeric_limits::max()) + {Error="far tile candidate list exceeds the GPU 32-bit offset limit";return false;} + Out.sampleShapeIds.reserve(static_cast(CandidateCount)); + for(size_t I=0;I(Out.sampleShapeIds.size()), + static_cast(Ids.size())}; + for(const uint32 Id:Ids) + { + if(Id>=Remap.size()||Remap[Id]==std::numeric_limits::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(std::chrono::steady_clock::now()-Stage).count(); Stage=std::chrono::steady_clock::now(); BuildSpatialBins(SpatialBinSize,false); @@ -1407,6 +1533,64 @@ private: static std::array Cross(const std::array& A,const std::array& 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(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& Radii, const std::array& Axis0={1,0,0}, const std::array& 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(XP.Max[0]+DetailBand ||YP.Max[1]+DetailBand ||ZP.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(Floor-Z), + const double Floor=VF_FieldPrimitiveFloorAt(Shared,X,Y); + Distance=VF_FieldSmoothMax(Distance,static_cast(Floor-Z), static_cast(P.FloorBlendRadius)); } - Cave=SmoothMinDistance(Cave,Distance,static_cast(P.BlendRadius)); + Cave=VF_FieldSmoothMin(Cave,Distance,static_cast(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 LayerRanges; std::vector LayerFields; std::vector Primitives; + std::vector SharedFieldPrimitives; std::vector FarConnectorSegments; std::unordered_map,FBinHash> FarConnectorSpatialBins; std::vector EditBrushes; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 398544a..5f4f401 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -733,6 +733,24 @@ namespace VoxelForgeCore return result; } + std::optional 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.lodconfiguration.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<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(); } diff --git a/Standalone/Core/src/VoxelForgeFieldShared.h b/Standalone/Core/src/VoxelForgeFieldShared.h index 7a5946e..c0cb70e 100644 --- a/Standalone/Core/src/VoxelForgeFieldShared.h +++ b/Standalone/Core/src/VoxelForgeFieldShared.h @@ -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 #include #include #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(v) #define VF_FIELD_CAST_U32(v) static_cast(v) #define VF_FIELD_CAST_F32(v) static_cast(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 diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldPort.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldPort.ps1 new file mode 100644 index 0000000..015edba --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldPort.ps1 @@ -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" diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldViewer.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldViewer.ps1 new file mode 100644 index 0000000..cd1207c --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldViewer.ps1 @@ -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" } diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareFarCpuHashes.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareFarCpuHashes.ps1 new file mode 100644 index 0000000..9b0007a --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareFarCpuHashes.ps1 @@ -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 } diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp new file mode 100644 index 0000000..ab1bd84 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp @@ -0,0 +1,1446 @@ +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include "CoreMinimal.h" +#include "VoxelForgeCore/VoxelForgeCore.h" +#include "VoxelForgeFieldShared.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using int32 = std::int32_t; +#include "MarchingCubesTables.h" + +#include +#include +#include +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; +using Clock = std::chrono::steady_clock; +using namespace VoxelForgeCore; + +namespace +{ + constexpr std::uint32_t InvalidIndex = 0xffffffffu; + constexpr int Corner[8][3] = { + {0,0,0},{1,0,0},{1,1,0},{0,1,0},{0,0,1},{1,0,1},{1,1,1},{0,1,1}}; + constexpr int Edge[12][2] = { + {0,1},{1,2},{2,3},{3,0},{4,5},{5,6},{6,7},{7,4},{0,4},{1,5},{2,6},{3,7}}; + + struct CpuScheduler final : JobScheduler + { + double parallel_for(std::size_t count, std::size_t batchSize, std::size_t workerLimit, + void* context, Task task, double* active, + std::size_t activeCapacity) const override + { + if (count == 0) return 0.0; + const std::size_t workers = std::clamp(workerLimit, 1, + std::min(8, count)); + const std::size_t grain = std::max(1, batchSize); + std::atomic next{0}; + std::vector elapsed(workers, 0.0); + std::vector threads; + threads.reserve(workers); + for (std::size_t worker = 0; worker < workers; ++worker) + { + threads.emplace_back([&, worker] + { + const auto start = Clock::now(); + for (;;) + { + const std::size_t first = next.fetch_add(grain, std::memory_order_relaxed); + if (first >= count) break; + const std::size_t end = first + std::min(grain, count - first); + for (std::size_t i = first; i < end; ++i) task(context, i, worker); + } + elapsed[worker] = std::chrono::duration(Clock::now() - start).count(); + }); + } + double total = 0.0; + for (std::size_t i = 0; i < threads.size(); ++i) + { + threads[i].join(); + total += elapsed[i]; + if (active && i < activeCapacity) active[i] = elapsed[i]; + } + return total; + } + }; + + struct MidpointRecord + { + std::uint32_t metadata[4]{}; + float values[12]{}; + }; + static_assert(sizeof(MidpointRecord) == 64); + + struct GpuVertex + { + float position[3]; + float normal[3]; + float uv[2]; + }; + static_assert(sizeof(GpuVertex) == 32); + + struct TileConstants + { + std::uint32_t cells, sampleDim, cellCount, groupCount; + std::int32_t originX, originY, originZ, stepVoxels; + float voxelSizeCm, pad[3]{}; + }; + static_assert(sizeof(TileConstants) == 48); + + struct FarFieldConstants + { + float strataLo, strataHi, strataBlend; + std::uint32_t sampleCount; + }; + static_assert(sizeof(FarFieldConstants) == 16); + static_assert(sizeof(FarFieldPrimitiveGpu) == 224); + static_assert(sizeof(FarFieldSampleRange) == 8); + static_assert(sizeof(FarFieldStratumBoundary) == 8); + + struct NoiseProbeInput + { + float position[3]; + std::uint32_t seed; + }; + struct NoiseStats + { + double mean = 0.0; + float maximum = 0.0f; + std::uint64_t differingBits = 0; + }; + struct TileData + { + TileKey key; + std::int32_t origin[3]{}; + std::uint32_t cells = 0, dim = 0, step = 0; + std::vector density; + std::vector signs; + std::vector expectedCases; + std::vector criticalCellMask; + std::vector midpointIds; + std::vector midpointRecords; + std::vector cpuSurfaceVertices; + std::uint32_t expectedVertexCount = 0; + std::uint64_t criticalSamples = 0; + std::uint64_t criticalCells = 0; + }; + struct GpuResult + { + std::vector vertices; + std::vector cases; + std::vector fieldDensity; + std::array drawArgs{}; + bool sequentialIndices = false; + bool indexedIndirectArgsValid = false; + bool hasAdapterMemoryInfo = false; + std::uint64_t adapterMemoryBeforeBytes = 0; + std::uint64_t adapterMemoryAfterBytes = 0; + std::uint64_t adapterMemoryBudgetBytes = 0; + std::uint64_t d3dBufferBytes = 0; + double dispatchMs = 0.0; + double fieldMs = 0.0; + double wallMs = 0.0; + }; + + void Check(HRESULT hr, const char* label) + { + if (FAILED(hr)) + { + char text[256]{}; + std::snprintf(text, sizeof(text), "%s failed (0x%08lx)", label, + static_cast(hr)); + throw std::runtime_error(text); + } + } + + std::string ReadFile(const std::filesystem::path& path) + { + std::ifstream in(path, std::ios::binary); + if (!in) throw std::runtime_error("cannot open " + path.string()); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; + } + + void SetJsonInt(std::string& json, const std::string& key, int value) + { + const std::string needle = "\"" + key + "\""; + std::size_t pos = json.find(needle); + while (pos != std::string::npos) + { + const std::size_t colon = json.find(':', pos + needle.size()); + if (colon == std::string::npos) break; + std::size_t first = colon + 1; + while (first < json.size() && std::isspace(static_cast(json[first]))) ++first; + std::size_t last = first; + if (last < json.size() && (json[last] == '-' || json[last] == '+')) ++last; + while (last < json.size() && std::isdigit(static_cast(json[last]))) ++last; + json.replace(first, last - first, std::to_string(value)); + pos = json.find(needle, first + 1); + } + } + + std::uint32_t SampleIndex(const TileData& t, int x, int y, int z) + { + const std::uint32_t ux = static_cast(x + 1); + const std::uint32_t uy = static_cast(y + 1); + const std::uint32_t uz = static_cast(z + 1); + return (uz * t.dim + uy) * t.dim + ux; + } + + float Sample(const TileData& t, int x, int y, int z) + { + return t.density[SampleIndex(t, x, y, z)]; + } + + std::uint32_t CaseFromSigns(const TileData& t, int x, int y, int z) + { + std::uint32_t c = 0; + for (int corner = 0; corner < 8; ++corner) + if (Sample(t, x + Corner[corner][0], y + Corner[corner][1], z + Corner[corner][2]) >= 0.0f) + c |= 1u << corner; + return c; + } + + std::array TileOrigin(const TileKey& key) + { + const std::int32_t extent = 32 << key.lod; + return {key.coordinate.x * extent, key.coordinate.y * extent, key.coordinate.z * extent}; + } + + std::uint32_t StepFor(const WorldConfiguration& config, const TileKey& key, + std::uint32_t& cells) + { + const std::uint32_t extent = 32u << key.lod; + cells = static_cast(key.lod < config.fullResolutionLodCount + ? 32 : std::max(1, config.coarseTileCells)); + return std::max(1u, extent / cells); + } + + std::vector GetFullDesired(World& world, const WorldConfiguration& config, + Vec3d camera) + { + return world.desiredTiles(camera); + } + + bool BuildRequestForDesired(std::span keys, const WorldConfiguration& config, + ShapeBuildRequest& request) + { + Vec3i fineMin{INT32_MAX,INT32_MAX,INT32_MAX}; + Vec3i fineMax{INT32_MIN,INT32_MIN,INT32_MIN}; + Vec3i clipMin{INT32_MAX,INT32_MAX,INT32_MAX}; + Vec3i clipMax{INT32_MIN,INT32_MIN,INT32_MIN}; + for (const TileKey& key : keys) + { + const auto origin = TileOrigin(key); + const std::int32_t extent = 32 << key.lod; + for (int axis = 0; axis < 3; ++axis) + { + (&clipMin.x)[axis] = std::min((&clipMin.x)[axis], origin[axis]); + (&clipMax.x)[axis] = std::max((&clipMax.x)[axis], origin[axis] + extent); + if (key.lod < config.fullResolutionLodCount) + { + (&fineMin.x)[axis] = std::min((&fineMin.x)[axis], origin[axis]); + (&fineMax.x)[axis] = std::max((&fineMax.x)[axis], origin[axis] + extent); + } + } + } + if (fineMin.x == INT32_MAX || clipMin.x == INT32_MAX) return false; + request.minVoxel = fineMin; + request.maxVoxel = fineMax; + request.clipMinVoxel = clipMin; + request.clipMaxVoxel = clipMax; + request.workerLimit = 8; + request.detail = DetailVariant::coherent; + request.includeFarWorld = true; + return true; + } + + float RefineT(float a, float b, float midpoint, bool hasMidpoint) + { + return VF_FieldRefineRoot(a, b, midpoint, hasMidpoint ? 1u : 0u); + } + + TileData MakeTileData(World& world, const TileKey& key, const WorldConfiguration& config, + const TileMesh& cpuMesh, bool auditCritical) + { + TileData t; + t.key = key; + const auto origin = TileOrigin(key); + std::copy(origin.begin(), origin.end(), t.origin); + t.step = StepFor(config, key, t.cells); + t.dim = t.cells + 3; + const std::size_t sampleCount = static_cast(t.dim) * t.dim * t.dim; + t.density.resize(sampleCount); + t.signs.assign((sampleCount + 31) / 32, 0u); + for (int z = -1; z <= static_cast(t.cells) + 1; ++z) + for (int y = -1; y <= static_cast(t.cells) + 1; ++y) + for (int x = -1; x <= static_cast(t.cells) + 1; ++x) + { + const std::uint32_t i = SampleIndex(t, x, y, z); + const Vec3i p{t.origin[0] + x * static_cast(t.step), + t.origin[1] + y * static_cast(t.step), + t.origin[2] + z * static_cast(t.step)}; + const float d = world.densityAtLattice(p); + t.density[i] = d; + if (d >= 0.0f) t.signs[i >> 5] |= 1u << (i & 31); + if (auditCritical && world.debugCriticalAirAt({double(p.x), double(p.y), double(p.z)})) + ++t.criticalSamples; + } + + if (cpuMesh.debugDensitySamples.size() != sampleCount) + throw std::runtime_error("CPU mesher did not return a complete halo grid for the selected tile"); + for (std::size_t i = 0; i < sampleCount; ++i) + { + if (std::bit_cast(t.density[i]) + != std::bit_cast(cpuMesh.debugDensitySamples[i])) + throw std::runtime_error("independent CPU lattice query differs from the reference mesher grid"); + } + + const std::uint32_t cellCount = t.cells * t.cells * t.cells; + t.expectedCases.resize(cellCount); + t.criticalCellMask.assign(cellCount, 0); + t.midpointIds.assign(cellCount, InvalidIndex); + for (std::uint32_t z = 0; z < t.cells; ++z) + for (std::uint32_t y = 0; y < t.cells; ++y) + for (std::uint32_t x = 0; x < t.cells; ++x) + { + const std::uint32_t cell = (z * t.cells + y) * t.cells + x; + const std::uint32_t mcCase = CaseFromSigns(t, static_cast(x), static_cast(y), static_cast(z)); + t.expectedCases[cell] = mcCase; + std::uint32_t vertexCount = 0; + while (vertexCount < 15 && MCTriTable[mcCase][vertexCount] >= 0) ++vertexCount; + t.expectedVertexCount += vertexCount; + if (auditCritical) + { + bool hasCritical = false; + for (int corner = 0; corner < 8 && !hasCritical; ++corner) + { + const Vec3d p{double(t.origin[0] + (static_cast(x) + Corner[corner][0]) * t.step), + double(t.origin[1] + (static_cast(y) + Corner[corner][1]) * t.step), + double(t.origin[2] + (static_cast(z) + Corner[corner][2]) * t.step)}; + hasCritical = world.debugCriticalAirAt(p); + } + if (hasCritical) + { + t.criticalCellMask[cell] = 1; + ++t.criticalCells; + } + } + if (MCEdgeTable[mcCase] == 0) continue; + + MidpointRecord record{}; + for (int edge = 0; edge < 12; ++edge) + { + if ((MCEdgeTable[mcCase] & (1 << edge)) == 0 || key.lod != 0) continue; + const int a = Edge[edge][0], b = Edge[edge][1]; + const int ax = static_cast(x) + Corner[a][0]; + const int ay = static_cast(y) + Corner[a][1]; + const int az = static_cast(z) + Corner[a][2]; + const int bx = static_cast(x) + Corner[b][0]; + const int by = static_cast(y) + Corner[b][1]; + const int bz = static_cast(z) + Corner[b][2]; + const float da = Sample(t, ax, ay, az); + const float db = Sample(t, bx, by, bz); + float linear = (std::abs(db - da) < 1.0e-4f) ? 0.5f : -da / (db - da); + linear = std::clamp(linear, 0.0f, 1.0f); + if (linear < 0.375f || linear > 0.625f) continue; + + const float baseCm[3] = {static_cast(t.origin[0]) * 25.0f, + static_cast(t.origin[1]) * 25.0f, static_cast(t.origin[2]) * 25.0f}; + const float paCm[3] = {baseCm[0] + static_cast(ax * t.step) * 25.0f, + baseCm[1] + static_cast(ay * t.step) * 25.0f, + baseCm[2] + static_cast(az * t.step) * 25.0f}; + const float pbCm[3] = {baseCm[0] + static_cast(bx * t.step) * 25.0f, + baseCm[1] + static_cast(by * t.step) * 25.0f, + baseCm[2] + static_cast(bz * t.step) * 25.0f}; + const Vec3d midpointVoxel{ + ((paCm[0] + pbCm[0]) * 0.5f) / 25.0f, + ((paCm[1] + pbCm[1]) * 0.5f) / 25.0f, + ((paCm[2] + pbCm[2]) * 0.5f) / 25.0f}; + const float mid = world.unclampedDensityAt(midpointVoxel); + record.values[edge] = mid; + record.metadata[0] |= 1u << edge; + } + if (record.metadata[0] != 0) + { + t.midpointIds[cell] = static_cast(t.midpointRecords.size()); + t.midpointRecords.push_back(record); + } + } + for (std::uint32_t i = 0; i < cpuMesh.groups.surfaceVertexCount; ++i) + { + const MeshVertex& in = cpuMesh.vertices[i]; + GpuVertex out{}; + out.position[0] = static_cast(in.position.x); + out.position[1] = static_cast(in.position.y); + out.position[2] = static_cast(in.position.z); + out.normal[0] = static_cast(in.normal.x); + out.normal[1] = static_cast(in.normal.y); + out.normal[2] = static_cast(in.normal.z); + t.cpuSurfaceVertices.push_back(out); + } + return t; + } + + ComPtr CompileShader(const std::filesystem::path& shaderPath, + const char* entry) + { + ComPtr bytecode, errors; + const std::wstring path = shaderPath.wstring(); + const HRESULT hr = D3DCompileFromFile(path.c_str(), nullptr, + D3D_COMPILE_STANDARD_FILE_INCLUDE, entry, "cs_5_0", + D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_IEEE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, + 0, &bytecode, &errors); + if (FAILED(hr)) + { + std::string detail; + if (errors && errors->GetBufferPointer()) + detail.assign(static_cast(errors->GetBufferPointer()), errors->GetBufferSize()); + throw std::runtime_error(std::string("D3DCompile ") + entry + " failed: " + detail); + } + return bytecode; + } + + struct DeviceRun + { + ComPtr device; + ComPtr context; + ComPtr fieldEval, classify, scan, emit, noiseProbe; + ComPtr memoryAdapter; + std::string name; + UINT64 timestampFrequency = 0; + + void Initialize(const std::filesystem::path& shaderPath, bool warp) + { + D3D_FEATURE_LEVEL requested[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0}; + D3D_FEATURE_LEVEL created{}; + HRESULT hr = D3D11CreateDevice(nullptr, + warp ? D3D_DRIVER_TYPE_WARP : D3D_DRIVER_TYPE_HARDWARE, + nullptr, D3D11_CREATE_DEVICE_SINGLETHREADED, + requested, 2, D3D11_SDK_VERSION, &device, &created, &context); + if (hr == E_INVALIDARG) + hr = D3D11CreateDevice(nullptr, + warp ? D3D_DRIVER_TYPE_WARP : D3D_DRIVER_TYPE_HARDWARE, + nullptr, D3D11_CREATE_DEVICE_SINGLETHREADED, + requested + 1, 1, D3D11_SDK_VERSION, &device, &created, &context); + Check(hr, warp ? "D3D11CreateDevice(WARP)" : "D3D11CreateDevice(HARDWARE)"); + name = warp ? "WARP" : "hardware"; + ComPtr dxgiDevice; + ComPtr adapter; + DXGI_ADAPTER_DESC desc{}; + if (SUCCEEDED(device.As(&dxgiDevice)) && SUCCEEDED(dxgiDevice->GetAdapter(&adapter)) + && SUCCEEDED(adapter->GetDesc(&desc))) + { + char narrow[256]{}; + WideCharToMultiByte(CP_UTF8, 0, desc.Description, -1, narrow, sizeof(narrow), nullptr, nullptr); + if (!warp) name = narrow; + adapter.As(&memoryAdapter); + } + ComPtr code = CompileShader(shaderPath, "EvaluateFarFieldCS"); + Check(device->CreateComputeShader(code->GetBufferPointer(), code->GetBufferSize(), nullptr, &fieldEval), "CreateComputeShader(EvaluateFarFieldCS)"); + code = CompileShader(shaderPath, "ClassifyCS"); + Check(device->CreateComputeShader(code->GetBufferPointer(), code->GetBufferSize(), nullptr, &classify), "CreateComputeShader(ClassifyCS)"); + code = CompileShader(shaderPath, "ScanGroupsCS"); + Check(device->CreateComputeShader(code->GetBufferPointer(), code->GetBufferSize(), nullptr, &scan), "CreateComputeShader(ScanGroupsCS)"); + code = CompileShader(shaderPath, "EmitCS"); + Check(device->CreateComputeShader(code->GetBufferPointer(), code->GetBufferSize(), nullptr, &emit), "CreateComputeShader(EmitCS)"); + code = CompileShader(shaderPath, "NoiseProbeCS"); + Check(device->CreateComputeShader(code->GetBufferPointer(), code->GetBufferSize(), nullptr, &noiseProbe), "CreateComputeShader(NoiseProbeCS)"); + } + + bool QueryLocalMemory(std::uint64_t& usage, std::uint64_t& budget) const + { + if (!memoryAdapter) return false; + DXGI_QUERY_VIDEO_MEMORY_INFO info{}; + if (FAILED(memoryAdapter->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &info))) + return false; + usage = info.CurrentUsage; + budget = info.Budget; + return true; + } + + template + ComPtr Structured(const T* data, std::size_t count, UINT bindFlags, + UINT miscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED) + { + if (count == 0) count = 1; + if (count * sizeof(T) > std::numeric_limits::max()) + throw std::runtime_error("D3D11 structured buffer exceeds ByteWidth limit"); + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = static_cast(count * sizeof(T)); + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = bindFlags; + desc.MiscFlags = miscFlags; + desc.StructureByteStride = sizeof(T); + D3D11_SUBRESOURCE_DATA initial{}; + initial.pSysMem = data; + ComPtr buffer; + const HRESULT hr = device->CreateBuffer(&desc, data ? &initial : nullptr, &buffer); + if (FAILED(hr)) + { + char message[256]{}; + std::snprintf(message, sizeof(message), + "CreateBuffer(structured) failed: count=%zu stride=%zu bytes=%u bind=0x%x misc=0x%x hr=0x%08lx", + count, sizeof(T), desc.ByteWidth, desc.BindFlags, desc.MiscFlags, + static_cast(hr)); + throw std::runtime_error(message); + } + return buffer; + } + + template + ComPtr Srv(ID3D11Buffer* buffer, std::size_t count) + { + D3D11_SHADER_RESOURCE_VIEW_DESC desc{}; + desc.Format = DXGI_FORMAT_UNKNOWN; + desc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER; + desc.Buffer.NumElements = static_cast(count); + ComPtr view; + Check(device->CreateShaderResourceView(buffer, &desc, &view), "CreateShaderResourceView"); + return view; + } + + ComPtr Uav(ID3D11Buffer* buffer, std::size_t count) + { + D3D11_UNORDERED_ACCESS_VIEW_DESC desc{}; + desc.Format = DXGI_FORMAT_UNKNOWN; + desc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; + desc.Buffer.NumElements = static_cast(count); + ComPtr view; + Check(device->CreateUnorderedAccessView(buffer, &desc, &view), "CreateUnorderedAccessView"); + return view; + } + + ComPtr Staging(UINT byteWidth) + { + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = byteWidth; + desc.Usage = D3D11_USAGE_STAGING; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + ComPtr result; + Check(device->CreateBuffer(&desc, nullptr, &result), "CreateBuffer(staging)"); + return result; + } + + ComPtr RawMeshBuffer(UINT byteWidth, UINT bindFlags, + UINT miscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS) + { + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = byteWidth; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = bindFlags; + desc.MiscFlags = miscFlags; + ComPtr result; + Check(device->CreateBuffer(&desc, nullptr, &result), "CreateBuffer(raw mesh output)"); + return result; + } + + ComPtr RawUav(ID3D11Buffer* buffer, UINT dwordCount) + { + D3D11_UNORDERED_ACCESS_VIEW_DESC desc{}; + desc.Format = DXGI_FORMAT_R32_TYPELESS; + desc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; + desc.Buffer.NumElements = dwordCount; + desc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW; + ComPtr view; + Check(device->CreateUnorderedAccessView(buffer, &desc, &view), "CreateUnorderedAccessView(raw)"); + return view; + } + + void WaitForQuery(ID3D11Query* query) + { + for (;;) + { + const HRESULT hr = context->GetData(query, nullptr, 0, 0); + if (hr == S_FALSE) { std::this_thread::yield(); continue; } + Check(hr, "GetData(query completion)"); + return; + } + } + + template + T ReadQuery(ID3D11Query* query, const char* label) + { + T value{}; + for (;;) + { + const HRESULT hr = context->GetData(query, &value, sizeof(value), 0); + if (hr == S_FALSE) { std::this_thread::yield(); continue; } + Check(hr, label); + return value; + } + } + + GpuResult Run(const TileData& tile, const FarFieldTileInput* farField = nullptr, + bool readback = true) + { + using namespace std::chrono; + const auto wallStart = Clock::now(); + std::uint64_t localUsage = 0, localBudget = 0; + const bool hasLocalBefore = QueryLocalMemory(localUsage, localBudget); + GpuResult result; + if (hasLocalBefore) result.adapterMemoryBeforeBytes = localUsage; + const std::uint32_t cellCount = tile.cells * tile.cells * tile.cells; + const std::uint32_t groupCount = (cellCount + 255) / 256; + const std::size_t triCount = 256 * 16; + std::array tri{}; + std::array edge{}; + for (std::size_t c = 0; c < 256; ++c) + { + edge[c] = static_cast(MCEdgeTable[c]); + for (std::size_t slot = 0; slot < 16; ++slot) tri[c * 16 + slot] = MCTriTable[c][slot]; + } + (void)edge; + const std::size_t maxVertices = static_cast(cellCount) * 15; + if (farField && (farField->sampleDim != static_cast(tile.dim) + || farField->cells != static_cast(tile.cells) + || farField->step != static_cast(tile.step))) + throw std::runtime_error("far field upload dimensions do not match the tile"); + result.d3dBufferBytes = tile.signs.size() * sizeof(std::uint32_t) + + triCount * sizeof(std::int32_t) + + tile.density.size() * sizeof(float) + + tile.midpointIds.size() * sizeof(std::uint32_t) + + std::max(1, tile.midpointRecords.size()) * sizeof(MidpointRecord) + + 2ull * cellCount * sizeof(std::uint32_t) + + 2ull * groupCount * sizeof(std::uint32_t) + + 5ull * sizeof(std::uint32_t) + + maxVertices * sizeof(GpuVertex) + + maxVertices * sizeof(std::uint32_t) + + sizeof(TileConstants); + if (farField) + result.d3dBufferBytes += std::max(1, farField->shapes.size()) * sizeof(FarFieldPrimitiveGpu) + + farField->sampleShapeRanges.size() * sizeof(FarFieldSampleRange) + + std::max(1, farField->sampleShapeIds.size()) * sizeof(std::uint32_t) + + farField->rockAirSigns.size() * sizeof(std::uint32_t) + + farField->protectedAir.size() * sizeof(std::uint32_t) + + tile.density.size() * sizeof(float) + + sizeof(FarFieldConstants); + if (readback) + result.d3dBufferBytes += cellCount * sizeof(std::uint32_t) + + std::max(1, tile.expectedVertexCount) * sizeof(GpuVertex) + + std::max(1, tile.expectedVertexCount) * sizeof(std::uint32_t) + + 5 * sizeof(std::uint32_t); + auto signs = Structured(tile.signs.data(), tile.signs.size(), D3D11_BIND_SHADER_RESOURCE); + auto triBuffer = Structured(tri.data(), triCount, D3D11_BIND_SHADER_RESOURCE); + auto density = Structured(farField ? static_cast(nullptr) : tile.density.data(), + tile.density.size(), D3D11_BIND_SHADER_RESOURCE + | (farField ? D3D11_BIND_UNORDERED_ACCESS : 0)); + auto midpointIds = Structured(tile.midpointIds.data(), tile.midpointIds.size(), D3D11_BIND_SHADER_RESOURCE); + const MidpointRecord zeroRecord{}; + auto midpointRecords = Structured(tile.midpointRecords.empty() ? &zeroRecord : tile.midpointRecords.data(), + std::max(1, tile.midpointRecords.size()), D3D11_BIND_SHADER_RESOURCE); + auto cases = Structured(nullptr, cellCount, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE); + auto localOffsets = Structured(nullptr, cellCount, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE); + auto groupCounts = Structured(nullptr, groupCount, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE); + auto groupOffsets = Structured(nullptr, groupCount, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE); + auto args = RawMeshBuffer(5 * sizeof(std::uint32_t), D3D11_BIND_UNORDERED_ACCESS, + D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS | D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS); + auto vertices = RawMeshBuffer(static_cast(maxVertices * sizeof(GpuVertex)), + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_VERTEX_BUFFER); + auto indices = RawMeshBuffer(static_cast(maxVertices * sizeof(std::uint32_t)), + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_INDEX_BUFFER); + const FarFieldPrimitiveGpu zeroShape{}; + const FarFieldSampleRange zeroRange{}; + const std::uint32_t zeroId=0,zeroWord=0; + auto farShapes = farField ? Structured(farField->shapes.empty()?&zeroShape:farField->shapes.data(), + std::max(1,farField->shapes.size()),D3D11_BIND_SHADER_RESOURCE) + : ComPtr{}; + auto farRanges = farField ? Structured(farField->sampleShapeRanges.empty()?&zeroRange:farField->sampleShapeRanges.data(), + std::max(1,farField->sampleShapeRanges.size()),D3D11_BIND_SHADER_RESOURCE) + : ComPtr{}; + auto farIds = farField ? Structured(farField->sampleShapeIds.empty()?&zeroId:farField->sampleShapeIds.data(), + std::max(1,farField->sampleShapeIds.size()),D3D11_BIND_SHADER_RESOURCE) + : ComPtr{}; + auto farSigns = farField ? Structured(farField->rockAirSigns.empty()?&zeroWord:farField->rockAirSigns.data(), + std::max(1,farField->rockAirSigns.size()),D3D11_BIND_SHADER_RESOURCE) + : ComPtr{}; + auto farProtected = farField ? Structured(farField->protectedAir.empty()?&zeroWord:farField->protectedAir.data(), + std::max(1,farField->protectedAir.size()),D3D11_BIND_SHADER_RESOURCE) + : ComPtr{}; + auto signSrv = Srv(signs.Get(), tile.signs.size()); + auto triSrv = Srv(triBuffer.Get(), triCount); + auto densitySrv = Srv(density.Get(), tile.density.size()); + auto farShapeSrv = farField ? Srv(farShapes.Get(),std::max(1,farField->shapes.size())) + : ComPtr{}; + auto farRangeSrv = farField ? Srv(farRanges.Get(),std::max(1,farField->sampleShapeRanges.size())) + : ComPtr{}; + auto farIdSrv = farField ? Srv(farIds.Get(),std::max(1,farField->sampleShapeIds.size())) + : ComPtr{}; + auto farSignSrv = farField ? Srv(farSigns.Get(),std::max(1,farField->rockAirSigns.size())) + : ComPtr{}; + auto farProtectedSrv = farField ? Srv(farProtected.Get(),std::max(1,farField->protectedAir.size())) + : ComPtr{}; + auto midpointIdSrv = Srv(midpointIds.Get(), tile.midpointIds.size()); + auto midpointRecordSrv = Srv(midpointRecords.Get(), std::max(1, tile.midpointRecords.size())); + auto caseSrv = Srv(cases.Get(), cellCount); + auto localOffsetSrv = Srv(localOffsets.Get(), cellCount); + auto groupCountSrv = Srv(groupCounts.Get(), groupCount); + auto groupOffsetSrv = Srv(groupOffsets.Get(), groupCount); + auto caseUav = Uav(cases.Get(), cellCount); + auto localOffsetUav = Uav(localOffsets.Get(), cellCount); + auto groupCountUav = Uav(groupCounts.Get(), groupCount); + auto groupOffsetUav = Uav(groupOffsets.Get(), groupCount); + auto densityUav = farField ? Uav(density.Get(),tile.density.size()) + : ComPtr{}; + auto argsUav = RawUav(args.Get(), 5); + auto vertexUav = RawUav(vertices.Get(), static_cast(maxVertices * sizeof(GpuVertex) / sizeof(std::uint32_t))); + auto indexUav = RawUav(indices.Get(), static_cast(maxVertices)); + D3D11_BUFFER_DESC cbDesc{}; + cbDesc.ByteWidth = sizeof(TileConstants); + cbDesc.Usage = D3D11_USAGE_DEFAULT; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + TileConstants constants{tile.cells, tile.dim, cellCount, groupCount, + tile.origin[0], tile.origin[1], tile.origin[2], static_cast(tile.step), 25.0f,{}}; + D3D11_SUBRESOURCE_DATA cbData{}; cbData.pSysMem = &constants; + ComPtr cb; + Check(device->CreateBuffer(&cbDesc, &cbData, &cb), "CreateBuffer(constants)"); + ComPtr farCb; + if (farField) + { + const FarFieldConstants farConstants{farField->strataLo,farField->strataHi, + farField->strataBlend,static_cast(tile.density.size())}; + D3D11_BUFFER_DESC farDesc{};farDesc.ByteWidth=sizeof(FarFieldConstants); + farDesc.Usage=D3D11_USAGE_DEFAULT;farDesc.BindFlags=D3D11_BIND_CONSTANT_BUFFER; + D3D11_SUBRESOURCE_DATA farData{};farData.pSysMem=&farConstants; + Check(device->CreateBuffer(&farDesc,&farData,&farCb),"CreateBuffer(far constants)"); + } + + D3D11_QUERY_DESC timestampDesc{D3D11_QUERY_TIMESTAMP, 0}; + D3D11_QUERY_DESC disjointDesc{D3D11_QUERY_TIMESTAMP_DISJOINT, 0}; + ComPtr startQuery, endQuery, disjointQuery; + ComPtr fieldStartQuery,fieldEndQuery; + Check(device->CreateQuery(×tampDesc, &startQuery), "CreateQuery(start)"); + Check(device->CreateQuery(×tampDesc, &endQuery), "CreateQuery(end)"); + Check(device->CreateQuery(&disjointDesc, &disjointQuery), "CreateQuery(disjoint)"); + if(farField) + { + Check(device->CreateQuery(×tampDesc,&fieldStartQuery),"CreateQuery(field start)"); + Check(device->CreateQuery(×tampDesc,&fieldEndQuery),"CreateQuery(field end)"); + } + + context->CSSetConstantBuffers(0, 1, cb.GetAddressOf()); + context->Begin(disjointQuery.Get()); + context->End(startQuery.Get()); + if(farField) + { + context->End(fieldStartQuery.Get()); + context->CSSetConstantBuffers(1,1,farCb.GetAddressOf()); + context->CSSetShader(fieldEval.Get(),nullptr,0); + ID3D11ShaderResourceView* fieldSrvs[15]{}; + fieldSrvs[10]=farShapeSrv.Get();fieldSrvs[11]=farRangeSrv.Get(); + fieldSrvs[12]=farIdSrv.Get();fieldSrvs[13]=farSignSrv.Get(); + fieldSrvs[14]=farProtectedSrv.Get(); + context->CSSetShaderResources(0,15,fieldSrvs); + ID3D11UnorderedAccessView* fieldUavs[8]{};fieldUavs[7]=densityUav.Get(); + context->CSSetUnorderedAccessViews(0,8,fieldUavs,nullptr); + context->Dispatch(static_cast((tile.density.size()+63)/64),1,1); + ID3D11ShaderResourceView* clearSrvs[16]{}; + ID3D11UnorderedAccessView* clearUavs[8]{}; + context->CSSetShaderResources(0,16,clearSrvs); + context->CSSetUnorderedAccessViews(0,8,clearUavs,nullptr); + context->End(fieldEndQuery.Get()); + } + context->CSSetShader(classify.Get(), nullptr, 0); + ID3D11ShaderResourceView* classifySrvs[] = {signSrv.Get(), triSrv.Get()}; + context->CSSetShaderResources(0, 2, classifySrvs); + ID3D11UnorderedAccessView* classifyUavs[] = {caseUav.Get(), localOffsetUav.Get(), groupCountUav.Get()}; + context->CSSetUnorderedAccessViews(0, 3, classifyUavs, nullptr); + context->Dispatch(groupCount, 1, 1); + ID3D11ShaderResourceView* nullSrvs[16]{}; + ID3D11UnorderedAccessView* nullUavs[8]{}; + context->CSSetShaderResources(0, 16, nullSrvs); + context->CSSetUnorderedAccessViews(0, 8, nullUavs, nullptr); + + 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); + context->CSSetShaderResources(0, 16, nullSrvs); + context->CSSetUnorderedAccessViews(0, 8, nullUavs, nullptr); + + context->CSSetShader(emit.Get(), nullptr, 0); + ID3D11ShaderResourceView* emitSrvs[9]{}; + emitSrvs[1] = triSrv.Get(); emitSrvs[2] = densitySrv.Get(); emitSrvs[3] = caseSrv.Get(); + emitSrvs[4] = localOffsetSrv.Get(); emitSrvs[5] = groupOffsetSrv.Get(); + emitSrvs[6] = midpointIdSrv.Get(); emitSrvs[7] = midpointRecordSrv.Get(); + context->CSSetShaderResources(0, 9, emitSrvs); + ID3D11UnorderedAccessView* emitUavs[7]{}; emitUavs[5] = vertexUav.Get(); emitUavs[6] = indexUav.Get(); + context->CSSetUnorderedAccessViews(0, 7, emitUavs, nullptr); + context->Dispatch(groupCount, 1, 1); + context->CSSetShaderResources(0, 16, nullSrvs); + context->CSSetUnorderedAccessViews(0, 7, nullUavs, nullptr); + context->End(endQuery.Get()); + context->End(disjointQuery.Get()); + + if (readback) + { + auto caseStaging = Staging(cellCount * sizeof(std::uint32_t)); + auto fieldStaging = farField ? Staging(static_cast(tile.density.size()*sizeof(float))) + : ComPtr{}; + auto vertexStaging = Staging(static_cast(std::max(1, tile.expectedVertexCount) + * sizeof(GpuVertex))); + auto indexStaging = Staging(static_cast(std::max(1, tile.expectedVertexCount) + * sizeof(std::uint32_t))); + auto argsStaging = Staging(5 * sizeof(std::uint32_t)); + if (QueryLocalMemory(localUsage, localBudget)) + { + result.hasAdapterMemoryInfo = true; + result.adapterMemoryAfterBytes = localUsage; + result.adapterMemoryBudgetBytes = localBudget; + } + context->CopyResource(caseStaging.Get(), cases.Get()); + const UINT copyBytes = tile.expectedVertexCount * sizeof(GpuVertex); + if (copyBytes != 0) + { + const D3D11_BOX box{0, 0, 0, copyBytes, 1, 1}; + context->CopySubresourceRegion(vertexStaging.Get(), 0, 0, 0, 0, + vertices.Get(), 0, &box); + const D3D11_BOX indexBox{0, 0, 0, tile.expectedVertexCount * sizeof(std::uint32_t), 1, 1}; + context->CopySubresourceRegion(indexStaging.Get(), 0, 0, 0, 0, + indices.Get(), 0, &indexBox); + } + context->CopyResource(argsStaging.Get(), args.Get()); + if(farField)context->CopyResource(fieldStaging.Get(),density.Get()); + context->Flush(); + WaitForQuery(disjointQuery.Get()); + const D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint = ReadQuery(disjointQuery.Get(), "GetData(disjoint)"); + const UINT64 start = ReadQuery(startQuery.Get(), "GetData(start timestamp)"); + const UINT64 end = ReadQuery(endQuery.Get(), "GetData(end timestamp)"); + result.dispatchMs = disjoint.Disjoint ? 0.0 : + 1000.0 * static_cast(end - start) / static_cast(disjoint.Frequency); + D3D11_MAPPED_SUBRESOURCE mapped{}; + if(farField) + { + const UINT64 fieldStart=ReadQuery(fieldStartQuery.Get(),"GetData(field timestamp start)"); + const UINT64 fieldEnd=ReadQuery(fieldEndQuery.Get(),"GetData(field timestamp end)"); + result.fieldMs=disjoint.Disjoint?0.0:1000.0*static_cast(fieldEnd-fieldStart) + /static_cast(disjoint.Frequency); + Check(context->Map(fieldStaging.Get(),0,D3D11_MAP_READ,0,&mapped),"Map(far field density)"); + const auto* fieldData=static_cast(mapped.pData); + result.fieldDensity.assign(fieldData,fieldData+tile.density.size()); + context->Unmap(fieldStaging.Get(),0); + } + Check(context->Map(caseStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(cases)"); + result.cases.assign(static_cast(mapped.pData), + static_cast(mapped.pData) + cellCount); + context->Unmap(caseStaging.Get(), 0); + Check(context->Map(argsStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(args)"); + std::copy_n(static_cast(mapped.pData), 5, result.drawArgs.begin()); + context->Unmap(argsStaging.Get(), 0); + result.indexedIndirectArgsValid = result.drawArgs[0] == tile.expectedVertexCount + && result.drawArgs[1] == 1 && result.drawArgs[2] == 0 + && result.drawArgs[3] == 0 && result.drawArgs[4] == 0; + const std::uint32_t outputCount = std::min(result.drawArgs[0], + static_cast(maxVertices)); + // The staging buffer contains the GPU output. Only retain the prefix named by + // the indirect draw arguments, even though CopyResource transferred its capacity. + Check(context->Map(vertexStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(vertices)"); + const auto* begin = static_cast(mapped.pData); + result.vertices.assign(begin, begin + outputCount); + context->Unmap(vertexStaging.Get(), 0); + Check(context->Map(indexStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(indices)"); + const auto* indexData = static_cast(mapped.pData); + result.sequentialIndices = outputCount == tile.expectedVertexCount; + for (std::uint32_t i = 0; i < outputCount; ++i) + result.sequentialIndices &= indexData[i] == i; + context->Unmap(indexStaging.Get(), 0); + } + else + { + context->Flush(); + WaitForQuery(disjointQuery.Get()); + const D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint = ReadQuery(disjointQuery.Get(), "GetData(disjoint)"); + const UINT64 start = ReadQuery(startQuery.Get(), "GetData(start timestamp)"); + const UINT64 end = ReadQuery(endQuery.Get(), "GetData(end timestamp)"); + result.dispatchMs = disjoint.Disjoint ? 0.0 : + 1000.0 * static_cast(end - start) / static_cast(disjoint.Frequency); + if(farField) + { + const UINT64 fieldStart=ReadQuery(fieldStartQuery.Get(),"GetData(field timestamp start)"); + const UINT64 fieldEnd=ReadQuery(fieldEndQuery.Get(),"GetData(field timestamp end)"); + result.fieldMs=disjoint.Disjoint?0.0:1000.0*static_cast(fieldEnd-fieldStart) + /static_cast(disjoint.Frequency); + } + } + result.wallMs = duration(Clock::now() - wallStart).count(); + return result; + } + + std::vector RunNoiseProbe(const std::vector& inputs) + { + auto in = Structured(inputs.data(), inputs.size(), D3D11_BIND_SHADER_RESOURCE); + auto out = Structured(nullptr, inputs.size(), D3D11_BIND_UNORDERED_ACCESS); + auto inSrv = Srv(in.Get(), inputs.size()); + auto outUav = Uav(out.Get(), inputs.size()); + auto stage = Staging(static_cast(inputs.size() * sizeof(float))); + context->CSSetShader(noiseProbe.Get(), nullptr, 0); + ID3D11ShaderResourceView* srvs[10]{}; srvs[9] = inSrv.Get(); + context->CSSetShaderResources(0, 10, srvs); + ID3D11UnorderedAccessView* uavs[7]{}; uavs[6] = outUav.Get(); + context->CSSetUnorderedAccessViews(0, 7, uavs, nullptr); + context->Dispatch(static_cast((inputs.size() + 63) / 64), 1, 1); + ID3D11ShaderResourceView* nullSrvs[10]{}; + ID3D11UnorderedAccessView* nullUavs[7]{}; + context->CSSetShaderResources(0, 10, nullSrvs); + context->CSSetUnorderedAccessViews(0, 7, nullUavs, nullptr); + context->CopyResource(stage.Get(), out.Get()); + context->Flush(); + D3D11_MAPPED_SUBRESOURCE mapped{}; + Check(context->Map(stage.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(noise outputs)"); + const auto* data = static_cast(mapped.pData); + std::vector values(data, data + inputs.size()); + context->Unmap(stage.Get(), 0); + return values; + } + }; + + struct FarSelection + { + TileKey key; + FarFieldTileInput input; + double exportPreparationMs=0.0; + }; + + bool FarInputHasSurface(const TileKey& key,const FarFieldTileInput& input) + { + TileData probe; + probe.key=key; + probe.cells=static_cast(input.cells); + probe.dim=static_cast(input.sampleDim); + probe.step=static_cast(input.step); + probe.origin[0]=input.origin.x;probe.origin[1]=input.origin.y;probe.origin[2]=input.origin.z; + const std::size_t sampleCount=static_cast(probe.dim)*probe.dim*probe.dim; + probe.density.resize(sampleCount); + for(std::size_t i=0;i>5]&(1u<<(i&31)))?1.0f:-1.0f; + for(std::uint32_t z=0;z(x),static_cast(y),static_cast(z))]!=0) + return true; + return false; + } + + struct SpatialKey + { + int x, y, z; + bool operator==(const SpatialKey&) const = default; + }; + struct SpatialHash + { + std::size_t operator()(const SpatialKey& p) const noexcept + { + std::uint64_t h = static_cast(p.x) * 0x9E3779B185EBCA87ull; + h ^= static_cast(static_cast(p.y)) * 0xC2B2AE3D27D4EB4Full; + h ^= static_cast(static_cast(p.z)) * 0x165667B19E3779F9ull; + return static_cast(h ^ (h >> 32)); + } + }; + + struct DistanceStats + { + double meanCm = 0.0; + float maxCm = 0.0f; + std::uint64_t count = 0; + float from[3]{}; + float to[3]{}; + }; + + DistanceStats NearestDistance(const std::vector& from, + const std::vector& to) + { + DistanceStats stats; + if (from.empty() || to.empty()) return stats; + std::unordered_map, SpatialHash> bins; + bins.reserve(to.size() * 2); + auto keyFor = [](const GpuVertex& v) + { + return SpatialKey{static_cast(std::floor(v.position[0] / 25.0f)), + static_cast(std::floor(v.position[1] / 25.0f)), + static_cast(std::floor(v.position[2] / 25.0f))}; + }; + for (std::size_t i = 0; i < to.size(); ++i) bins[keyFor(to[i])].push_back(i); + double sum = 0.0; + for (const GpuVertex& v : from) + { + const SpatialKey center = keyFor(v); + float bestSq = std::numeric_limits::infinity(); + for (int dz = -1; dz <= 1; ++dz) + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const auto it = bins.find({center.x + dx, center.y + dy, center.z + dz}); + if (it == bins.end()) continue; + for (std::size_t i : it->second) + { + const float x = v.position[0] - to[i].position[0]; + const float y = v.position[1] - to[i].position[1]; + const float z = v.position[2] - to[i].position[2]; + bestSq = std::min(bestSq, x*x + y*y + z*z); + } + } + if (!std::isfinite(bestSq)) continue; + const float d = std::sqrt(bestSq); + sum += d; + if (d > stats.maxCm) + { + stats.maxCm = d; + std::copy_n(v.position, 3, stats.from); + float nearestSq = std::numeric_limits::infinity(); + for (int dz = -1; dz <= 1; ++dz) + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const auto it = bins.find({center.x + dx, center.y + dy, center.z + dz}); + if (it == bins.end()) continue; + for (std::size_t i : it->second) + { + const float x = v.position[0] - to[i].position[0]; + const float y = v.position[1] - to[i].position[1]; + const float z = v.position[2] - to[i].position[2]; + const float d2 = x*x + y*y + z*z; + if (d2 < nearestSq) + { + nearestSq = d2; + std::copy_n(to[i].position, 3, stats.to); + } + } + } + } + ++stats.count; + } + stats.meanCm = stats.count ? sum / stats.count : 0.0; + return stats; + } + + NoiseStats CompareNoise(const std::vector& inputs, + const std::vector& a, const std::vector& b) + { + NoiseStats stats; + double sum = 0.0; + for (std::size_t i = 0; i < inputs.size(); ++i) + { + const float d = std::abs(a[i] - b[i]); + sum += d; + stats.maximum = std::max(stats.maximum, d); + if (std::bit_cast(a[i]) != std::bit_cast(b[i])) + ++stats.differingBits; + } + stats.mean = inputs.empty() ? 0.0 : sum / inputs.size(); + return stats; + } + + std::vector MakeNoiseProbes(std::uint32_t seed) + { + std::vector out(4096); + for (std::size_t i = 0; i < out.size(); ++i) + { + const std::uint32_t n = static_cast(i); + out[i] = {{static_cast(static_cast(n % 509) - 254) * 0.37f, + static_cast(static_cast((n * 17) % 701) - 350) * 0.19f, + static_cast(static_cast((n * 43) % 397) - 198) * 0.23f}, + seed ^ (n * 0x9E3779B9u)}; + } + return out; + } + + std::vector SelectTiles(World& world, const WorldConfiguration& config, + Vec3d camera) + { + const auto desired = GetFullDesired(world, config, camera); + std::vector result; + auto nearest = [&](int lod) -> std::optional + { + double best = std::numeric_limits::infinity(); + std::optional selected; + for (const TileKey& key : desired) + { + if (key.lod != lod) continue; + const auto origin = TileOrigin(key); + const double extent = double(32 << lod); + const double dx = origin[0] + extent * 0.5 - camera.x; + const double dy = origin[1] + extent * 0.5 - camera.y; + const double dz = origin[2] + extent * 0.5 - camera.z; + const double d = dx*dx + dy*dy + dz*dz; + if (d < best) { best = d; selected = key; } + } + return selected; + }; + if (auto key = nearest(0)) result.push_back(*key); + if (auto key = nearest(1)) result.push_back(*key); + std::vector lod0; + for (const TileKey& key : desired) if (key.lod == 0) lod0.push_back(key); + std::sort(lod0.begin(), lod0.end(), [&](const TileKey& a, const TileKey& b) + { + const auto oa = TileOrigin(a), ob = TileOrigin(b); + const double da = std::pow(oa[0] + 16.0 - camera.x, 2) + + std::pow(oa[1] + 16.0 - camera.y, 2) + std::pow(oa[2] + 16.0 - camera.z, 2); + const double db = std::pow(ob[0] + 16.0 - camera.x, 2) + + std::pow(ob[1] + 16.0 - camera.y, 2) + std::pow(ob[2] + 16.0 - camera.z, 2); + return da < db; + }); + std::optional criticalTile; + for (const TileKey& key : lod0) + { + const auto origin = TileOrigin(key); + bool found = false; + for (int z = 0; z < 32 && !found; ++z) + for (int y = 0; y < 32 && !found; ++y) + for (int x = 0; x < 32 && !found; ++x) + found = world.debugCriticalAirAt({double(origin[0] + x), double(origin[1] + y), double(origin[2] + z)}); + if (found) { criticalTile = key; break; } + } + if (criticalTile && std::find(result.begin(), result.end(), *criticalTile) == result.end()) + { + std::printf("[RouteCriticalTileSelected] key=(%d,%d,%d) lod=%d\n", + criticalTile->coordinate.x, criticalTile->coordinate.y, + criticalTile->coordinate.z, criticalTile->lod); + result.push_back(*criticalTile); + } + if (!criticalTile) + std::printf("[RouteCriticalSearch] lod0_tiles=%zu result=no_protected_air_samples_found\n", lod0.size()); + return result; + } + + std::vector SelectFarTiles(World& world,const WorldConfiguration& config,Vec3d camera) + { + std::vector result; + const auto desired=GetFullDesired(world,config,camera); + const int32_t lastLod=std::min(config.maxLod,8); + for(int32_t lod=std::max(config.fullResolutionLodCount,2);lod<=lastLod;++lod) + { + std::vector candidates; + for(const TileKey& key:desired)if(key.lod==lod)candidates.push_back(key); + std::sort(candidates.begin(),candidates.end(),[&](const TileKey& a,const TileKey& b) + { + const auto oa=TileOrigin(a),ob=TileOrigin(b); + const double ea=double(32<( + Clock::now()-exportStart).count(); + if(!input) + { + if(error.find("near analytic/detail region")!=std::string::npos)continue; + throw std::runtime_error("far field export failed: "+error); + } + if(!FarInputHasSurface(key,*input))continue; + result.push_back({key,std::move(*input),exportPreparationMs}); + std::printf("[FarSurfaceTileSelected] key=(%d,%d,%d) lod=%d\n", + key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod); + break; + } + } + return result; + } + + void PrintProcessMemory(const char* stage) + { + PROCESS_MEMORY_COUNTERS_EX memory{}; + memory.cb = sizeof(memory); + if (GetProcessMemoryInfo(GetCurrentProcess(), + reinterpret_cast(&memory), sizeof(memory))) + { + std::printf("[GpuProcessMemory] stage=%s working_set_mb=%.2f peak_working_set_mb=%.2f private_mb=%.2f\n", + stage, memory.WorkingSetSize / (1024.0 * 1024.0), + memory.PeakWorkingSetSize / (1024.0 * 1024.0), + memory.PrivateUsage / (1024.0 * 1024.0)); + } + } + + bool ReportTile(const TileData& tile, const TileMesh& cpuMesh, + const GpuResult& hwFirst, const GpuResult& hwSecond, + const GpuResult& warp,const FarFieldTileInput& farInput) + { + std::uint64_t hwTopologyDiff = 0, warpTopologyDiff = 0; + std::uint64_t criticalTopologyDiff = 0, criticalWarpTopologyDiff = 0; + for (std::size_t i = 0; i < tile.expectedCases.size(); ++i) + { + const bool hwDiff = tile.expectedCases[i] != hwFirst.cases[i]; + const bool warpDiff = tile.expectedCases[i] != warp.cases[i]; + hwTopologyDiff += hwDiff; + warpTopologyDiff += warpDiff; + if (tile.criticalCellMask[i] && hwDiff) ++criticalTopologyDiff; + if (tile.criticalCellMask[i] && warpDiff) ++criticalWarpTopologyDiff; + } + std::uint64_t repeatBitDiff = 0, warpBitDiff = 0; + std::uint64_t repeatCaseDiff = 0, repeatArgsDiff = 0; + const std::size_t repeatCaseCount = std::min(hwFirst.cases.size(), hwSecond.cases.size()); + for (std::size_t i = 0; i < repeatCaseCount; ++i) + repeatCaseDiff += hwFirst.cases[i] != hwSecond.cases[i]; + for (std::size_t i = 0; i < hwFirst.drawArgs.size(); ++i) + repeatArgsDiff += hwFirst.drawArgs[i] != hwSecond.drawArgs[i]; + const std::size_t repeatCount = std::min(hwFirst.vertices.size(), hwSecond.vertices.size()); + for (std::size_t i = 0; i < repeatCount; ++i) + { + const std::uint32_t* a = reinterpret_cast(&hwFirst.vertices[i]); + const std::uint32_t* b = reinterpret_cast(&hwSecond.vertices[i]); + for (int c = 0; c < 8; ++c) repeatBitDiff += a[c] != b[c]; + } + const std::size_t paired = std::min(hwFirst.vertices.size(), warp.vertices.size()); + for (std::size_t i = 0; i < paired; ++i) + { + const std::uint32_t* a = reinterpret_cast(&hwFirst.vertices[i]); + const std::uint32_t* b = reinterpret_cast(&warp.vertices[i]); + for (int c = 0; c < 8; ++c) warpBitDiff += a[c] != b[c]; + } + const DistanceStats cpuToGpu = NearestDistance(hwFirst.vertices, tile.cpuSurfaceVertices); + const DistanceStats gpuToCpu = NearestDistance(tile.cpuSurfaceVertices, hwFirst.vertices); + const DistanceStats cpuToWarp = NearestDistance(warp.vertices,tile.cpuSurfaceVertices); + const DistanceStats warpToCpu = NearestDistance(tile.cpuSurfaceVertices,warp.vertices); + const DistanceStats gpuToWarp = NearestDistance(hwFirst.vertices, warp.vertices); + const DistanceStats warpToGpu = NearestDistance(warp.vertices, hwFirst.vertices); + const double directedCount = static_cast(cpuToGpu.count + gpuToCpu.count); + const double mean = directedCount + ? (cpuToGpu.meanCm * cpuToGpu.count + gpuToCpu.meanCm * gpuToCpu.count) / directedCount : 0.0; + const float maximum = std::max(cpuToGpu.maxCm, gpuToCpu.maxCm); + const double warpDirectedCount = static_cast(gpuToWarp.count + warpToGpu.count); + const double warpMean = warpDirectedCount + ? (gpuToWarp.meanCm * gpuToWarp.count + warpToGpu.meanCm * warpToGpu.count) / warpDirectedCount : 0.0; + const float warpMaximum = std::max(gpuToWarp.maxCm, warpToGpu.maxCm); + const double cpuWarpDirectedCount=static_cast(cpuToWarp.count+warpToCpu.count); + const double cpuWarpMean=cpuWarpDirectedCount + ?(cpuToWarp.meanCm*cpuToWarp.count+warpToCpu.meanCm*warpToCpu.count)/cpuWarpDirectedCount:0.0; + const float cpuWarpMaximum=std::max(cpuToWarp.maxCm,warpToCpu.maxCm); + double fieldErrorSum=0.0,warpFieldErrorSum=0.0; + float fieldErrorMax=0.0f,warpFieldErrorMax=0.0f; + std::uint64_t fieldSignDiff=0,warpFieldSignDiff=0; + const std::size_t fieldCount=std::min(hwFirst.fieldDensity.size(),tile.density.size()); + const std::size_t warpFieldCount=std::min(warp.fieldDensity.size(),tile.density.size()); + for(std::size_t I=0;I(farInput.protectedSampleCount),static_cast(tile.criticalSamples), + static_cast(tile.criticalCells), cpuMesh.groups.surfaceVertexCount, + hwFirst.vertices.size(), warp.vertices.size(), tile.expectedVertexCount, + hwFirst.sequentialIndices, warp.sequentialIndices, + hwFirst.indexedIndirectArgsValid, warp.indexedIndirectArgsValid, + hwFirst.d3dBufferBytes / (1024.0 * 1024.0), + hwFirst.adapterMemoryBeforeBytes / (1024.0 * 1024.0), + hwFirst.adapterMemoryAfterBytes / (1024.0 * 1024.0), + hwFirst.adapterMemoryBudgetBytes / (1024.0 * 1024.0), warp.hasAdapterMemoryInfo, + hwFirst.fieldMs,hwFirst.dispatchMs, hwFirst.wallMs, hwSecond.fieldMs,hwSecond.dispatchMs, + warp.fieldMs,warp.dispatchMs, warp.wallMs); + std::printf("[FarFieldAgreement] tile=(%d,%d,%d,%d) cpu_gpu_field_mean_abs_vox=%.9g max_abs_vox=%.9g sign_difference_samples=%llu cpu_warp_field_mean_abs_vox=%.9g max_abs_vox=%.9g sign_difference_samples=%llu compared_samples_gpu=%zu compared_samples_warp=%zu protected_air_samples=%llu topology_diff_cells_hw=%llu topology_diff_cells_warp=%llu\n", + tile.key.coordinate.x,tile.key.coordinate.y,tile.key.coordinate.z,tile.key.lod, + fieldErrorMean,fieldErrorMax,static_cast(fieldSignDiff), + warpFieldErrorMean,warpFieldErrorMax,static_cast(warpFieldSignDiff), + fieldCount,warpFieldCount, + static_cast(farInput.protectedSampleCount), + static_cast(hwTopologyDiff),static_cast(warpTopologyDiff)); + const bool outputCountMatch = hwFirst.vertices.size() == tile.expectedVertexCount + && warp.vertices.size() == tile.expectedVertexCount; + const bool vertexAgreement=maximum<=0.05f&&cpuWarpMaximum<=0.05f&&warpMaximum<=0.05f; + const bool repeatedOutputsIdentical = repeatBitDiff == 0 && repeatCaseDiff == 0 + && repeatArgsDiff == 0 && hwFirst.vertices.size() == hwSecond.vertices.size() + && hwFirst.sequentialIndices && hwSecond.sequentialIndices + && hwFirst.indexedIndirectArgsValid && hwSecond.indexedIndirectArgsValid; + const bool status = hwTopologyDiff == 0 && warpTopologyDiff == 0 + &&fieldCount==tile.density.size()&&fieldSignDiff==0&&fieldErrorMax<=0.05f + &&warpFieldCount==tile.density.size()&&warpFieldSignDiff==0&&warpFieldErrorMax<=0.05f + && repeatedOutputsIdentical && outputCountMatch + &&vertexAgreement + && 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 route_critical_cells_audited=%llu route_critical_diff_cells_hw=%llu route_critical_diff_cells_warp=%llu thin_feature_cells_audited=0 thin_feature_status=not_audited shell_patch_survival=not_audited vertex_mean_cm=%.8g vertex_max_cm=%.8g vertex_max_pair_cm=(%.5f,%.5f,%.5f)->(%.5f,%.5f,%.5f) cpu_warp_vertex_mean_cm=%.8g cpu_warp_vertex_max_cm=%.8g 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", + tile.key.coordinate.x, tile.key.coordinate.y, tile.key.coordinate.z, tile.key.lod, + static_cast(hwTopologyDiff), + static_cast(warpTopologyDiff), + static_cast(tile.criticalCells), + static_cast(criticalTopologyDiff), + static_cast(criticalWarpTopologyDiff), + mean, maximum, + 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], + cpuToGpu.maxCm >= gpuToCpu.maxCm ? cpuToGpu.to[0] : gpuToCpu.to[0], + cpuToGpu.maxCm >= gpuToCpu.maxCm ? cpuToGpu.to[1] : gpuToCpu.to[1], + cpuToGpu.maxCm >= gpuToCpu.maxCm ? cpuToGpu.to[2] : gpuToCpu.to[2], + cpuWarpMean,cpuWarpMaximum,warpMean,warpMaximum, + static_cast(repeatBitDiff), + static_cast(repeatCaseDiff), + static_cast(repeatArgsDiff), repeatedOutputsIdentical, + static_cast(warpBitDiff), outputCountMatch, + vertexAgreement,status ? "PASS" : "FAIL"); + return status; + } + + int Run(int argc, char** argv) + { + SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS); + SetProcessAffinityMask(GetCurrentProcess(), static_cast(0xFF00)); + const std::filesystem::path root = std::filesystem::absolute(argv[0]).parent_path().parent_path(); + const std::filesystem::path dataPath = root / "data" / "owner_world.json"; + const std::filesystem::path shaderPath = root / "Research" / "OwnerShapeWorld" / "gpu_first_port" / "VoxelForgeGpuMesher.hlsl"; + std::string json = ReadFile(dataPath); + SetJsonInt(json, "clip_radius", 3); + SetJsonInt(json, "ClipRadius", 3); + std::string error; + auto snapshot = ResolvedWorldSnapshot::FromJson(json, error); + if (!snapshot) throw std::runtime_error("snapshot load failed: " + error); + World world(std::move(*snapshot)); + const WorldConfiguration config = world.configuration(); + const Vec3d camera = config.viewerStartFound ? config.viewerStartVoxels : Vec3d{}; + const std::vector desired = GetFullDesired(world, config, camera); + PrintProcessMemory("after_snapshot"); + ShapeBuildRequest request; + if (!BuildRequestForDesired(desired, config, request)) + throw std::runtime_error("could not derive full clipmap compile bounds"); + CpuScheduler scheduler; + const auto compileStart = Clock::now(); + if (!world.compile(request, scheduler, error)) throw std::runtime_error("shape compile failed: " + error); + const double compileMs = std::chrono::duration(Clock::now() - compileStart).count(); + std::printf("[GpuPortWorld] desired_tiles=%zu camera_vox=(%.3f,%.3f,%.3f) full_resolution_lods=%d coarse_cells=%d shape_compile_ms=%.3f shape_stats_ms=(%.3f,%.3f,%.3f,%.3f) memory_limit_mb=8192\n", + desired.size(), camera.x, camera.y, camera.z, config.fullResolutionLodCount, + config.coarseTileCells, compileMs, + world.buildStats().shapeSeconds * 1000.0, world.buildStats().intervalSeconds * 1000.0, + world.buildStats().surfaceSeconds * 1000.0, world.buildStats().farShapeSeconds * 1000.0); + PrintProcessMemory("after_shape_compile"); + + DeviceRun hardware, warp; + hardware.Initialize(shaderPath, false); + warp.Initialize(shaderPath, true); + std::printf("[GpuDevices] hardware=\"%s\" warp=Microsoft_WARP feature_level=11_0\n", hardware.name.c_str()); + const std::vector probes = MakeNoiseProbes(static_cast(config.seed)); + std::vector cpuNoise(probes.size()); + for (std::size_t i = 0; i < probes.size(); ++i) + cpuNoise[i] = VF_FieldSurfaceRelief(probes[i].position[0], probes[i].position[1], probes[i].position[2], probes[i].seed); + const std::vector gpuNoise1 = hardware.RunNoiseProbe(probes); + const std::vector gpuNoise2 = hardware.RunNoiseProbe(probes); + const std::vector warpNoise = warp.RunNoiseProbe(probes); + const NoiseStats noiseCpuGpu = CompareNoise(probes, cpuNoise, gpuNoise1); + const NoiseStats noiseRepeat = CompareNoise(probes, gpuNoise1, gpuNoise2); + const NoiseStats noiseWarp = CompareNoise(probes, gpuNoise1, warpNoise); + std::printf("[SharedFieldMath] probes=%zu cpu_gpu_mean_abs=%.9g cpu_gpu_max_abs=%.9g cpu_gpu_bit_differences=%llu repeat_max_abs=%.9g repeat_bit_differences=%llu warp_mean_abs=%.9g warp_max_abs=%.9g warp_bit_differences=%llu hash_integer_source=shared\n", + probes.size(), noiseCpuGpu.mean, noiseCpuGpu.maximum, + static_cast(noiseCpuGpu.differingBits), noiseRepeat.maximum, + static_cast(noiseRepeat.differingBits), noiseWarp.mean, + noiseWarp.maximum, static_cast(noiseWarp.differingBits)); + + const std::vector selected = SelectFarTiles(world, config, camera); + if (selected.empty()) throw std::runtime_error("no pure far-field tiles could be exported"); + bool allPass = noiseCpuGpu.differingBits == 0 && noiseRepeat.differingBits == 0 + && 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; + for (const FarSelection& selection : selected) + { + const TileKey& key=selection.key; + TileGenerationOptions options; + options.captureDensitySamples = true; + options.skipUvAndColors = true; + options.skipMaterialGroups = true; + const auto cpuStart = Clock::now(); + auto cpuMesh = world.generateTile(key, options, error); + const double cpuWallMs = std::chrono::duration(Clock::now() - cpuStart).count(); + if (!cpuMesh) throw std::runtime_error("CPU reference tile failed: " + error); + const auto inputStart = Clock::now(); + TileData tile = MakeTileData(world, key, config, *cpuMesh, true); + const double inputWallMs = std::chrono::duration(Clock::now() - inputStart).count(); + if(tile.signs!=selection.input.rockAirSigns) + throw std::runtime_error("exported far sign words differ from the CPU reference lattice"); + const GpuResult gpu1 = hardware.Run(tile,&selection.input); + const GpuResult gpu2 = hardware.Run(tile,&selection.input); + const GpuResult warpResult = warp.Run(tile,&selection.input); + const bool agreementPass = ReportTile(tile, *cpuMesh, gpu1, gpu2, warpResult,selection.input); + std::printf("[GpuFieldPipelineWall] key=(%d,%d,%d) lod=%d cpu_export_shape_sign_upload_prep_ms=%.4f cpu_generateTile_ms=%.4f cpu_reference_lattice_topology_check_ms=%.4f rx7900_field_gpu_ms=%.4f rx7900_mesh_plus_field_gpu_ms=%.4f rx7900_wall_upload_dispatch_readback_ms=%.4f warp_field_gpu_ms=%.4f warp_mesh_plus_field_wall_ms=%.4f\n", + key.coordinate.x, key.coordinate.y, key.coordinate.z, key.lod, + selection.exportPreparationMs,cpuWallMs,inputWallMs,gpu1.fieldMs,gpu1.dispatchMs,gpu1.wallMs, + warpResult.fieldMs,warpResult.wallMs); + std::printf("[CpuTileReference] key=(%d,%d,%d) lod=%d wall_ms=%.4f mode=single_generateTile_api cpu_surface_vertices=%u cpu_indices=%zu\n", + key.coordinate.x, key.coordinate.y, key.coordinate.z, key.lod, cpuWallMs, + cpuMesh->groups.surfaceVertexCount, cpuMesh->indices.size()); + 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 + && gpu2.vertices.size() == gpu1.vertices.size() + && warpResult.vertices.size() == tile.expectedVertexCount + && gpu1.sequentialIndices && gpu2.sequentialIndices && warpResult.sequentialIndices + && gpu1.indexedIndirectArgsValid && gpu2.indexedIndirectArgsValid + && warpResult.indexedIndirectArgsValid + && gpu1.cases == tile.expectedCases && gpu2.cases == tile.expectedCases + && warpResult.cases == tile.expectedCases && gpu1.drawArgs == gpu2.drawArgs + && agreementPass; + allPass &= tilePass; + ++testedTiles;cpuTileWallSum+=cpuWallMs;cpuFieldPrepSum+=inputWallMs; + cpuExportPrepSum+=selection.exportPreparationMs; + gpuFieldSum+=gpu1.fieldMs;gpuTotalSum+=gpu1.dispatchMs;warpFieldSum+=warpResult.fieldMs; + PrintProcessMemory("after_tile_pair"); + } + std::printf("[GpuFarPortSummary] tested_tiles=%llu levels=2..8 cpu_export_shape_sign_prep_sum_ms=%.4f cpu_generateTile_sum_ms=%.4f cpu_reference_lattice_topology_check_sum_ms=%.4f rx7900_field_dispatch_sum_ms=%.4f rx7900_field_plus_meshing_sum_ms=%.4f warp_field_dispatch_sum_ms=%.4f whole_view_gpu_far=not_measured viewer_direct_draw=not_integrated shell_survival=not_audited\n", + static_cast(testedTiles),cpuExportPrepSum,cpuTileWallSum,cpuFieldPrepSum,gpuFieldSum, + gpuTotalSum,warpFieldSum); + std::printf("[GpuPortStatus] far_field_subset_status=%s overall_port=INCOMPLETE field=shared_analytic_shapes_strata_cpu_sign_masks meshing=gpu_compute_viewer_draw=not_integrated captures=not_integrated\n", + allPass ? "PASS" : "FAIL"); + return allPass ? 0 : 3; + } +} + +int main(int argc, char** argv) +{ + try { return Run(argc, argv); } + catch (const std::exception& e) + { + std::fprintf(stderr, "[GpuPortError] %s\n", e.what()); + return 2; + } +} diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/REPORT.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/REPORT.md new file mode 100644 index 0000000..88e3c0f --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/REPORT.md @@ -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. diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/RunGpuFieldPort.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/RunGpuFieldPort.ps1 new file mode 100644 index 0000000..50e3eff --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/RunGpuFieldPort.ps1 @@ -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 +} diff --git a/Standalone/Research/OwnerShapeWorld/gpu_first_port/VoxelForgeGpuMesher.hlsl b/Standalone/Research/OwnerShapeWorld/gpu_first_port/VoxelForgeGpuMesher.hlsl index 5662814..d3ad2e3 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_first_port/VoxelForgeGpuMesher.hlsl +++ b/Standalone/Research/OwnerShapeWorld/gpu_first_port/VoxelForgeGpuMesher.hlsl @@ -126,6 +126,73 @@ RWStructuredBuffer CellCases : register(u0); RWStructuredBuffer CellLocalOffsets : register(u1); RWStructuredBuffer GroupVertexCounts : register(u2); +struct FarFieldSampleRange { uint offset; uint count; }; +StructuredBuffer FarShapes : register(t10); +StructuredBuffer FarSampleShapeRanges : register(t11); +StructuredBuffer FarSampleShapeIds : register(t12); +StructuredBuffer FarRockAirSigns : register(t13); +StructuredBuffer FarProtectedAir : register(t14); +RWStructuredBuffer 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)]