diff --git a/Standalone/Core/src/VoxelForgeFieldShared.h b/Standalone/Core/src/VoxelForgeFieldShared.h new file mode 100644 index 0000000..7a5946e --- /dev/null +++ b/Standalone/Core/src/VoxelForgeFieldShared.h @@ -0,0 +1,173 @@ +#pragma once + +// Restricted scalar field math shared by the standalone C++ GPU adapter and its HLSL. +// Define VF_FIELD_HLSL before including this file from a shader. Keep this header free of +// containers, engine types, and vector intrinsics so it remains usable by both compilers. +#if defined(VF_FIELD_HLSL) + #define VF_FIELD_INLINE + typedef int VF_FieldI32; + typedef uint VF_FieldU32; + #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) +#else + #include + #include + #define VF_FIELD_INLINE inline + typedef std::int32_t VF_FieldI32; + typedef std::uint32_t VF_FieldU32; + #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) +#endif + +VF_FIELD_INLINE VF_FieldU32 VF_FieldMix(VF_FieldU32 x) +{ + x ^= x >> 16; + x *= 0x7FEB352Du; + x ^= x >> 15; + x *= 0x846CA68Bu; + x ^= x >> 16; + return x; +} + +VF_FIELD_INLINE VF_FieldU32 VF_FieldNoiseHash(VF_FieldI32 x, VF_FieldI32 y, + VF_FieldI32 z, VF_FieldU32 seed) +{ + VF_FieldU32 h = VF_FieldMix(seed ^ 0xB5297A4Du); + h = VF_FieldMix(h ^ VF_FIELD_CAST_U32(x) * 0x68E31DA4u); + h = VF_FieldMix(h ^ VF_FIELD_CAST_U32(y) * 0x1B56C4E9u); + return VF_FieldMix(h ^ VF_FIELD_CAST_U32(z) * 0x9E3779B9u); +} + +VF_FIELD_INLINE float VF_FieldNoiseValue(VF_FieldI32 x, VF_FieldI32 y, + VF_FieldI32 z, VF_FieldU32 seed) +{ + return VF_FIELD_CAST_F32(VF_FieldNoiseHash(x, y, z, seed) >> 8) + * (2.0f / 16777215.0f) - 1.0f; +} + +VF_FIELD_INLINE float VF_FieldFade(float t) +{ + return t * t * (3.0f - 2.0f * t); +} + +VF_FIELD_INLINE float VF_FieldLerp(float a, float b, float t) +{ + return a + (b - a) * t; +} + +VF_FIELD_INLINE float VF_FieldValueNoise(float x, float y, float z, + VF_FieldI32 wavelength, VF_FieldU32 seed) +{ + const float inv = 1.0f / VF_FIELD_CAST_F32(wavelength); + const float fx = x * inv, fy = y * inv, fz = z * inv; + const VF_FieldI32 x0 = VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fx)); + const VF_FieldI32 y0 = VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fy)); + const VF_FieldI32 z0 = VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fz)); + const float tx = VF_FieldFade(fx - VF_FIELD_CAST_F32(x0)); + const float ty = VF_FieldFade(fy - VF_FIELD_CAST_F32(y0)); + const float tz = VF_FieldFade(fz - VF_FIELD_CAST_F32(z0)); + const float z00 = VF_FieldLerp(VF_FieldNoiseValue(x0, y0, z0, seed), + VF_FieldNoiseValue(x0 + 1, y0, z0, seed), tx); + const float z10 = VF_FieldLerp(VF_FieldNoiseValue(x0, y0 + 1, z0, seed), + VF_FieldNoiseValue(x0 + 1, y0 + 1, z0, seed), tx); + const float z01 = VF_FieldLerp(VF_FieldNoiseValue(x0, y0, z0 + 1, seed), + VF_FieldNoiseValue(x0 + 1, y0, z0 + 1, seed), tx); + const float z11 = VF_FieldLerp(VF_FieldNoiseValue(x0, y0 + 1, z0 + 1, seed), + VF_FieldNoiseValue(x0 + 1, y0 + 1, z0 + 1, seed), tx); + return VF_FieldLerp(VF_FieldLerp(z00, z10, ty), VF_FieldLerp(z01, z11, ty), tz); +} + +VF_FIELD_INLINE float VF_FieldSurfaceRelief(float x, float y, float z, VF_FieldU32 seed) +{ + const float macro = VF_FieldValueNoise(x, y, z, 32, seed ^ 0xB5297A4Du); + const float mid = VF_FieldValueNoise(x, y, z, 16, seed ^ 0x63D83595u); + const float fine = VF_FieldValueNoise(x, y, z, 4, seed ^ 0xA511E9B3u); + return 1.20f * macro + (0.80f + 0.25f * macro) * (0.40f * mid + 0.60f * fine); +} + +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); +#endif + return value * value * (3.0f - 2.0f * value); +} + +VF_FIELD_INLINE float VF_FieldSmoothMin(float a, float b, float width) +{ + if (width <= 0.0f) + return a < b ? a : b; + const float delta = a - b; +#if defined(VF_FIELD_HLSL) + const float absolute = abs(delta); + const float minimum = min(a, b); + 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; +#endif + return minimum - (h * h * h) * (width / 6.0f); +} + +// 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, + VF_FieldU32 hasMidpoint) +{ + float t = (d2 - d1 < 1.0e-4f && d1 - d2 < 1.0e-4f) + ? 0.5f : (-d1) / (d2 - d1); +#if defined(VF_FIELD_HLSL) + t = clamp(t, 0.0f, 1.0f); +#else + t = t < 0.0f ? 0.0f : (t > 1.0f ? 1.0f : t); +#endif + // hasMidpoint is an integer CPU decision made from the reference edge parameter. Do not + // repeat its threshold test in floating point on different GPU implementations. + if (hasMidpoint != 0u) + { + if (midpoint < 1.0e-6f && midpoint > -1.0e-6f) + return 0.5f; + if ((midpoint < 0.0f) == (d1 < 0.0f)) + { + const float denominator = d2 - midpoint; + if (denominator < -1.0e-4f || denominator > 1.0e-4f) + { + float half = (-midpoint) / denominator; +#if defined(VF_FIELD_HLSL) + half = clamp(half, 0.0f, 1.0f); +#else + half = half < 0.0f ? 0.0f : (half > 1.0f ? 1.0f : half); +#endif + return 0.5f + 0.5f * half; + } + } + else + { + const float denominator = midpoint - d1; + if (denominator < -1.0e-4f || denominator > 1.0e-4f) + { + float half = (-d1) / denominator; +#if defined(VF_FIELD_HLSL) + half = clamp(half, 0.0f, 1.0f); +#else + half = half < 0.0f ? 0.0f : (half > 1.0f ? 1.0f : half); +#endif + return 0.5f * half; + } + } + } + return t; +} + +#undef VF_FIELD_INLINE +#undef VF_FIELD_CAST_I32 +#undef VF_FIELD_CAST_U32 +#undef VF_FIELD_CAST_F32 +#undef VF_FIELD_FLOOR diff --git a/Standalone/Research/OwnerShapeWorld/gpu_first_port/BuildGpuPort.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_first_port/BuildGpuPort.ps1 new file mode 100644 index 0000000..c71125f --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_first_port/BuildGpuPort.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\VoxelForgeGpuFirstPort' +$WrapperDir = Join-Path $BuildDir 'wrappers' +$BinDir = Join-Path $Standalone 'bin' +$Output = Join-Path $BinDir 'VoxelForgeGpuFirstPort.exe' +$CoreLib = Join-Path $BuildDir 'VoxelForgeGpuPortCore.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 'gpu_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 'gpu_core_library.rsp' +[IO.File]::WriteAllLines($LibRsp, @('/nologo',('/OUT:"{0}"' -f $CoreLib)) + $ObjectNames, [Text.UTF8Encoding]::new($false)) +$RunnerRsp = Join-Path $BuildDir 'gpu_runner_link.rsp' +$Runner = Join-Path $PSScriptRoot 'GpuFirstPort.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_first_port/GpuFirstPort.cpp b/Standalone/Research/OwnerShapeWorld/gpu_first_port/GpuFirstPort.cpp new file mode 100644 index 0000000..586fe1d --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_first_port/GpuFirstPort.cpp @@ -0,0 +1,1207 @@ +#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 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::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 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 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, "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, 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; + 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 (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(tile.density.data(), tile.density.size(), D3D11_BIND_SHADER_RESOURCE); + 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); + auto signSrv = Srv(signs.Get(), tile.signs.size()); + auto triSrv = Srv(triBuffer.Get(), triCount); + auto densitySrv = Srv(density.Get(), tile.density.size()); + 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 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)"); + + D3D11_QUERY_DESC timestampDesc{D3D11_QUERY_TIMESTAMP, 0}; + D3D11_QUERY_DESC disjointDesc{D3D11_QUERY_TIMESTAMP_DISJOINT, 0}; + ComPtr startQuery, endQuery, disjointQuery; + Check(device->CreateQuery(×tampDesc, &startQuery), "CreateQuery(start)"); + Check(device->CreateQuery(×tampDesc, &endQuery), "CreateQuery(end)"); + Check(device->CreateQuery(&disjointDesc, &disjointQuery), "CreateQuery(disjoint)"); + + context->CSSetConstantBuffers(0, 1, cb.GetAddressOf()); + context->Begin(disjointQuery.Get()); + context->End(startQuery.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[9]{}; + ID3D11UnorderedAccessView* nullUavs[8]{}; + context->CSSetShaderResources(0, 9, 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, 9, 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, 9, 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 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()); + 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{}; + 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); + } + 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 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; + } + + 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) + { + 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 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); + std::printf("[GpuTile] key=(%d,%d,%d) lod=%d cells=%u source_samples=%zu critical_lattice=%llu critical_cells=%llu cpu_vertices=%u gpu_vertices=%zu warp_vertices=%zu expected_gpu_vertices=%u gpu_indices_sequential=%d warp_indices_sequential=%d gpu_indexed_indirect_args=%d warp_indexed_indirect_args=%d d3d_buffer_mb=%.3f gpu_adapter_memory_mb=(%.2f,%.2f) budget_mb=%.2f warp_adapter_memory_info=%d gpu_dispatch_ms=%.4f gpu_wall_ms=%.4f repeat_dispatch_ms=%.4f warp_dispatch_ms=%.4f warp_wall_ms=%.4f\n", + tile.key.coordinate.x, tile.key.coordinate.y, tile.key.coordinate.z, tile.key.lod, + tile.cells, tile.density.size(), 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.dispatchMs, hwFirst.wallMs, hwSecond.dispatchMs, warp.dispatchMs, warp.wallMs); + const bool outputCountMatch = hwFirst.vertices.size() == tile.expectedVertexCount + && warp.vertices.size() == tile.expectedVertexCount; + 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 + && repeatedOutputsIdentical && outputCountMatch + && 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_diff_cells_hw=%llu vertex_mean_cm=%.8g vertex_max_cm=%.8g vertex_max_pair_cm=(%.5f,%.5f,%.5f)->(%.5f,%.5f,%.5f) warp_vertex_mean_cm=%.8g 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 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), + static_cast(hwTopologyDiff), 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], + warpMean, warpMaximum, + static_cast(repeatBitDiff), + static_cast(repeatCaseDiff), + static_cast(repeatArgsDiff), repeatedOutputsIdentical, + static_cast(warpBitDiff), outputCountMatch, 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 = SelectTiles(world, config, camera); + if (selected.empty()) throw std::runtime_error("no nearby LOD0/LOD1 tiles found"); + bool allPass = noiseCpuGpu.differingBits == 0 && noiseRepeat.differingBits == 0 + && noiseWarp.differingBits == 0; + for (const TileKey& key : selected) + { + 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(); + const bool hasCritical = key.lod == 0; + TileData tile = MakeTileData(world, key, config, *cpuMesh, hasCritical); + const double inputWallMs = std::chrono::duration(Clock::now() - inputStart).count(); + const GpuResult gpu1 = hardware.Run(tile); + const GpuResult gpu2 = hardware.Run(tile); + const GpuResult warpResult = warp.Run(tile); + const bool agreementPass = ReportTile(tile, *cpuMesh, gpu1, gpu2, warpResult); + std::printf("[GpuPipelineWall] key=(%d,%d,%d) lod=%d cpu_field_input_prep_ms=%.4f hardware_total_ms=%.4f warp_total_ms=%.4f (GPU wall includes input upload and output readback)\n", + key.coordinate.x, key.coordinate.y, key.coordinate.z, key.lod, + inputWallMs, inputWallMs + gpu1.wallMs, inputWallMs + 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.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; + PrintProcessMemory("after_tile_pair"); + } + std::printf("[GpuPortStatus] subset_status=%s overall_port=INCOMPLETE work=one_nearby_lod0_and_lod1_tile field=cpu_exact_lattice_plus_cpu_midpoint_records meshing=gpu_compute_vertex_index_buffers 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_first_port/REPORT.md b/Standalone/Research/OwnerShapeWorld/gpu_first_port/REPORT.md new file mode 100644 index 0000000..fd24848 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_first_port/REPORT.md @@ -0,0 +1,50 @@ +# VoxelForge GPU first port + +## Status + +This is a passing **GPU meshing prototype**, not the complete field-to-view GPU port. It takes the owner world’s field samples from the CPU, then classifies cells, scans output offsets, and emits render buffers with D3D11 compute shaders. The owner-specific shape-distance and relief evaluation still runs on the CPU. The viewer, full-view comparison, chunk-crossing GPU timing, and six GPU captures are not integrated. + +The CPU implementation files were not changed. `HEAD` remained `566d1f3`. The new shared header is used by this harness and the HLSL, but is not included by the canonical Core implementation, preserving its existing output path. + +## What runs on each side + +- The CPU compiles the world and decides tile keys. For each tested tile it supplies the exact packed air/rock sign words, lattice density samples, and the LOD0 analytic edge-midpoint values selected by the CPU mesher’s root rule. The GPU never chooses a sign or feature topology. +- `ClassifyCS` builds marching-cubes cases from the sign words. `ScanGroupsCS` creates deterministic compact output offsets and a five-word `DrawIndexedInstancedIndirect` record. `EmitCS` writes positions, gradient normals, UVs, and a sequential index buffer. +- The vertex and index outputs are D3D11 raw buffers with vertex/index bind flags. The indirect-argument buffer has the D3D11 indirect-argument flag. The harness validates all three, then reads the results back for comparison; it does not issue a render draw. +- [VoxelForgeFieldShared.h](../../../Core/src/VoxelForgeFieldShared.h) contains integer noise hashing, value noise, the relief sum, smooth-min helpers, and the shared root-refinement rule in a C++/HLSL-compatible subset. A 4,096-point probe was bit-identical between its C++ harness implementation, the RX 7900 XTX, and WARP. The owner’s actual `ShapeWorldInternal.h` field evaluator is not yet shared or called by this header. The probe therefore validates the shared helper source, not cross-checks against the canonical owner field. + +The field split is consequently incomplete: the current GPU vertices interpolate CPU-computed density samples. GPU shape distances, partition-of-unity detail blending, owner roughness, and final field signs are future work. Collision remains on the CPU. + +## Tile agreement and timings + +Run: [first_run.log](first_run.log). The harness built the owner snapshot with the current three-ring clip (`2,226` desired tiles), then audited the nearest LOD0 and LOD1 tiles. Each tile contains 32³ cells and 42,875 lattice samples including the gradient halo. + +| Tile | GPU dispatch (three kernels) | RX 7900 D3D wall (upload, dispatch, readback) | CPU field-input preparation | CPU `generateTile` reference | CPU/GPU topology differences | CPU/GPU mean / max vertex distance | GPU/WARP mean / max vertex distance | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| LOD0 `(-2,-15,-3)` | 0.049 ms | 3.538 ms | 3.712 ms | 2.329 ms | 0 | 4.52e-7 / 0.0001221 cm | 2.17e-7 / 0.0001221 cm | +| LOD1 `(-1,-10,-2)` | 0.071 ms | 2.208 ms | 0.632 ms | 1.992 ms | 0 | 1.26e-6 / 0.0002442 cm | 2.45e-7 / 0.0002442 cm | + +The vertex metric is the symmetric nearest-vertex distance between CPU surface vertices and GPU triangle-soup vertices; it is not a continuous-surface Hausdorff measurement. CPU output has 1,103 and 2,237 surface vertices; GPU output has 6,192 and 12,504 vertices respectively, with sequential indices. Each comparison covered all 32,768 cells. Both hardware and WARP had zero case differences. Running the RX 7900 path twice produced zero differing vertex-component bits, cases, or indirect arguments. GPU/WARP float components were not bit-identical (8,296 and 16,287 differing components), while their maximum nearest-vertex distances remained at most 0.0002442 cm. + +The critical-air scan checked all 512 LOD0 keys in the compiled view and found no protected-air samples, so there was no route-critical tile to select. The two tested tiles had zero critical cells. Overall topology was identical for every cell tested because the CPU sign bits feed the integer classifier directly. The GPU harness did not separately locate and exercise a 1–2 voxel wall; the existing CPU whole-view audit reports zero thin sheets, but that is not a GPU thin-feature capture. + +On the RX 7900, the actual compute work is below 0.1 ms per tested tile, so those kernels fit inside a 0.5–1 ms 90 Hz work slice. CPU input preparation alone took 0.63–3.71 ms, and the current upload/readback path took 2.21–3.54 ms wall. This prototype does not yet demonstrate a sliced field-to-render frame. + +For context, the current CPU baseline report records a 0.992 s mean cold whole view for 2,226 tiles: [Round 9 report](../speed_round/round9/REPORT.md). The current CPU crossing report records 105 ms crossing wall and 478 ms aggregate worker CPU for crossing 1, including 128 LOD1 meshes: [chunk results](../../../Core/verification/chunk_fast_column_results.md). These are CPU reference results; this prototype did **not** measure a GPU whole view or GPU chunk crossing. + +### GTX 1060-class estimate + +The RX 7900 XTX measured 0.049–0.071 ms for the tested 32³ classify/scan/emit work. Scaling that measured interval by the nominal FP32 peak ratio (61.4 TFLOPS for the RX 7900 XTX and the owner-provided 4.4 TFLOPS for a GTX 1060-class card) gives about **0.7–1.0 ms per tested tile’s compute kernels**. This is a rough shader-only estimate, not a measured result or a whole-view estimate. The kernel is not purely FP32-bound, Pascal integer throughput and driver overhead differ, and the actual GPU field evaluator is not present; its cost is not included. AMD’s published RX 7900 XTX specification lists 61.4 TFLOPS: [AMD product specifications](https://www.amd.com/en/products/graphics/desktops/radeon/7000-series/amd-radeon-rx-7900xtx.html). + +## Memory and build + +Process peak working set was 428.65 MiB; the largest sampled private usage was 658.75 MiB. Requested D3D11 buffers, including readback staging, totaled 17.82–17.99 MiB per tile; only one tile’s buffers were live per dispatch. DXGI also returned adapter-wide local-memory usage and budget, but those counters include other applications and are not used as a per-process measurement. The observed process plus tile buffers stayed well below the 8 GiB limit. + +The isolated build outputs are `Standalone\Build\VoxelForgeGpuFirstPort\VoxelForgeGpuPortCore.lib` and `Standalone\bin\VoxelForgeGpuFirstPort.exe`. The build used `/MP8`; the runner and build shell set BelowNormal priority and affinity `0xFF00`. The Core library’s Unreal-symbol check passed. No `0xC0000142` occurred. Build output retains the existing `vswhere.exe` setup diagnostic and a `TEXT` macro redefinition warning from the compatibility header. + +## Remaining work for the requested port + +1. Export exact CPU layout and decision data into compact GPU tile records, then move shared shape-distance, relief, and field evaluation into the common C++/HLSL source without changing CPU hashes. +2. Keep CPU-owned sign and protected-feature masks authoritative while evaluating the smooth field on GPU; add a GPU audit tile containing real route-critical and thin-feature samples. +3. Integrate the produced buffers and indirect draw record into the viewer/render graph, then capture and compare the six fixed views. +4. Measure a cold GPU whole view and a new-slab chunk crossing with GPU timestamps plus full wall time, then measure sliced multi-tile work. The current per-tile numbers do not establish a whole-view speedup. diff --git a/Standalone/Research/OwnerShapeWorld/gpu_first_port/VoxelForgeGpuMesher.hlsl b/Standalone/Research/OwnerShapeWorld/gpu_first_port/VoxelForgeGpuMesher.hlsl new file mode 100644 index 0000000..5662814 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_first_port/VoxelForgeGpuMesher.hlsl @@ -0,0 +1,244 @@ +#define VF_FIELD_HLSL 1 +#include "../../../Core/src/VoxelForgeFieldShared.h" + +struct MidpointRecord +{ + uint4 metadata; + float4 values[3]; +}; + +struct GpuVertex +{ + float3 position; + float3 normal; + float2 uv; +}; + +cbuffer TileConstants : register(b0) +{ + uint CellsPerAxis; + uint SampleDim; + uint CellCount; + uint GroupCount; + int OriginX; + int OriginY; + int OriginZ; + int StepVoxels; + float VoxelSizeCm; + float3 ConstantPad; +}; + +static const int3 CornerOffset[8] = { + int3(0,0,0), int3(1,0,0), int3(1,1,0), int3(0,1,0), + int3(0,0,1), int3(1,0,1), int3(1,1,1), int3(0,1,1) +}; + +static const int2 EdgeCorners[12] = { + int2(0,1), int2(1,2), int2(2,3), int2(3,0), + int2(4,5), int2(5,6), int2(6,7), int2(7,4), + int2(0,4), int2(1,5), int2(2,6), int2(3,7) +}; + +uint CellIndex(uint3 c) +{ + return (c.z * CellsPerAxis + c.y) * CellsPerAxis + c.x; +} + +uint SampleIndex(int3 p) +{ + uint3 q = uint3(p + 1); + return (q.z * SampleDim + q.y) * SampleDim + q.x; +} + +uint LatticeSign(StructuredBuffer signs, int3 p) +{ + uint i = SampleIndex(p); + return (signs[i >> 5] >> (i & 31)) & 1; +} + +float LatticeValue(StructuredBuffer density, int3 p) +{ + return density[SampleIndex(p)]; +} + +float3 LatticeGradient(StructuredBuffer density, int3 p) +{ + return float3( + LatticeValue(density, p + int3(1,0,0)) - LatticeValue(density, p - int3(1,0,0)), + LatticeValue(density, p + int3(0,1,0)) - LatticeValue(density, p - int3(0,1,0)), + LatticeValue(density, p + int3(0,0,1)) - LatticeValue(density, p - int3(0,0,1))); +} + +float3 EdgePosition(int3 cell, uint edge, StructuredBuffer density, + StructuredBuffer midpointIds, + StructuredBuffer midpointRecords) +{ + const int2 corners = EdgeCorners[edge]; + const int3 pa = cell + CornerOffset[corners.x]; + const int3 pb = cell + CornerOffset[corners.y]; + const float da = LatticeValue(density, pa); + const float db = LatticeValue(density, pb); + const uint midId = midpointIds[CellIndex(uint3(cell))]; + float mid = 0.0f; + uint hasMid = 0; + if (midId != 0xffffffffu) + { + const MidpointRecord record = midpointRecords[midId]; + hasMid = (record.metadata.x >> edge) & 1; + mid = record.values[edge >> 2][edge & 3]; + } + const float t = VF_FieldRefineRoot(da, db, mid, hasMid); + const float3 originCm = float3(OriginX, OriginY, OriginZ) * VoxelSizeCm; + const float3 a = originCm + float3(pa * StepVoxels) * VoxelSizeCm; + const float3 b = originCm + float3(pb * StepVoxels) * VoxelSizeCm; + return lerp(a, b, t); +} + +float3 EdgeNormal(int3 cell, uint edge, StructuredBuffer density, + StructuredBuffer midpointIds, + StructuredBuffer midpointRecords) +{ + const int2 corners = EdgeCorners[edge]; + const int3 pa = cell + CornerOffset[corners.x]; + const int3 pb = cell + CornerOffset[corners.y]; + const float da = LatticeValue(density, pa); + const float db = LatticeValue(density, pb); + const uint midId = midpointIds[CellIndex(uint3(cell))]; + float mid = 0.0f; + uint hasMid = 0; + if (midId != 0xffffffffu) + { + const MidpointRecord record = midpointRecords[midId]; + hasMid = (record.metadata.x >> edge) & 1; + mid = record.values[edge >> 2][edge & 3]; + } + const float t = VF_FieldRefineRoot(da, db, mid, hasMid); + const float3 ga = LatticeGradient(density, pa); + const float3 gb = LatticeGradient(density, pb); + const float3 g = lerp(ga, gb, t); + const float magnitude = length(g); + return magnitude > 1.0e-20f ? g / magnitude : float3(0,0,1); +} + +StructuredBuffer Signs : register(t0); +StructuredBuffer TriTable : register(t1); +RWStructuredBuffer CellCases : register(u0); +RWStructuredBuffer CellLocalOffsets : register(u1); +RWStructuredBuffer GroupVertexCounts : register(u2); + +groupshared uint ScanValues[256]; + +[numthreads(256,1,1)] +void ClassifyCS(uint3 dispatchId : SV_DispatchThreadID, + uint3 groupId : SV_GroupID, + uint3 groupThreadId : SV_GroupThreadID) +{ + const uint index = dispatchId.x; + uint vertexCount = 0; + uint caseIndex = 0; + if (index < CellCount) + { + const uint3 c = uint3(index % CellsPerAxis, + (index / CellsPerAxis) % CellsPerAxis, + index / (CellsPerAxis * CellsPerAxis)); + [unroll] + for (uint corner = 0; corner < 8; ++corner) + caseIndex |= LatticeSign(Signs, int3(c) + CornerOffset[corner]) << corner; + [unroll] + for (uint slot = 0; slot < 15; ++slot) + { + if (TriTable[caseIndex * 16 + slot] < 0) break; + ++vertexCount; + } + CellCases[index] = caseIndex; + } + ScanValues[groupThreadId.x] = vertexCount; + GroupMemoryBarrierWithGroupSync(); + [unroll] + for (uint offset = 1; offset < 256; offset <<= 1) + { + const uint add = groupThreadId.x >= offset ? ScanValues[groupThreadId.x - offset] : 0; + GroupMemoryBarrierWithGroupSync(); + ScanValues[groupThreadId.x] += add; + GroupMemoryBarrierWithGroupSync(); + } + if (index < CellCount) + CellLocalOffsets[index] = ScanValues[groupThreadId.x] - vertexCount; + if (groupThreadId.x == 255) + GroupVertexCounts[groupId.x] = ScanValues[255]; +} + +StructuredBuffer InputGroupVertexCounts : register(t8); +RWStructuredBuffer GroupOffsets : register(u3); +RWByteAddressBuffer DrawArgs : register(u4); + +[numthreads(1,1,1)] +void ScanGroupsCS(uint3 dispatchId : SV_DispatchThreadID) +{ + uint offset = 0; + for (uint i = 0; i < GroupCount; ++i) + { + GroupOffsets[i] = offset; + offset += InputGroupVertexCounts[i]; + } + DrawArgs.Store(0, offset); + DrawArgs.Store(4, 1); + DrawArgs.Store(8, 0); + DrawArgs.Store(12, 0); + DrawArgs.Store(16, 0); +} + +StructuredBuffer Density : register(t2); +StructuredBuffer Cases : register(t3); +StructuredBuffer LocalOffsets : register(t4); +StructuredBuffer PrefixGroups : register(t5); +StructuredBuffer Triangles : register(t1); +StructuredBuffer MidpointCellIds : register(t6); +StructuredBuffer MidpointData : register(t7); +RWByteAddressBuffer VertexBytes : register(u5); +RWByteAddressBuffer IndexBytes : register(u6); + +[numthreads(256,1,1)] +void EmitCS(uint3 dispatchId : SV_DispatchThreadID) +{ + const uint index = dispatchId.x; + if (index >= CellCount) return; + const uint caseIndex = Cases[index]; + const uint base = LocalOffsets[index] + PrefixGroups[index / 256]; + const uint3 cell = uint3(index % CellsPerAxis, + (index / CellsPerAxis) % CellsPerAxis, + index / (CellsPerAxis * CellsPerAxis)); + [unroll] + for (uint slot = 0; slot < 15; ++slot) + { + uint tableSlot = slot; + if (slot % 3 == 1) tableSlot = slot + 1; + else if (slot % 3 == 2) tableSlot = slot - 1; + const int edgeValue = Triangles[caseIndex * 16 + tableSlot]; + if (edgeValue < 0) break; + const uint edge = uint(edgeValue); + GpuVertex v; + v.position = EdgePosition(int3(cell), edge, Density, MidpointCellIds, MidpointData); + v.normal = EdgeNormal(int3(cell), edge, Density, MidpointCellIds, MidpointData); + v.uv = v.position.xy / VoxelSizeCm; + const uint byteAddress = (base + slot) * 32; + VertexBytes.Store3(byteAddress, asuint(v.position)); + VertexBytes.Store3(byteAddress + 12, asuint(v.normal)); + VertexBytes.Store2(byteAddress + 24, asuint(v.uv)); + IndexBytes.Store((base + slot) * 4, base + slot); + } +} + +// Smoke probe for the dual-language noise path. This is intentionally separate from the mesh +// dispatch until shape records can be exported from the exact CPU layout compiler. +struct NoiseProbeInput { float3 position; uint seed; }; +StructuredBuffer NoiseInputs : register(t9); +RWStructuredBuffer NoiseOutputs : register(u6); + +[numthreads(64,1,1)] +void NoiseProbeCS(uint3 dispatchId : SV_DispatchThreadID) +{ + const NoiseProbeInput input = NoiseInputs[dispatchId.x]; + NoiseOutputs[dispatchId.x] = VF_FieldSurfaceRelief( + input.position.x, input.position.y, input.position.z, input.seed); +} diff --git a/Standalone/Research/OwnerShapeWorld/gpu_first_port/first_run.log b/Standalone/Research/OwnerShapeWorld/gpu_first_port/first_run.log new file mode 100644 index 0000000..a80ffe5 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_first_port/first_run.log @@ -0,0 +1,30 @@ +[GpuProcessMemory] stage=after_snapshot working_set_mb=30.75 peak_working_set_mb=35.45 private_mb=22.21 +[GridStairDistanceRepair] exact_endpoint_proxies=34 occupancy_corrections=2 +[Walkability] PASS 5642/5642 passage centerline samples remain open; native landing floors retained +[FarParents] pitch=2048 rooms=6050 links=18401 clip=(-57344,-57344)..(49152,49152) +[FarShapeStage] parent_shapes=9.145 ms spatial_bins=35.649 ms total=44.794 ms primitives=18401 +[ShapeProfile] local_primitives=37.698ms line_bins=3.984ms rock_lines_wall=21.393ms rock_lines_worker=140.496ms candidate_axes_wall=50.826ms candidate_axes_worker=355.792ms candidate_merge_wall=14.305ms candidate_merge_worker=54.843ms candidate_merge_serial=1.372ms detail_lines_wall=216.813ms detail_lines_worker=1709.270ms +[ShapeAllocation] words=36.71MiB candidate_axis_words=36.71MiB proxy=263.57MiB primitives=11.25MiB spatial_bins=422754 spatial_bin_refs=679503 spatial_bin_capacity=1810768 inline_bins=392944 overflow_bins=29810 far_primitive_build=9.145ms far_bin_rebuild=35.649ms +[ShapeCompile] primitives=18401 rooms=77 tunnels=84 passages=2 nodes=138188413 packed=36.71 MiB surface_proxy_nodes=6755507 proxy_address_capacity=263.57 MiB workers=8 +[ShapeStage] local-shapes=41.704 ms intervals+signs=95.925 ms surface-band=232.287 ms far-parents=44.794 ms total=419.984 ms candidates=9389017 flips=392124 walk=5642/5642 +[RoomFloorJoinAudit] automatic_room_overlaps=10 thin_positive_support_overlays=0 status=PASS +[FloorSlopeJoinAudit] raw_unmarked_max_delta=4.43634 vox generated_tangent_delta=0.00000 vox bend_direction_slope_change=0.80702 vox marked_max_delta=0.00000 vox marked_joins=0 threshold=0.750 status=PASS +[FloorProfileBlend] c1_segments=374 max_curve_deviation=0.993 vox threshold=2.000 status=PASS +[TunnelRadiusAudit] shared_joins=280 legacy_fixed_segment_max_step=2.9156 vox smoothed_profile_max_step=1.7487 vox tapered_endpoint_max_delta=0.000000 vox radius_slope_join_delta=0.00000000 floor_endpoint_max_delta=0.000000 vox floor_slope_join_delta=0.00000 unmarked_floor_slope_delta=0.00000 marked_floor_slope_delta=0.00000 marked_slope_joins=0 smooth_union_growth_bound=0.2500 vox thresholds=(0.005,0.000001,0.005,0.25) status=PASS +[TunnelBendAudit] sharpest_bend=47.29 deg centerline=(611.64,72.10,-213.54) vox tangent=(-0.8106,-0.4443,0.3814) camera_yaw=-118.73 camera_pitch=22.42 +[GpuPortWorld] desired_tiles=2226 camera_vox=(-46.450,-472.885,-79.381) full_resolution_lods=2 coarse_cells=16 shape_compile_ms=420.016 shape_stats_ms=(41.704,95.925,232.287,44.794) memory_limit_mb=8192 +[GpuProcessMemory] stage=after_shape_compile working_set_mb=245.78 peak_working_set_mb=245.78 private_mb=409.59 +[GpuDevices] hardware="AMD Radeon RX 7900 XTX" warp=Microsoft_WARP feature_level=11_0 +[SharedFieldMath] probes=4096 cpu_gpu_mean_abs=0 cpu_gpu_max_abs=0 cpu_gpu_bit_differences=0 repeat_max_abs=0 repeat_bit_differences=0 warp_mean_abs=0 warp_max_abs=0 warp_bit_differences=0 hash_integer_source=shared +[RouteCriticalSearch] lod0_tiles=512 result=no_protected_air_samples_found +[GpuTile] key=(-2,-15,-3) lod=0 cells=32 source_samples=42875 critical_lattice=0 critical_cells=0 cpu_vertices=1103 gpu_vertices=6192 warp_vertices=6192 expected_gpu_vertices=6192 gpu_indices_sequential=1 warp_indices_sequential=1 gpu_indexed_indirect_args=1 warp_indexed_indirect_args=1 d3d_buffer_mb=17.815 gpu_adapter_memory_mb=(11.56,12.56) budget_mb=23748.98 warp_adapter_memory_info=1 gpu_dispatch_ms=0.0490 gpu_wall_ms=3.5378 repeat_dispatch_ms=0.0447 warp_dispatch_ms=65.4443 warp_wall_ms=80.8484 +[GpuAgreement] tile=(-2,-15,-3,0) topology_diff_cells_hw=0 topology_diff_cells_warp=0 route_critical_cells_audited=0 route_critical_diff_cells_hw=0 route_critical_diff_cells_warp=0 thin_feature_diff_cells_hw=0 vertex_mean_cm=4.5180239e-07 vertex_max_cm=0.00012207031 vertex_max_pair_cm=(-1075.00000,-11225.00000,-1857.88184)->(-1075.00000,-11225.00000,-1857.88171) warp_vertex_mean_cm=2.1685618e-07 warp_vertex_max_cm=0.00012207031 repeat_vertex_component_bit_differences=0 repeat_case_differences=0 repeat_draw_arg_differences=0 repeated_gpu_outputs_identical=1 warp_component_bit_differences=8296 output_count_match=1 status=PASS +[GpuPipelineWall] key=(-2,-15,-3) lod=0 cpu_field_input_prep_ms=3.7120 hardware_total_ms=7.2498 warp_total_ms=84.5604 (GPU wall includes input upload and output readback) +[CpuTileReference] key=(-2,-15,-3) lod=0 wall_ms=2.3287 mode=single_generateTile_api cpu_surface_vertices=1103 cpu_indices=7872 +[GpuProcessMemory] stage=after_tile_pair working_set_mb=404.26 peak_working_set_mb=407.15 private_mb=619.71 +[GpuTile] key=(-1,-10,-2) lod=1 cells=32 source_samples=42875 critical_lattice=0 critical_cells=0 cpu_vertices=2237 gpu_vertices=12504 warp_vertices=12504 expected_gpu_vertices=12504 gpu_indices_sequential=1 warp_indices_sequential=1 gpu_indexed_indirect_args=1 warp_indexed_indirect_args=1 d3d_buffer_mb=17.990 gpu_adapter_memory_mb=(31.56,31.56) budget_mb=23748.98 warp_adapter_memory_info=1 gpu_dispatch_ms=0.0711 gpu_wall_ms=2.2075 repeat_dispatch_ms=0.0440 warp_dispatch_ms=64.7658 warp_wall_ms=84.1260 +[GpuAgreement] tile=(-1,-10,-2,1) topology_diff_cells_hw=0 topology_diff_cells_warp=0 route_critical_cells_audited=0 route_critical_diff_cells_hw=0 route_critical_diff_cells_warp=0 thin_feature_diff_cells_hw=0 vertex_mean_cm=1.2568044e-06 vertex_max_cm=0.00024414062 vertex_max_pair_cm=(-1250.00000,-15850.00000,-2761.93750)->(-1250.00000,-15850.00000,-2761.93726) warp_vertex_mean_cm=2.4474895e-07 warp_vertex_max_cm=0.00024414062 repeat_vertex_component_bit_differences=0 repeat_case_differences=0 repeat_draw_arg_differences=0 repeated_gpu_outputs_identical=1 warp_component_bit_differences=16287 output_count_match=1 status=PASS +[GpuPipelineWall] key=(-1,-10,-2) lod=1 cpu_field_input_prep_ms=0.6316 hardware_total_ms=2.8391 warp_total_ms=84.7576 (GPU wall includes input upload and output readback) +[CpuTileReference] key=(-1,-10,-2) lod=1 wall_ms=1.9918 mode=single_generateTile_api cpu_surface_vertices=2237 cpu_indices=16128 +[GpuProcessMemory] stage=after_tile_pair working_set_mb=426.37 peak_working_set_mb=428.65 private_mb=658.75 +[GpuPortStatus] subset_status=PASS overall_port=INCOMPLETE work=one_nearby_lod0_and_lod1_tile field=cpu_exact_lattice_plus_cpu_midpoint_records meshing=gpu_compute_vertex_index_buffers viewer_draw=not_integrated captures=not_integrated