wip(standalone): GPU step 8 partial -- GPU skirts, first-present hold; remote gap open

Luna's session ended on an API "Bad Request" before a final report; state is
unverified by me. In the log: GPU clipped-skirt emitter (count/scan/emit,
protected signs and critical air honoured), exposed-skirt audit passing at
the end; first-present stall reproduced on every launch (2.36-2.59 s, not
exe identity/idle/CPU run) and hidden by holding the CPU-ready frame until
GPU residency. Remote LOD3-4 band still differs from CPU; draw ranges and the
one inspected LOD3 tile match, cause not yet found (one-tile probe mid-fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-07 02:19:38 +02:00
1 parent 90421d0655
commit f2de8fe9c6
42 files changed
+1381 -59

No files matched your search

@@ -120,7 +120,7 @@ if ($Target -eq 'All') {
Sort-Object -Property FullName -Descending | Select-Object -First 1
if ($FxC) {
[IO.Directory]::CreateDirectory($PrecompiledShaderDir) | Out-Null
foreach ($Entry in @('EvaluateFarFieldCS','ClassifyCS','ClassifyFarCS','ScanGroupsCS','EmitCS','NoiseProbeCS','BuildFarConnectorRingsCS','EmitFarConnectorPanelsCS')) {
foreach ($Entry in @('EvaluateFarFieldCS','ClassifyCS','ClassifyFarCS','ScanGroupsCS','EmitCS','NoiseProbeCS','BuildFarConnectorRingsCS','EmitFarConnectorPanelsCS','ClassifyFarSkirtsCS','ScanFarSkirtsCS','EmitFarSkirtsCS')) {
$ShaderBinary = Join-Path $PrecompiledShaderDir ($Entry+'.cso')
$CommandLine = '"{0}" /nologo /T cs_5_0 /E {1} /O3 /Gis /Ges /Fo "{2}" "{3}"' -f $FxC.FullName,$Entry,$ShaderBinary,$ShaderSource
Invoke-FieldViewerBuildCommand $CommandLine "precompile HLSL entry $Entry"
@@ -0,0 +1,70 @@
import argparse
from pathlib import Path
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parent
parser = argparse.ArgumentParser()
parser.add_argument("--prefix", default="step8")
args = parser.parse_args()
if not args.prefix or any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for c in args.prefix):
raise SystemExit("prefix must contain only letters, digits, underscores, or hyphens")
VIEWS = (
"chamber",
"tunnel",
"passage",
"far",
"passage_floor_closeup",
"tunnel_bend",
"remote_yaw0",
"remote_yaw90",
"remote_yaw180",
"remote_yaw270",
)
rows = []
for view in VIEWS:
cpu_path = ROOT / f"{args.prefix}_cpu_{view}.png"
gpu_path = ROOT / f"{args.prefix}_gpu_{view}.png"
cpu = np.asarray(Image.open(cpu_path).convert("RGB"), dtype=np.int16)
gpu = np.asarray(Image.open(gpu_path).convert("RGB"), dtype=np.int16)
if cpu.shape != gpu.shape:
raise RuntimeError(f"dimension mismatch for {view}: {cpu.shape} vs {gpu.shape}")
diff = np.abs(cpu - gpu)
pixel_max = diff.max(axis=2)
rows.append(
{
"view": view,
"size": f"{cpu.shape[1]}x{cpu.shape[0]}",
"mae_rgb": float(diff.mean()),
"rmse_rgb": float(np.sqrt(np.square(diff, dtype=np.float64).mean())),
"p95_pixel_max": float(np.percentile(pixel_max, 95)),
"p99_pixel_max": float(np.percentile(pixel_max, 99)),
"max_pixel_channel": int(pixel_max.max()),
"changed_pct": float(np.count_nonzero(pixel_max) * 100.0 / pixel_max.size),
"over_8_pct": float(np.count_nonzero(pixel_max > 8) * 100.0 / pixel_max.size),
"over_32_pct": float(np.count_nonzero(pixel_max > 32) * 100.0 / pixel_max.size),
}
)
lines = [
f"# {args.prefix} CPU/GPU capture comparison",
"",
"Each pair is a 1280x800 back-buffer capture from the fixed camera. `mae_rgb` and `rmse_rgb` are channel errors on the 0 to 255 scale; pixel percentiles use the maximum RGB channel error per pixel.",
"",
"| View | MAE RGB | RMSE RGB | p95 pixel max | p99 pixel max | Max channel | Changed pixels | >8 | >32 |",
"|---|---:|---:|---:|---:|---:|---:|---:|---:|",
]
for row in rows:
lines.append(
f"| {row['view']} | {row['mae_rgb']:.4f} | {row['rmse_rgb']:.4f} | "
f"{row['p95_pixel_max']:.0f} | {row['p99_pixel_max']:.0f} | {row['max_pixel_channel']} | "
f"{row['changed_pct']:.2f}% | {row['over_8_pct']:.2f}% | {row['over_32_pct']:.2f}% |"
)
report = ROOT / f"{args.prefix}_capture_comparison.md"
report.write_text("\n".join(lines) + "\n", encoding="utf-8")
print("\n".join(lines))
print(f"\nWrote {report}")
@@ -0,0 +1,50 @@
from pathlib import Path
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parent
rows = []
for lod in range(2, 9):
cpu = np.asarray(
Image.open(ROOT / f"step8_lod{lod}_cpu_remote_yaw0.png").convert("RGB"),
dtype=np.int16,
)
gpu = np.asarray(
Image.open(ROOT / f"step8_lod{lod}_gpu_remote_yaw0.png").convert("RGB"),
dtype=np.int16,
)
if cpu.shape != gpu.shape:
raise RuntimeError(f"LOD {lod} dimensions differ: {cpu.shape} vs {gpu.shape}")
diff = np.abs(cpu - gpu)
pixel_max = diff.max(axis=2)
rows.append(
{
"lod": lod,
"mae": float(diff.mean()),
"rmse": float(np.sqrt(np.square(diff, dtype=np.float64).mean())),
"changed": float(np.count_nonzero(pixel_max) * 100.0 / pixel_max.size),
"over32": float(np.count_nonzero(pixel_max > 32) * 100.0 / pixel_max.size),
"max": int(pixel_max.max()),
}
)
lines = [
"# Step 8 remote yaw 0, isolated LOD render comparison",
"",
"The viewer generated and fully-resident clip radius 3 for each pair; `--capture-only-lod` filtered drawing only.",
"",
"| LOD | MAE RGB | RMSE RGB | Changed pixels | Pixels >32 | Max channel |",
"|---:|---:|---:|---:|---:|---:|",
]
for row in rows:
lines.append(
f"| {row['lod']} | {row['mae']:.4f} | {row['rmse']:.4f} | "
f"{row['changed']:.2f}% | {row['over32']:.2f}% | {row['max']} |"
)
report = ROOT / "step8_lod_isolation_comparison.md"
report.write_text("\n".join(lines) + "\n", encoding="utf-8")
print("\n".join(lines))
print(f"\nWrote {report}")
@@ -26,6 +26,7 @@
#include <stdexcept>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -111,7 +112,9 @@ namespace
struct FarConnectorConstants
{
std::uint32_t connectorCount=0,ringCount=0,vertexCount=0,panelVertexCapacity=0;
std::uint32_t shapeCount=0,padding[3]{};
std::uint32_t shapeCount=0,skirtFaceMask=0;
float skirtDepthVoxels=0.0f;
std::uint32_t padding=0;
};
static_assert(sizeof(FarConnectorConstants)==32);
@@ -120,6 +123,8 @@ namespace
float position[3];
float normal[3];
float uv[2];
friend bool operator==(const GpuVertex& a,const GpuVertex& b) noexcept
{return std::memcmp(&a,&b,sizeof(GpuVertex))==0;}
};
static_assert(sizeof(GpuVertex) == 32);
@@ -179,12 +184,14 @@ namespace
struct GpuResult
{
std::vector<GpuVertex> vertices;
std::vector<GpuVertex> skirtVertices;
std::vector<std::uint32_t> cases;
std::vector<float> fieldDensity;
ComPtr<ID3D11Buffer> vertexBuffer, indexBuffer, indirectArgs;
ComPtr<ID3D11Buffer> connectorVertexBuffer,connectorIndexBuffer;
ComPtr<ID3D11Query> startTimestamp,endTimestamp,disjointTimestamp;
std::array<std::uint32_t,5> drawArgs{};
std::array<std::uint32_t,6> connectorDrawArgs{};
bool sequentialIndices = false;
bool indexedIndirectArgsValid = false;
std::uint32_t outputBaseVertex = 0, outputArgsByteOffset = 0;
@@ -195,7 +202,7 @@ namespace
std::uint64_t d3dBufferBytes = 0;
double dispatchMs = 0.0;
double fieldMs = 0.0;
std::array<double,6> kernelMs{};
std::array<double,9> kernelMs{};
std::uint64_t fieldDistanceEvaluations=0;
std::uint32_t maxFieldDistanceEvaluationsPerSample=0;
double wallMs = 0.0;
@@ -506,6 +513,7 @@ namespace
struct FarOutputChunk
{
ComPtr<ID3D11Buffer> vertices, indices, args;
ComPtr<ID3D11ShaderResourceView> surfaceVertexSrv;
ComPtr<ID3D11UnorderedAccessView> vertexUav, indexUav, argsUav;
ComPtr<ID3D11Buffer> connectorVertices,connectorIndices;
ComPtr<ID3D11UnorderedAccessView> connectorVertexUav,connectorIndexUav;
@@ -528,6 +536,9 @@ namespace
ComPtr<ID3D11ShaderResourceView> connectorRingSrv;
ComPtr<ID3D11UnorderedAccessView> densityUav,nearNormalDensityUav,caseUav,localOffsetUav,groupCountUav,groupOffsetUav;
ComPtr<ID3D11UnorderedAccessView> connectorRingUav;
ComPtr<ID3D11Buffer> skirtScratch;
ComPtr<ID3D11UnorderedAccessView> skirtScratchUav;
std::uint32_t skirtScratchCapacity=0;
DynamicInput shapes,ranges,ids,protectedAir,protectedSolid,nearPackedProxy;
DynamicInput connectors;
std::uint32_t connectorRingCapacity=0;
@@ -542,6 +553,7 @@ namespace
ComPtr<ID3D11DeviceContext> immediateContext,deferredContext;
ComPtr<ID3D11ComputeShader> fieldEval, classify, fieldClassify, scan, emit, noiseProbe;
ComPtr<ID3D11ComputeShader> connectorRings,connectorPanels;
ComPtr<ID3D11ComputeShader> classifyFarSkirts,scanFarSkirts,emitFarSkirts;
ComPtr<IDXGIAdapter3> memoryAdapter;
std::string name;
UINT64 timestampFrequency = 0;
@@ -550,7 +562,10 @@ namespace
std::uint32_t farOutputSlotsPerChunk=128,farMaxVerticesPerTile=0;
static constexpr std::uint32_t FarOutputVertexCapacityPerTile=
VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile;
static constexpr std::uint32_t FarPanelVertexCapacity=28160;
// Full clip-radius-3 skirt audit peaks at 27,840 panel+skirt vertices per
// far tile; this leaves 4,928 records of headroom without allocating 49k
// records for every near and far output slot.
static constexpr std::uint32_t FarPanelVertexCapacity=32768;
void CompileShaders(const std::filesystem::path& shaderPath)
{
@@ -586,6 +601,9 @@ namespace
Create("NoiseProbeCS",noiseProbe);
Create("BuildFarConnectorRingsCS",connectorRings);
Create("EmitFarConnectorPanelsCS",connectorPanels);
Create("ClassifyFarSkirtsCS",classifyFarSkirts);
Create("ScanFarSkirtsCS",scanFarSkirts);
Create("EmitFarSkirtsCS",emitFarSkirts);
std::printf("[GpuPipelineProfile] stage=shader_pipeline_total duration_ms=%.3f\n",
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-PipelineStart).count());
}
@@ -783,17 +801,18 @@ namespace
farOutputCapacitySlots-chunkIndex*farOutputSlotsPerChunk);
const std::uint64_t vertexBytes=static_cast<std::uint64_t>(slots)*farMaxVerticesPerTile*sizeof(GpuVertex);
const std::uint64_t indexBytes=static_cast<std::uint64_t>(slots)*farMaxVerticesPerTile*sizeof(std::uint32_t);
const std::uint64_t argsBytes=static_cast<std::uint64_t>(slots)*10u*sizeof(std::uint32_t);
const std::uint64_t argsBytes=static_cast<std::uint64_t>(slots)*11u*sizeof(std::uint32_t);
if(vertexBytes>std::numeric_limits<UINT>::max()||indexBytes>std::numeric_limits<UINT>::max())
throw std::runtime_error("far output chunk exceeds the D3D11 buffer size limit");
FarOutputChunk chunk;
chunk.vertices=RawMeshBuffer(static_cast<UINT>(vertexBytes),
D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_VERTEX_BUFFER);
D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_VERTEX_BUFFER|D3D11_BIND_SHADER_RESOURCE);
chunk.indices=RawMeshBuffer(static_cast<UINT>(indexBytes),
D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_INDEX_BUFFER);
chunk.args=RawMeshBuffer(static_cast<UINT>(argsBytes),D3D11_BIND_UNORDERED_ACCESS,
D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS|D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS);
chunk.vertexUav=RawUav(chunk.vertices.Get(),static_cast<UINT>(vertexBytes/sizeof(std::uint32_t)));
chunk.surfaceVertexSrv=RawSrv(chunk.vertices.Get(),static_cast<UINT>(vertexBytes/sizeof(std::uint32_t)));
chunk.indexUav=RawUav(chunk.indices.Get(),static_cast<UINT>(indexBytes/sizeof(std::uint32_t)));
chunk.argsUav=RawUav(chunk.args.Get(),static_cast<UINT>(argsBytes/sizeof(std::uint32_t)));
const std::uint64_t connectorVertexBytes=static_cast<std::uint64_t>(slots)
@@ -885,6 +904,9 @@ namespace
scratch.localOffsets=Structured<std::uint32_t>(nullptr,cells,D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_SHADER_RESOURCE);
scratch.groupCounts=Structured<std::uint32_t>(nullptr,groups,D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_SHADER_RESOURCE);
scratch.groupOffsets=Structured<std::uint32_t>(nullptr,groups,D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_SHADER_RESOURCE);
scratch.skirtScratchCapacity=std::max<std::uint32_t>(1,farMaxVerticesPerTile);
scratch.skirtScratch=Structured<std::uint32_t>(nullptr,scratch.skirtScratchCapacity,
D3D11_BIND_UNORDERED_ACCESS);
scratch.signSrv=Srv<std::uint32_t>(scratch.signs.Get(),1);
scratch.triSrv=Srv<std::int32_t>(scratch.tri.Get(),256u*16u);
scratch.densitySrv=Srv<float>(scratch.density.Get(),samples);
@@ -901,6 +923,7 @@ namespace
scratch.localOffsetUav=Uav(scratch.localOffsets.Get(),cells);
scratch.groupCountUav=Uav(scratch.groupCounts.Get(),groups);
scratch.groupOffsetUav=Uav(scratch.groupOffsets.Get(),groups);
scratch.skirtScratchUav=Uav(scratch.skirtScratch.Get(),scratch.skirtScratchCapacity);
const auto DynamicConstant=[&](UINT bytes,const char* label)
{
D3D11_BUFFER_DESC desc{};desc.ByteWidth=bytes;desc.Usage=D3D11_USAGE_DYNAMIC;
@@ -938,6 +961,19 @@ namespace
return view;
}
ComPtr<ID3D11ShaderResourceView> RawSrv(ID3D11Buffer* buffer,UINT dwordCount)
{
D3D11_SHADER_RESOURCE_VIEW_DESC desc{};
desc.Format=DXGI_FORMAT_R32_TYPELESS;
desc.ViewDimension=D3D11_SRV_DIMENSION_BUFFEREX;
desc.BufferEx.NumElements=dwordCount;
desc.BufferEx.Flags=D3D11_BUFFEREX_SRV_FLAG_RAW;
ComPtr<ID3D11ShaderResourceView> view;
Check(device->CreateShaderResourceView(buffer,&desc,&view),
"CreateShaderResourceView(raw surface vertices)");
return view;
}
void WaitForQuery(ID3D11Query* query)
{
for (;;)
@@ -998,7 +1034,7 @@ namespace
const std::uint32_t localSlot=outputSlot%farOutputSlotsPerChunk;
pooledChunk=&farOutputChunks[chunkIndex];
outputBaseVertex=localSlot*farMaxVerticesPerTile;
outputArgsByteOffset=localSlot*10u*sizeof(std::uint32_t);
outputArgsByteOffset=localSlot*11u*sizeof(std::uint32_t);
}
if (farField && (farField->sampleDim != static_cast<std::int32_t>(tile.dim)
|| farField->cells != static_cast<std::int32_t>(tile.cells)
@@ -1153,36 +1189,49 @@ namespace
}
ComPtr<ID3D11Buffer> args,vertices,indices;
ComPtr<ID3D11UnorderedAccessView> argsUav,vertexUav,indexUav;
ComPtr<ID3D11ShaderResourceView> surfaceVertexSrv;
ComPtr<ID3D11Buffer> connectorVertices,connectorIndices;
ComPtr<ID3D11UnorderedAccessView> connectorVertexUav,connectorIndexUav;
ComPtr<ID3D11Buffer> skirtScratchBuffer;
ComPtr<ID3D11UnorderedAccessView> skirtScratchUav;
const std::uint32_t connectorPanelCapacity=pooledChunk?FarPanelVertexCapacity:
std::max<std::uint32_t>(1,farField?farField->connectorVertexCount:0);
std::max<std::uint32_t>(1,farField&&(farField->visibleSkirtFaceMask!=0)
?FarPanelVertexCapacity:(farField?farField->connectorVertexCount:0));
if(farField&&farField->connectorVertexCount>connectorPanelCapacity)
throw std::runtime_error("far connector repair output exceeds the pooled panel capacity");
if(pooledChunk)
{
args=pooledChunk->args;vertices=pooledChunk->vertices;indices=pooledChunk->indices;
surfaceVertexSrv=pooledChunk->surfaceVertexSrv;
argsUav=pooledChunk->argsUav;vertexUav=pooledChunk->vertexUav;indexUav=pooledChunk->indexUav;
connectorVertices=pooledChunk->connectorVertices;connectorIndices=pooledChunk->connectorIndices;
connectorVertexUav=pooledChunk->connectorVertexUav;connectorIndexUav=pooledChunk->connectorIndexUav;
skirtScratchBuffer=farScratch.skirtScratch;skirtScratchUav=farScratch.skirtScratchUav;
}
else
{
args = RawMeshBuffer(10 * sizeof(std::uint32_t), D3D11_BIND_UNORDERED_ACCESS,
args = RawMeshBuffer(11 * sizeof(std::uint32_t), D3D11_BIND_UNORDERED_ACCESS,
D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS | D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS);
vertices = RawMeshBuffer(static_cast<UINT>(maxVertices * sizeof(GpuVertex)),
D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_VERTEX_BUFFER);
D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_SHADER_RESOURCE);
indices = RawMeshBuffer(static_cast<UINT>(maxVertices * sizeof(std::uint32_t)),
D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_INDEX_BUFFER);
argsUav=RawUav(args.Get(),10);
argsUav=RawUav(args.Get(),11);
vertexUav=RawUav(vertices.Get(),static_cast<UINT>(maxVertices*sizeof(GpuVertex)/sizeof(std::uint32_t)));
indexUav=RawUav(indices.Get(),static_cast<UINT>(maxVertices));
surfaceVertexSrv=RawSrv(vertices.Get(),static_cast<UINT>(maxVertices*sizeof(GpuVertex)/sizeof(std::uint32_t)));
connectorVertices=RawMeshBuffer(connectorPanelCapacity*sizeof(GpuVertex),
D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_VERTEX_BUFFER);
connectorIndices=RawMeshBuffer(connectorPanelCapacity*sizeof(std::uint32_t),
D3D11_BIND_UNORDERED_ACCESS|D3D11_BIND_INDEX_BUFFER);
connectorVertexUav=RawUav(connectorVertices.Get(),connectorPanelCapacity*sizeof(GpuVertex)/sizeof(std::uint32_t));
connectorIndexUav=RawUav(connectorIndices.Get(),connectorPanelCapacity);
if(farField&&farField->visibleSkirtFaceMask!=0)
{
skirtScratchBuffer=Structured<std::uint32_t>(nullptr,
static_cast<std::uint32_t>(maxVertices),D3D11_BIND_UNORDERED_ACCESS);
skirtScratchUav=Uav(skirtScratchBuffer.Get(),static_cast<UINT>(maxVertices));
}
}
D3D11_BUFFER_DESC cbDesc{};
cbDesc.ByteWidth = sizeof(TileConstants);
@@ -1213,7 +1262,9 @@ namespace
const FarConnectorConstants connectorConstants{
static_cast<std::uint32_t>(farField->connectorPanels.size()),
farField->connectorRingCount,farField->connectorVertexCount,connectorPanelCapacity,
static_cast<std::uint32_t>(farField->worldInput->shapes.size()),{0,0,0}};
static_cast<std::uint32_t>(farField->worldInput->shapes.size()),
static_cast<std::uint32_t>(farField->visibleSkirtFaceMask),
farField->skirtDepthVoxels,0};
if(pooledChunk)
{
cb=farScratch.tileConstants;farCb=farScratch.farConstants;
@@ -1255,7 +1306,7 @@ namespace
D3D11_QUERY_DESC disjointDesc{D3D11_QUERY_TIMESTAMP_DISJOINT, 0};
ComPtr<ID3D11Query> startQuery, endQuery, disjointQuery;
ComPtr<ID3D11Query> fieldStartQuery,fieldEndQuery;
std::array<ComPtr<ID3D11Query>,6> kernelStartQueries,kernelEndQueries;
std::array<ComPtr<ID3D11Query>,9> kernelStartQueries,kernelEndQueries;
if(captureGpuTiming)
{
Check(device->CreateQuery(&timestampDesc, &startQuery), "CreateQuery(start)");
@@ -1388,6 +1439,49 @@ namespace
context->CSSetUnorderedAccessViews(0,7,nullUavs,nullptr);
}
}
if(farField&&farField->visibleSkirtFaceMask!=0)
{
ID3D11Buffer* skirtCbs[3]={cb.Get(),farCb.Get(),farConnectorConstantsBuffer.Get()};
context->CSSetConstantBuffers(0,3,skirtCbs);
ID3D11ShaderResourceView* skirtSrvs[20]{};
skirtSrvs[10]=farShapeSrv.Get();skirtSrvs[11]=farRangeSrv.Get();
skirtSrvs[12]=farIdSrv.Get();skirtSrvs[13]=farProtectedSrv.Get();
skirtSrvs[14]=farProtectedSolidSrv.Get();skirtSrvs[19]=surfaceVertexSrv.Get();
context->CSSetShaderResources(0,20,skirtSrvs);
ID3D11UnorderedAccessView* classifySkirtUavs[8]{};
classifySkirtUavs[0]=skirtScratchUav.Get();classifySkirtUavs[4]=argsUav.Get();
const std::uint32_t skirtDispatchVertices=constants.outputVertexCapacity;
beginKernel(6);
context->CSSetShader(classifyFarSkirts.Get(),nullptr,0);
context->CSSetUnorderedAccessViews(0,8,classifySkirtUavs,nullptr);
context->Dispatch((skirtDispatchVertices+63u)/64u,1,1);
endKernel(6);
ID3D11ShaderResourceView* nullSkirtSrvs[20]{};
ID3D11UnorderedAccessView* nullSkirtUavs[8]{};
context->CSSetShaderResources(0,20,nullSkirtSrvs);
context->CSSetUnorderedAccessViews(0,8,nullSkirtUavs,nullptr);
beginKernel(7);
context->CSSetShader(scanFarSkirts.Get(),nullptr,0);
ID3D11UnorderedAccessView* scanSkirtUavs[8]{};
scanSkirtUavs[0]=skirtScratchUav.Get();scanSkirtUavs[4]=argsUav.Get();
context->CSSetUnorderedAccessViews(0,8,scanSkirtUavs,nullptr);
context->Dispatch(1,1,1);
endKernel(7);
context->CSSetUnorderedAccessViews(0,8,nullSkirtUavs,nullptr);
context->CSSetShaderResources(0,20,skirtSrvs);
beginKernel(8);
context->CSSetShader(emitFarSkirts.Get(),nullptr,0);
ID3D11UnorderedAccessView* emitSkirtUavs[8]{};
emitSkirtUavs[0]=skirtScratchUav.Get();emitSkirtUavs[4]=argsUav.Get();
emitSkirtUavs[5]=connectorVertexUav.Get();emitSkirtUavs[6]=connectorIndexUav.Get();
context->CSSetUnorderedAccessViews(0,8,emitSkirtUavs,nullptr);
context->Dispatch((skirtDispatchVertices+63u)/64u,1,1);
endKernel(8);
context->CSSetShaderResources(0,20,nullSkirtSrvs);
context->CSSetUnorderedAccessViews(0,8,nullSkirtUavs,nullptr);
}
if(captureGpuTiming)
{
context->End(endQuery.Get());
@@ -1405,11 +1499,13 @@ namespace
* sizeof(GpuVertex)));
auto indexStaging = Staging(static_cast<UINT>(readbackCapacity
* sizeof(std::uint32_t)));
auto argsStaging = Staging(10 * sizeof(std::uint32_t));
auto connectorVertexStaging=farField&&farField->connectorVertexCount
?Staging(farField->connectorVertexCount*sizeof(GpuVertex)):ComPtr<ID3D11Buffer>{};
auto connectorIndexStaging=farField&&farField->connectorVertexCount
?Staging(farField->connectorVertexCount*sizeof(std::uint32_t)):ComPtr<ID3D11Buffer>{};
auto argsStaging = Staging(11 * sizeof(std::uint32_t));
const bool hasConnectorOutput=farField&&(farField->connectorVertexCount
||farField->visibleSkirtFaceMask!=0);
auto connectorVertexStaging=hasConnectorOutput
?Staging(connectorPanelCapacity*sizeof(GpuVertex)):ComPtr<ID3D11Buffer>{};
auto connectorIndexStaging=hasConnectorOutput
?Staging(connectorPanelCapacity*sizeof(std::uint32_t)):ComPtr<ID3D11Buffer>{};
if (QueryLocalMemory(localUsage, localBudget))
{
result.hasAdapterMemoryInfo = true;
@@ -1436,14 +1532,14 @@ namespace
indices.Get(), 0, &indexSource);
}
const D3D11_BOX argsSource{outputArgsByteOffset,0,0,
outputArgsByteOffset+10u*sizeof(std::uint32_t),1,1};
outputArgsByteOffset+11u*sizeof(std::uint32_t),1,1};
context->CopySubresourceRegion(argsStaging.Get(),0,0,0,0,args.Get(),0,&argsSource);
std::uint32_t connectorBase=0;
if(farField&&farField->connectorVertexCount)
if(hasConnectorOutput)
{
connectorBase=pooledChunk?outputBaseVertex/farMaxVerticesPerTile*FarPanelVertexCapacity:0;
const UINT vertexBytes=farField->connectorVertexCount*sizeof(GpuVertex);
const UINT indexBytes=farField->connectorVertexCount*sizeof(std::uint32_t);
const UINT vertexBytes=connectorPanelCapacity*sizeof(GpuVertex);
const UINT indexBytes=connectorPanelCapacity*sizeof(std::uint32_t);
const D3D11_BOX connectorVertexSource{connectorBase*sizeof(GpuVertex),0,0,
connectorBase*sizeof(GpuVertex)+vertexBytes,1,1};
const D3D11_BOX connectorIndexSource{connectorBase*sizeof(std::uint32_t),0,0,
@@ -1490,7 +1586,8 @@ namespace
if(profileGpuKernels&&!disjoint.Disjoint&&disjoint.Frequency)
for(std::size_t stage=0;stage<result.kernelMs.size();++stage)
{
if(stage==5&&farField->connectorVertexCount==0)continue;
if((stage==5&&farField->connectorVertexCount==0)
||(stage>=6&&farField->visibleSkirtFaceMask==0))continue;
const UINT64 stageStart=ReadQuery<UINT64>(kernelStartQueries[stage].Get(),
"GetData(kernel stage start)");
const UINT64 stageEnd=ReadQuery<UINT64>(kernelEndQueries[stage].Get(),
@@ -1505,18 +1602,23 @@ namespace
static_cast<std::uint32_t*>(mapped.pData) + cellCount);
context->Unmap(caseStaging.Get(), 0);
Check(context->Map(argsStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(args)");
std::array<std::uint32_t,10> allArgs{};
std::copy_n(static_cast<const std::uint32_t*>(mapped.pData),10,allArgs.begin());
std::array<std::uint32_t,11> allArgs{};
std::copy_n(static_cast<const std::uint32_t*>(mapped.pData),11,allArgs.begin());
std::copy_n(allArgs.begin(),5,result.drawArgs.begin());
std::copy_n(allArgs.begin()+5,6,result.connectorDrawArgs.begin());
context->Unmap(argsStaging.Get(), 0);
result.indexedIndirectArgsValid = (useIndirectOutputCount
?result.drawArgs[0]<=maxVertices
:result.drawArgs[0] == tile.expectedVertexCount)
&& result.drawArgs[1] == 1 && result.drawArgs[2] == outputBaseVertex
&& result.drawArgs[3] == 0 && result.drawArgs[4] == 0;
result.indexedIndirectArgsValid &= allArgs[5]==(farField?farField->connectorVertexCount:0)
&&allArgs[6]==(farField&&farField->connectorVertexCount?1u:0u)
&&allArgs[7]==connectorBase&&allArgs[8]==connectorBase&&allArgs[9]==0;
const std::uint32_t connectorOutputCount=std::min<std::uint32_t>(allArgs[5],
connectorPanelCapacity);
result.indexedIndirectArgsValid &= allArgs[5]>=
(farField?farField->connectorVertexCount:0)
&&allArgs[6]==(connectorOutputCount>0?1u:0u)
&&allArgs[7]==connectorBase&&allArgs[8]==0&&allArgs[9]==0
&&allArgs[10]==0;
const std::uint32_t outputCount = std::min<std::uint32_t>(result.drawArgs[0],
static_cast<std::uint32_t>(maxVertices));
// The staging buffer contains the GPU output. Only retain the prefix named by
@@ -1531,18 +1633,22 @@ namespace
for (std::uint32_t i = 0; i < outputCount; ++i)
result.sequentialIndices &= indexData[i] == outputBaseVertex+i;
context->Unmap(indexStaging.Get(), 0);
if(farField&&farField->connectorVertexCount)
if(hasConnectorOutput&&connectorOutputCount>0)
{
Check(context->Map(connectorVertexStaging.Get(),0,D3D11_MAP_READ,0,&mapped),
"Map(far connector panel vertices)");
"Map(far connector and skirt vertices)");
const auto* connectorData=static_cast<const GpuVertex*>(mapped.pData);
const std::uint32_t panelOutputCount=std::min<std::uint32_t>(
farField->connectorVertexCount,connectorOutputCount);
result.vertices.insert(result.vertices.end(),connectorData,
connectorData+farField->connectorVertexCount);
connectorData+panelOutputCount);
result.skirtVertices.assign(connectorData+panelOutputCount,
connectorData+connectorOutputCount);
context->Unmap(connectorVertexStaging.Get(),0);
Check(context->Map(connectorIndexStaging.Get(),0,D3D11_MAP_READ,0,&mapped),
"Map(far connector panel indices)");
"Map(far connector and skirt indices)");
const auto* connectorIndexData=static_cast<const std::uint32_t*>(mapped.pData);
for(std::uint32_t i=0;i<farField->connectorVertexCount;++i)
for(std::uint32_t i=0;i<connectorOutputCount;++i)
result.sequentialIndices &= connectorIndexData[i]==connectorBase+i;
context->Unmap(connectorIndexStaging.Get(),0);
}
@@ -1618,6 +1724,7 @@ namespace
TileKey key;
FarFieldTileInput input;
double exportPreparationMs=0.0;
std::uint8_t skirtFaceMask=0;
};
struct SpatialKey
@@ -1892,6 +1999,38 @@ namespace
return result;
}
std::uint8_t ComputeVisibleFarSkirtFaceMask(const TileKey& tile,
const std::unordered_set<TileKey,TileKeyHash>& desired)
{
const std::int64_t extent=static_cast<std::int64_t>(32)<<tile.lod;
const std::int64_t origin[3]={tile.coordinate.x*extent,
tile.coordinate.y*extent,tile.coordinate.z*extent};
const auto floorDiv=[](std::int64_t value,std::int64_t divisor)
{
std::int64_t quotient=value/divisor;
if(value%divisor<0)--quotient;
return static_cast<std::int32_t>(quotient);
};
std::uint8_t mask=0;
for(int axis=0;axis<3;++axis)
for(int side=0;side<2;++side)
{
std::int64_t probe[3]={origin[0]+extent/2,origin[1]+extent/2,origin[2]+extent/2};
probe[axis]=side?origin[axis]+extent:origin[axis]-1;
int neighborLod=-1;
for(int lod=0;lod<=8;++lod)
{
const std::int64_t neighborExtent=static_cast<std::int64_t>(32)<<lod;
const TileKey candidate{{floorDiv(probe[0],neighborExtent),
floorDiv(probe[1],neighborExtent),floorDiv(probe[2],neighborExtent)},lod};
if(desired.contains(candidate)){neighborLod=lod;break;}
}
if(neighborLod<0||neighborLod!=tile.lod)
mask|=static_cast<std::uint8_t>(1u<<(axis*2+side));
}
return mask;
}
void PrintProcessMemory(const char* stage)
{
PROCESS_MEMORY_COUNTERS_EX memory{};
@@ -1906,6 +2045,185 @@ namespace
}
}
struct SkirtAuditResult
{
std::uint64_t cpuTriangles=0,gpuTriangles=0,topologyDifferenceTriangles=0;
std::uint64_t quantizedKeyDifferenceTriangles=0;
double meanVertexDistanceCm=0.0;
float maxVertexDistanceCm=0.0f;
float vertexBoundCm=0.15f;
bool pass=false;
};
SkirtAuditResult AuditGpuSkirts(World& world,const TileMesh& cpuMesh,
const std::vector<GpuVertex>& gpuSkirtVertices,std::uint8_t visibleFaceMask)
{
using FQuantizedVertex=std::array<std::int64_t,3>;
using FTriangleKey=std::array<std::int64_t,9>;
std::map<FTriangleKey,std::uint32_t> cpuTriangles,gpuTriangles;
std::vector<GpuVertex> cpuVertices,gpuVertices;
std::vector<std::array<GpuVertex,3>> cpuGeometry,gpuGeometry;
const auto makeVertex=[](double x,double y,double z,double nx,double ny,double nz)
{
GpuVertex vertex{};
vertex.position[0]=static_cast<float>(x);
vertex.position[1]=static_cast<float>(y);
vertex.position[2]=static_cast<float>(z);
vertex.normal[0]=static_cast<float>(nx);
vertex.normal[1]=static_cast<float>(ny);
vertex.normal[2]=static_cast<float>(nz);
return vertex;
};
const auto triangleKey=[](const GpuVertex* vertices)
{
std::array<FQuantizedVertex,3> points{};
for(std::size_t i=0;i<3;++i)
points[i]={static_cast<std::int64_t>(std::llround(vertices[i].position[0])),
static_cast<std::int64_t>(std::llround(vertices[i].position[1])),
static_cast<std::int64_t>(std::llround(vertices[i].position[2]))};
std::sort(points.begin(),points.end());
FTriangleKey key{};std::size_t out=0;
for(const auto& point:points)for(const auto coordinate:point)key[out++]=coordinate;
return key;
};
const std::size_t cpuTriangleCount=cpuMesh.indices.size()/3;
for(std::size_t triangle=0;triangle<cpuTriangleCount;++triangle)
{
const std::uint8_t faceMask=triangle<cpuMesh.skirtFaceMasks.size()
?cpuMesh.skirtFaceMasks[triangle]:0;
if((faceMask&visibleFaceMask)==0)continue;
GpuVertex points[3]{};
bool validTriangle=true;
for(std::size_t corner=0;corner<3;++corner)
{
const std::uint32_t index=cpuMesh.indices[triangle*3+corner];
if(index>=cpuMesh.vertices.size()){validTriangle=false;break;}
const MeshVertex& source=cpuMesh.vertices[index];
points[corner]=makeVertex(source.position.x,source.position.y,source.position.z,
source.normal.x,source.normal.y,source.normal.z);
cpuVertices.push_back(points[corner]);
}
if(!validTriangle)continue;
cpuGeometry.push_back({points[0],points[1],points[2]});
++cpuTriangles[triangleKey(points)];
}
for(std::size_t triangle=0;triangle+2<gpuSkirtVertices.size();triangle+=3)
{
const GpuVertex points[3]={gpuSkirtVertices[triangle],gpuSkirtVertices[triangle+1],
gpuSkirtVertices[triangle+2]};
gpuGeometry.push_back({points[0],points[1],points[2]});
++gpuTriangles[triangleKey(points)];
gpuVertices.insert(gpuVertices.end(),std::begin(points),std::end(points));
}
SkirtAuditResult result;
result.cpuTriangles=cpuVertices.size()/3;
result.gpuTriangles=gpuVertices.size()/3;
auto cpuIt=cpuTriangles.begin(),gpuIt=gpuTriangles.begin();
while(cpuIt!=cpuTriangles.end()||gpuIt!=gpuTriangles.end())
{
if(gpuIt==gpuTriangles.end()||(cpuIt!=cpuTriangles.end()&&cpuIt->first<gpuIt->first))
{result.quantizedKeyDifferenceTriangles+=cpuIt->second;++cpuIt;}
else if(cpuIt==cpuTriangles.end()||gpuIt->first<cpuIt->first)
{result.quantizedKeyDifferenceTriangles+=gpuIt->second;++gpuIt;}
else
{
result.quantizedKeyDifferenceTriangles+=cpuIt->second>gpuIt->second
?cpuIt->second-gpuIt->second:gpuIt->second-cpuIt->second;
++cpuIt;++gpuIt;
}
}
const auto vertexDistance=[](const GpuVertex& a,const GpuVertex& b)
{
const double dx=static_cast<double>(a.position[0])-b.position[0];
const double dy=static_cast<double>(a.position[1])-b.position[1];
const double dz=static_cast<double>(a.position[2])-b.position[2];
return std::sqrt(dx*dx+dy*dy+dz*dz);
};
constexpr std::uint8_t permutations[6][3]={
{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}};
std::vector<bool> matched(gpuGeometry.size(),false);
for(const auto& cpuTriangle:cpuGeometry)
{
std::size_t best=gpuGeometry.size();double bestError=std::numeric_limits<double>::infinity();
for(std::size_t candidate=0;candidate<gpuGeometry.size();++candidate)
{
if(matched[candidate])continue;
for(const auto& permutation:permutations)
{
double maximum=0.0,sum=0.0;
for(std::size_t corner=0;corner<3;++corner)
{
const double distance=vertexDistance(cpuTriangle[corner],
gpuGeometry[candidate][permutation[corner]]);
maximum=std::max(maximum,distance);sum+=distance;
}
if(maximum<=0.15&&sum<bestError)
{best=candidate;bestError=sum;}
}
}
if(best==gpuGeometry.size())++result.topologyDifferenceTriangles;
else matched[best]=true;
}
result.topologyDifferenceTriangles+=static_cast<std::uint64_t>(
std::count(matched.begin(),matched.end(),false));
const DistanceStats cpuToGpu=NearestDistance(cpuVertices,gpuVertices);
const DistanceStats gpuToCpu=NearestDistance(gpuVertices,cpuVertices);
const std::uint64_t comparisonCount=cpuToGpu.count+gpuToCpu.count;
result.meanVertexDistanceCm=comparisonCount
?(cpuToGpu.meanCm*cpuToGpu.count+gpuToCpu.meanCm*gpuToCpu.count)/comparisonCount:0.0;
result.maxVertexDistanceCm=std::max(cpuToGpu.maxCm,gpuToCpu.maxCm);
if(result.maxVertexDistanceCm>1.0f)
{
const auto normalAngleDegrees=[](const GpuVertex& a,const GpuVertex& b)
{
const double al=std::sqrt(double(a.normal[0])*a.normal[0]
+double(a.normal[1])*a.normal[1]+double(a.normal[2])*a.normal[2]);
const double bl=std::sqrt(double(b.normal[0])*b.normal[0]
+double(b.normal[1])*b.normal[1]+double(b.normal[2])*b.normal[2]);
if(al<=1.0e-9||bl<=1.0e-9)return 180.0;
const double dot=(double(a.normal[0])*b.normal[0]
+double(a.normal[1])*b.normal[1]+double(a.normal[2])*b.normal[2])/(al*bl);
return std::acos(std::clamp(dot,-1.0,1.0))*57.29577951308232;
};
const auto fieldDensity=[&](const GpuVertex& vertex)
{
return world.densityAt({static_cast<double>(vertex.position[0])/25.0,
static_cast<double>(vertex.position[1])/25.0,
static_cast<double>(vertex.position[2])/25.0});
};
double maxCpuToGpu=0.0,maxGpuToCpu=0.0;
const GpuVertex* cpuWorst=nullptr;const GpuVertex* gpuNearest=nullptr;
const GpuVertex* gpuWorst=nullptr;const GpuVertex* cpuNearest=nullptr;
for(const GpuVertex& cpu:cpuVertices)
{
double best=std::numeric_limits<double>::infinity();const GpuVertex* nearest=nullptr;
for(const GpuVertex& gpu:gpuVertices)
{const double distance=vertexDistance(cpu,gpu);if(distance<best){best=distance;nearest=&gpu;}}
if(best>maxCpuToGpu){maxCpuToGpu=best;cpuWorst=&cpu;gpuNearest=nearest;}
}
for(const GpuVertex& gpu:gpuVertices)
{
double best=std::numeric_limits<double>::infinity();const GpuVertex* nearest=nullptr;
for(const GpuVertex& cpu:cpuVertices)
{const double distance=vertexDistance(gpu,cpu);if(distance<best){best=distance;nearest=&cpu;}}
if(best>maxGpuToCpu){maxGpuToCpu=best;gpuWorst=&gpu;cpuNearest=nearest;}
}
if(cpuWorst&&gpuNearest&&gpuWorst&&cpuNearest)
std::printf("[GpuSkirtAuditWorst] cpu_to_gpu_cm=%.6f cpu=(%.5f,%.5f,%.5f) cpu_field=%.9g gpu_near=(%.5f,%.5f,%.5f) gpu_near_cpu_field=%.9g normal_angle_deg=%.6f gpu_to_cpu_cm=%.6f gpu=(%.5f,%.5f,%.5f) gpu_cpu_field=%.9g cpu_near=(%.5f,%.5f,%.5f) cpu_near_field=%.9g normal_angle_deg=%.6f\n",
maxCpuToGpu,cpuWorst->position[0],cpuWorst->position[1],cpuWorst->position[2],
fieldDensity(*cpuWorst),gpuNearest->position[0],gpuNearest->position[1],
gpuNearest->position[2],fieldDensity(*gpuNearest),
normalAngleDegrees(*cpuWorst,*gpuNearest),maxGpuToCpu,gpuWorst->position[0],
gpuWorst->position[1],gpuWorst->position[2],fieldDensity(*gpuWorst),
cpuNearest->position[0],cpuNearest->position[1],cpuNearest->position[2],
fieldDensity(*cpuNearest),
normalAngleDegrees(*gpuWorst,*cpuNearest));
}
result.pass=result.cpuTriangles==result.gpuTriangles
&&result.topologyDifferenceTriangles==0&&result.maxVertexDistanceCm<=result.vertexBoundCm;
return result;
}
bool ReportTile(World& world,const TileData& tile, const TileMesh& cpuMesh,
const GpuResult& hwFirst, const GpuResult& hwSecond,
const GpuResult& warp,const FarFieldTileInput& farInput)
@@ -1928,6 +2246,8 @@ namespace
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];
for(std::size_t i=0;i<hwFirst.connectorDrawArgs.size();++i)
repeatArgsDiff+=hwFirst.connectorDrawArgs[i]!=hwSecond.connectorDrawArgs[i];
const std::size_t repeatCount = std::min(hwFirst.vertices.size(), hwSecond.vertices.size());
for (std::size_t i = 0; i < repeatCount; ++i)
{
@@ -1935,6 +2255,20 @@ namespace
const std::uint32_t* b = reinterpret_cast<const std::uint32_t*>(&hwSecond.vertices[i]);
for (int c = 0; c < 8; ++c) repeatBitDiff += a[c] != b[c];
}
repeatBitDiff+=hwFirst.skirtVertices.size()!=hwSecond.skirtVertices.size();
for(std::size_t i=0;i<std::min(hwFirst.skirtVertices.size(),hwSecond.skirtVertices.size());++i)
{
const std::uint32_t* a=reinterpret_cast<const std::uint32_t*>(&hwFirst.skirtVertices[i]);
const std::uint32_t* b=reinterpret_cast<const std::uint32_t*>(&hwSecond.skirtVertices[i]);
for(int c=0;c<8;++c)repeatBitDiff+=a[c]!=b[c];
}
warpBitDiff+=hwFirst.skirtVertices.size()!=warp.skirtVertices.size();
for(std::size_t i=0;i<std::min(hwFirst.skirtVertices.size(),warp.skirtVertices.size());++i)
{
const std::uint32_t* a=reinterpret_cast<const std::uint32_t*>(&hwFirst.skirtVertices[i]);
const std::uint32_t* b=reinterpret_cast<const std::uint32_t*>(&warp.skirtVertices[i]);
for(int c=0;c<8;++c)warpBitDiff+=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)
{
@@ -2132,6 +2466,34 @@ namespace
static_cast<unsigned long long>(fieldSignDiff),
static_cast<unsigned long long>(fieldCaseLatticeSignDiff),
static_cast<unsigned long long>(fieldHaloSignDiff));
SkirtAuditResult skirtAudit{};
SkirtAuditResult warpSkirtAudit{};
bool skirtAuditPass=hwFirst.skirtVertices.empty()&&warp.skirtVertices.empty();
if(farInput.visibleSkirtFaceMask!=0)
{
skirtAudit=AuditGpuSkirts(world,cpuMesh,hwFirst.skirtVertices,farInput.visibleSkirtFaceMask);
warpSkirtAudit=AuditGpuSkirts(world,cpuMesh,warp.skirtVertices,farInput.visibleSkirtFaceMask);
skirtAuditPass=skirtAudit.pass&&warpSkirtAudit.pass;
std::printf("[GpuSkirtAudit] tile=(%d,%d,%d,lod%d) face_mask=0x%02x cpu_triangles=%llu gpu_triangles=%llu topology_diff_triangles_at_0.15cm=%llu quantized_key_diff_triangles_1cm=%llu vertex_mean_cm=%.8g vertex_max_cm=%.8g vertex_bound_cm=%.2f connector_output_vertices=%zu skirt_vertices=%zu overflow=%u status=%s\n",
tile.key.coordinate.x,tile.key.coordinate.y,tile.key.coordinate.z,tile.key.lod,
static_cast<unsigned>(farInput.visibleSkirtFaceMask),
static_cast<unsigned long long>(skirtAudit.cpuTriangles),
static_cast<unsigned long long>(skirtAudit.gpuTriangles),
static_cast<unsigned long long>(skirtAudit.topologyDifferenceTriangles),
static_cast<unsigned long long>(skirtAudit.quantizedKeyDifferenceTriangles),
skirtAudit.meanVertexDistanceCm,skirtAudit.maxVertexDistanceCm,
skirtAudit.vertexBoundCm,hwFirst.vertices.size()-tile.expectedVertexCount,
hwFirst.skirtVertices.size(),hwFirst.connectorDrawArgs[5],skirtAudit.pass?"PASS":"FAIL");
std::printf("[WarpSkirtAudit] tile=(%d,%d,%d,lod%d) face_mask=0x%02x cpu_triangles=%llu warp_triangles=%llu topology_diff_triangles_at_0.15cm=%llu quantized_key_diff_triangles_1cm=%llu vertex_mean_cm=%.8g vertex_max_cm=%.8g vertex_bound_cm=%.2f overflow=%u status=%s\n",
tile.key.coordinate.x,tile.key.coordinate.y,tile.key.coordinate.z,tile.key.lod,
static_cast<unsigned>(farInput.visibleSkirtFaceMask),
static_cast<unsigned long long>(warpSkirtAudit.cpuTriangles),
static_cast<unsigned long long>(warpSkirtAudit.gpuTriangles),
static_cast<unsigned long long>(warpSkirtAudit.topologyDifferenceTriangles),
static_cast<unsigned long long>(warpSkirtAudit.quantizedKeyDifferenceTriangles),
warpSkirtAudit.meanVertexDistanceCm,warpSkirtAudit.maxVertexDistanceCm,
warpSkirtAudit.vertexBoundCm,warp.connectorDrawArgs[5],warpSkirtAudit.pass?"PASS":"FAIL");
}
const std::size_t expectedGpuOutputCount=static_cast<std::size_t>(tile.expectedVertexCount)
+farInput.connectorVertexCount;
const bool outputCountMatch = hwFirst.vertices.size() == expectedGpuOutputCount
@@ -2140,12 +2502,14 @@ namespace
const bool vertexAgreement=maximum<=vertexBoundCm&&cpuWarpMaximum<=vertexBoundCm&&warpMaximum<=vertexBoundCm;
const bool repeatedOutputsIdentical = repeatBitDiff == 0 && repeatCaseDiff == 0
&& repeatArgsDiff == 0 && hwFirst.vertices.size() == hwSecond.vertices.size()
&&hwFirst.skirtVertices.size()==hwSecond.skirtVertices.size()
&& hwFirst.sequentialIndices && hwSecond.sequentialIndices
&& hwFirst.indexedIndirectArgsValid && hwSecond.indexedIndirectArgsValid;
const bool status = hwTopologyDiff == 0 && warpTopologyDiff == 0
&&fieldCount==tile.density.size()
&&warpFieldCount==tile.density.size()
&& repeatedOutputsIdentical && outputCountMatch
&&skirtAuditPass
&&protectedAirSignDiffHw==0&&protectedAirSignDiffWarp==0
&&protectedSolidSignDiffHw==0&&protectedSolidSignDiffWarp==0
&&thinFeatureDiffGpu==0&&thinFeatureDiffWarp==0
@@ -2190,6 +2554,7 @@ namespace
bool wholeFar=false;
bool wholeNear=false;
bool profileGpuKernels=false;
bool skirtAuditOnly=false;
int clipRadiusOverride=-1;
int tileLimit=0;
bool hasCameraOverride=false;
@@ -2199,6 +2564,7 @@ namespace
if(std::string_view(argv[i])=="--whole-far")wholeFar=true;
if(std::string_view(argv[i])=="--whole-near")wholeNear=true;
if(std::string_view(argv[i])=="--profile-kernels")profileGpuKernels=true;
if(std::string_view(argv[i])=="--skirt-audit")skirtAuditOnly=true;
const std::string_view arg=argv[i];
if(arg.starts_with("--tile-limit="))
tileLimit=std::max(0,std::atoi(std::string(arg.substr(13)).c_str()));
@@ -2276,13 +2642,32 @@ namespace
static_cast<unsigned long long>(noiseRepeat.differingBits), noiseWarp.mean,
noiseWarp.maximum, static_cast<unsigned long long>(noiseWarp.differingBits));
const std::vector<FarSelection> selected = SelectFarTiles(world, config, camera,wholeFar,wholeNear);
std::vector<FarSelection> selected = skirtAuditOnly
?std::vector<FarSelection>{}:SelectFarTiles(world, config, camera,wholeFar,wholeNear);
if(skirtAuditOnly)
{
if(!config.generateSkirts)
throw std::runtime_error("--skirt-audit requires the owner's skirt option to be enabled");
const std::unordered_set<TileKey,TileKeyHash> desiredSet(desired.begin(),desired.end());
selected.clear();
for(const TileKey& key:desired)
{
if(key.lod<std::max(config.fullResolutionLodCount,2)||key.lod>config.maxLod
||!world.isFarFieldTileEligible(key))continue;
const std::uint8_t mask=ComputeVisibleFarSkirtFaceMask(key,desiredSet);
if(mask==0)continue;
selected.push_back({key,{},0.0,mask});
}
if(selected.empty())throw std::runtime_error("no desired far tile had exposed CPU skirt triangles");
std::printf("[GpuSkirtAuditSelection] candidate_tiles=%zu mode=all-visible-far-faces\n",
selected.size());
}
if (selected.empty()) throw std::runtime_error("no far-field tiles were selected");
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;
std::array<double,6> gpuKernelStageSum{};
std::array<double,9> gpuKernelStageSum{};
std::uint64_t gpuFieldDistanceEvaluations=0,gpuFieldSamples=0;
std::uint32_t gpuFieldMaxDistanceEvaluationsPerSample=0;
std::uint64_t nearTransitionSkipped=0,topologyDiffGpuTotal=0,topologyDiffWarpTotal=0;
@@ -2296,7 +2681,7 @@ namespace
FarFieldTileInput farInputStorage;
const FarFieldTileInput* farInput=&selection.input;
double exportMs=selection.exportPreparationMs;
if(wholeFar||wholeNear)
if((wholeFar||wholeNear)&&!skirtAuditOnly)
{
const auto exportStart=Clock::now();
auto exported=wholeNear?world.exportGpuFieldTile(key,error)
@@ -2322,6 +2707,25 @@ namespace
auto cpuMesh = world.generateTile(key, options, error);
const double cpuWallMs = std::chrono::duration<double, std::milli>(Clock::now() - cpuStart).count();
if (!cpuMesh) throw std::runtime_error("CPU reference tile failed: " + error);
if(skirtAuditOnly)
{
std::uint64_t exposedTriangles=0;
for(const std::uint8_t faces:cpuMesh->skirtFaceMasks)
exposedTriangles+=(faces&selection.skirtFaceMask)!=0;
const auto exportStart=Clock::now();
auto exported=world.exportFarFieldTile(key,error);
exportMs=std::chrono::duration<double,std::milli>(Clock::now()-exportStart).count();
if(!exported)throw std::runtime_error("skirt audit far field export failed: "+error);
farInputStorage=std::move(*exported);
farInputStorage.visibleSkirtFaceMask=selection.skirtFaceMask;
farInputStorage.skirtDepthVoxels=static_cast<float>(std::max(1,config.skirtCells)
*farInputStorage.step);
farInput=&farInputStorage;
std::printf("[GpuSkirtAuditSelection] key=(%d,%d,%d,lod%d) face_mask=0x%02x cpu_exposed_triangles=%llu\n",
key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod,
static_cast<unsigned>(selection.skirtFaceMask),
static_cast<unsigned long long>(exposedTriangles));
}
const auto inputStart = Clock::now();
TileData tile = MakeTileData(world, key, config, *cpuMesh, true,farInput);
const double inputWallMs = std::chrono::duration<double, std::milli>(Clock::now() - inputStart).count();
@@ -2359,11 +2763,14 @@ namespace
&&gpu1.vertices.size() == static_cast<std::size_t>(tile.expectedVertexCount)+farInput->connectorVertexCount
&& gpu2.vertices.size() == gpu1.vertices.size()
&& warpResult.vertices.size() == static_cast<std::size_t>(tile.expectedVertexCount)+farInput->connectorVertexCount
&&gpu1.skirtVertices==gpu2.skirtVertices
&&warpResult.skirtVertices.size()==gpu1.skirtVertices.size()
&& 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
&&gpu1.connectorDrawArgs==gpu2.connectorDrawArgs
&& agreementPass;
allPass &= tilePass;
++testedTiles;cpuTileWallSum+=cpuWallMs;cpuFieldPrepSum+=inputWallMs;
@@ -2403,9 +2810,10 @@ namespace
static_cast<unsigned long long>(protectedSolidOverlaySamples));
if(profileGpuKernels)
{
std::printf("[GpuKernelProfile] tiles=%llu field_ms=%.6f classify_ms=%.6f scan_ms=%.6f emit_ms=%.6f connector_rings_ms=%.6f connector_panels_ms=%.6f field_shape_distance_evaluations=%llu field_lattice_samples=%llu average_shape_distance_evaluations_per_lattice_sample=%.4f max_shape_distance_evaluations_per_lattice_sample=%u\n",
std::printf("[GpuKernelProfile] tiles=%llu field_ms=%.6f classify_ms=%.6f scan_ms=%.6f emit_ms=%.6f connector_rings_ms=%.6f connector_panels_ms=%.6f skirt_classify_ms=%.6f skirt_scan_ms=%.6f skirt_emit_ms=%.6f field_shape_distance_evaluations=%llu field_lattice_samples=%llu average_shape_distance_evaluations_per_lattice_sample=%.4f max_shape_distance_evaluations_per_lattice_sample=%u\n",
static_cast<unsigned long long>(testedTiles),gpuKernelStageSum[0],gpuKernelStageSum[1],
gpuKernelStageSum[2],gpuKernelStageSum[3],gpuKernelStageSum[4],gpuKernelStageSum[5],
gpuKernelStageSum[6],gpuKernelStageSum[7],gpuKernelStageSum[8],
static_cast<unsigned long long>(gpuFieldDistanceEvaluations),
static_cast<unsigned long long>(gpuFieldSamples),
gpuFieldSamples?static_cast<double>(gpuFieldDistanceEvaluations)/gpuFieldSamples:0.0,
@@ -2544,7 +2952,8 @@ namespace VoxelForgeGpuFarViewer
auditReadback!=nullptr,auditReadback!=nullptr,outputSlot,true,captureGpuTiming,batchTimingMode);
if(auditReadback)
{
auditReadback->vertices.resize(result.vertices.size());
const std::size_t completeGpuVertexCount=result.vertices.size()+result.skirtVertices.size();
auditReadback->vertices.resize(completeGpuVertexCount);
for(std::size_t i=0;i<result.vertices.size();++i)
{
for(int c=0;c<3;++c)
@@ -2553,7 +2962,16 @@ namespace VoxelForgeGpuFarViewer
auditReadback->vertices[i].normal[c]=result.vertices[i].normal[c];
}
}
auditReadback->indices.resize(result.vertices.size());
for(std::size_t i=0;i<result.skirtVertices.size();++i)
{
const GpuVertex& source=result.skirtVertices[i];
for(int c=0;c<3;++c)
{
auditReadback->vertices[result.vertices.size()+i].position[c]=source.position[c];
auditReadback->vertices[result.vertices.size()+i].normal[c]=source.normal[c];
}
}
auditReadback->indices.resize(completeGpuVertexCount);
std::iota(auditReadback->indices.begin(),auditReadback->indices.end(),0u);
}
if(completionEvent)
@@ -0,0 +1,73 @@
[CmdletBinding()]
param(
[string]$ExecutableName='VoxelForgeGpuFieldStep8SkirtViewerC.exe',
[string]$CapturePrefix='step8'
)
$ErrorActionPreference='Stop'
$HostProcess=Get-Process -Id $PID
$HostProcess.PriorityClass='BelowNormal'
$HostProcess.ProcessorAffinity=[IntPtr]0xFF00
$StandaloneRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$ExecutablePath=Join-Path $StandaloneRoot ('bin\'+$ExecutableName)
if([IO.Path]::GetFileName($ExecutableName)-ne$ExecutableName-or[IO.Path]::GetExtension($ExecutableName)-ne'.exe')
{throw 'ExecutableName must be an executable filename.'}
if($CapturePrefix -notmatch '^[A-Za-z0-9_-]+$')
{throw 'CapturePrefix must contain only letters, digits, underscores, or hyphens.'}
if(-not(Test-Path -LiteralPath $ExecutablePath -PathType Leaf)){throw "Viewer not found: $ExecutablePath"}
$Views=@(
@{Name='chamber';Camera='-11.75,-123.25,-19.85,0,0'},
@{Name='tunnel';Camera='-11.75,-123.25,-19.85,90,0'},
@{Name='passage';Camera='-39.19,6.27,-28.51,63,0'},
@{Name='far';Camera='-11.61244,-118.2212,-19.8452,0,0'},
@{Name='passage_floor_closeup';Camera='-39.19,6.27,-25.51,63,-25'},
@{Name='tunnel_bend';Camera='-198.97,-99.57,-45.58,33.59,23.17'},
@{Name='remote_yaw0';Camera='500,0,0,0,0'},
@{Name='remote_yaw90';Camera='500,0,0,90,0'},
@{Name='remote_yaw180';Camera='500,0,0,180,0'},
@{Name='remote_yaw270';Camera='500,0,0,270,0'}
)
foreach($View in $Views)
{
foreach($Mode in @('cpu','gpu'))
{
$Stem="${CapturePrefix}_${Mode}_$($View.Name)"
$CaptureName="$Stem.png"
$Arguments=@('--clip-radius=3','--workers=8','--quit-after-clipmap','--capture-after-clipmap',
"--camera=$($View.Camera)","--$Mode-far",
"--capture=Research/OwnerShapeWorld/gpu_field_port/$CaptureName")
$StdoutPath=Join-Path $PSScriptRoot ($Stem+'.stdout.log')
$StderrPath=Join-Path $PSScriptRoot ($Stem+'.stderr.log')
$CapturePath=Join-Path $PSScriptRoot $CaptureName
Write-Output "CAPTURE_COMMAND mode=$Mode view=$($View.Name) `"$ExecutablePath`" $($Arguments -join ' ')"
for($Attempt=0;$Attempt-lt2;$Attempt++)
{
if($Attempt-gt0)
{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 $Arguments -WorkingDirectory $StandaloneRoot `
-WindowStyle Hidden -PassThru -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$PeakWorkingSet=0L;$PeakPrivateMemory=0L;$OverMemoryLimit=$false
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)
if($Child.PrivateMemorySize64-ge8GB)
{$OverMemoryLimit=$true;$Child.Kill();$Child.WaitForExit();break}
Start-Sleep -Milliseconds 100
}
$Child.WaitForExit();$Child.Refresh()
if($Child.ExitCode-eq-1073741502-and$Attempt-eq0){continue}
if($Child.ExitCode-eq-1073741502){throw '0xC0000142 repeated after its single retry; stopping.'}
if($OverMemoryLimit){throw "Viewer stopped at the private-memory limit: $Stem"}
$LogText=Get-Content -LiteralPath $StdoutPath -Raw
if(-not$LogText.Contains('[InitialClipmapReady]')-or-not(Test-Path -LiteralPath $CapturePath -PathType Leaf))
{throw "Full-residency capture or readiness missing: $Stem, exit=$($Child.ExitCode)"}
Write-Output "CAPTURE_DONE mode=$Mode view=$($View.Name) pid=$($Child.Id) priority=BelowNormal affinity=0xFF00 exit=$($Child.ExitCode) peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) path=$CapturePath"
if($Child.ExitCode-ne0){throw "Viewer returned $($Child.ExitCode): $Stem"}
break
}
}
}
@@ -0,0 +1,56 @@
[CmdletBinding()]
param([string]$ExecutableName='VoxelForgeGpuFieldStep8SkirtViewerC.exe')
$ErrorActionPreference='Stop'
$HostProcess=Get-Process -Id $PID
$HostProcess.PriorityClass='BelowNormal'
$HostProcess.ProcessorAffinity=[IntPtr]0xFF00
$StandaloneRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$ExecutablePath=Join-Path $StandaloneRoot ('bin\'+$ExecutableName)
if([IO.Path]::GetFileName($ExecutableName)-ne$ExecutableName-or[IO.Path]::GetExtension($ExecutableName)-ne'.exe')
{throw 'ExecutableName must be an executable filename.'}
if(-not(Test-Path -LiteralPath $ExecutablePath -PathType Leaf)){throw "Viewer not found: $ExecutablePath"}
$Sequence=@(
@{Mode='cpu';Name='step8_interleaved_cpu_A'},
@{Mode='gpu';Name='step8_interleaved_gpu_A'},
@{Mode='gpu';Name='step8_interleaved_gpu_B'},
@{Mode='cpu';Name='step8_interleaved_cpu_B'},
@{Mode='cpu';Name='step8_interleaved_cpu_C'},
@{Mode='gpu';Name='step8_interleaved_gpu_C'}
)
foreach($Run in $Sequence)
{
$Arguments=@('--clip-radius=3','--workers=8','--quit-after-clipmap',"--$($Run.Mode)-far")
$StdoutPath=Join-Path $PSScriptRoot ($Run.Name+'.stdout.log')
$StderrPath=Join-Path $PSScriptRoot ($Run.Name+'.stderr.log')
Write-Output "COLD_RUN mode=$($Run.Mode) command=`"$ExecutablePath`" $($Arguments -join ' ')"
for($Attempt=0;$Attempt-lt2;$Attempt++)
{
if($Attempt-gt0)
{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 $Arguments -WorkingDirectory $StandaloneRoot `
-WindowStyle Hidden -PassThru -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$PeakWorkingSet=0L;$PeakPrivateMemory=0L;$OverMemoryLimit=$false
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)
if($Child.PrivateMemorySize64-ge8GB)
{$OverMemoryLimit=$true;$Child.Kill();$Child.WaitForExit();break}
Start-Sleep -Milliseconds 100
}
$Child.WaitForExit();$Child.Refresh()
if($Child.ExitCode-eq-1073741502-and$Attempt-eq0){continue}
if($Child.ExitCode-eq-1073741502){throw '0xC0000142 repeated after its single retry; stopping.'}
if($OverMemoryLimit){throw "Viewer stopped at the private-memory limit: $($Run.Name)"}
$LogText=Get-Content -LiteralPath $StdoutPath -Raw
if(-not$LogText.Contains('[InitialClipmapReady]'))
{throw "Full clipmap was not ready in $($Run.Name); exit=$($Child.ExitCode)"}
Write-Output "RUN_EXIT mode=$($Run.Mode) log=$($Run.Name) 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=$($Child.ExitCode)"
if($Child.ExitCode-ne0){throw "Cold run returned $($Child.ExitCode): $($Run.Name)"}
break
}
}
@@ -0,0 +1,70 @@
[CmdletBinding()]
param(
[string]$ExecutableName='VoxelForgeGpuFieldStep7SkirtSeamVerify.exe',
[int]$IdleSeconds=60
)
$ErrorActionPreference='Stop'
$HostProcess=Get-Process -Id $PID
$HostProcess.PriorityClass='BelowNormal'
$HostProcess.ProcessorAffinity=[IntPtr]0xFF00
$StandaloneRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$BinRoot=Join-Path $StandaloneRoot 'bin'
$ExecutablePath=Join-Path $BinRoot $ExecutableName
$RenamedPath=Join-Path $BinRoot 'VoxelForgeGpuFieldStep8PresentRenameControl.exe'
if([IO.Path]::GetFileName($ExecutableName)-ne$ExecutableName-or[IO.Path]::GetExtension($ExecutableName)-ne'.exe')
{throw 'ExecutableName must be an executable filename.'}
if(-not(Test-Path -LiteralPath $ExecutablePath -PathType Leaf)){throw "Viewer not found: $ExecutablePath"}
Copy-Item -LiteralPath $ExecutablePath -Destination $RenamedPath -Force
$Sequence=@(
@{Mode='gpu';Name='step8_present_same_A';Path=$ExecutablePath;Delay=0},
@{Mode='gpu';Name='step8_present_same_B';Path=$ExecutablePath;Delay=0},
@{Mode='cpu';Name='step8_present_cpu_control';Path=$ExecutablePath;Delay=0},
@{Mode='gpu';Name='step8_present_after_cpu';Path=$ExecutablePath;Delay=0},
@{Mode='gpu';Name='step8_present_after_idle';Path=$ExecutablePath;Delay=$IdleSeconds},
@{Mode='gpu';Name='step8_present_renamed_exe';Path=$RenamedPath;Delay=0}
)
foreach($Run in $Sequence)
{
if($Run.Delay-gt0)
{
Write-Output "CONTROL_DELAY seconds=$($Run.Delay) before=$($Run.Name)"
Start-Sleep -Seconds $Run.Delay
}
$Arguments=@('--clip-radius=3','--workers=8','--quit-after-clipmap',"--$($Run.Mode)-far")
$StdoutPath=Join-Path $PSScriptRoot ($Run.Name+'.stdout.log')
$StderrPath=Join-Path $PSScriptRoot ($Run.Name+'.stderr.log')
Write-Output "PRESENT_CONTROL command=`"$($Run.Path)`" $($Arguments -join ' ')"
for($Attempt=0;$Attempt-lt2;$Attempt++)
{
if($Attempt-gt0)
{Write-Output '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 $Run.Path -ArgumentList $Arguments -WorkingDirectory $StandaloneRoot `
-WindowStyle Hidden -PassThru -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$PeakPrivateMemory=0L;$OverMemoryLimit=$false
while(-not$Child.HasExited)
{
$Child.Refresh();if($Child.HasExited){break}
$Child.PriorityClass='BelowNormal';$Child.ProcessorAffinity=[IntPtr]0xFF00
$PeakPrivateMemory=[Math]::Max($PeakPrivateMemory,$Child.PrivateMemorySize64)
if($Child.PrivateMemorySize64-ge8GB)
{$OverMemoryLimit=$true;$Child.Kill();$Child.WaitForExit();break}
Start-Sleep -Milliseconds 100
}
$Child.WaitForExit();$Child.Refresh()
$ExitCode=0
if($null-ne$Child.ExitCode){$ExitCode=[int]$Child.ExitCode}
if($ExitCode-eq-1073741502-and$Attempt-eq0){continue}
if($ExitCode-eq-1073741502){throw '0xC0000142 repeated after its single retry; stopping.'}
if($OverMemoryLimit){throw "Viewer stopped at the private-memory limit: $($Run.Name)"}
$LogText=Get-Content -LiteralPath $StdoutPath -Raw
if(-not$LogText.Contains('[InitialClipmapReady]'))
{throw "Clipmap was not ready in $($Run.Name); exit=$ExitCode"}
$PresentLine=Select-String -Path $StdoutPath -Pattern '^\[PresentProfile\] ordinal=1 '
Write-Output "CONTROL_EXIT mode=$($Run.Mode) name=$($Run.Name) pid=$($Child.Id) priority=BelowNormal affinity=0xFF00 peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) first_present=$($PresentLine.Line) exit=$ExitCode"
if($ExitCode-ne0){throw "Cold run returned $ExitCode`: $($Run.Name)"}
break
}
}
@@ -0,0 +1,57 @@
[CmdletBinding()]
param([string]$SourceExecutable='VoxelForgeGpuFieldStep8SkirtViewerB.exe')
$ErrorActionPreference='Stop'
$HostProcess=Get-Process -Id $PID
$HostProcess.PriorityClass='BelowNormal'
$HostProcess.ProcessorAffinity=[IntPtr]0xFF00
$StandaloneRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$BinRoot=Join-Path $StandaloneRoot 'bin'
$SourcePath=Join-Path $BinRoot $SourceExecutable
if(-not(Test-Path -LiteralPath $SourcePath -PathType Leaf)){throw "Source viewer not found: $SourcePath"}
$Runs=@(
@{Name='step8_visible_first_blocking';WindowStyle='Normal'},
@{Name='step8_hidden_first_blocking';WindowStyle='Hidden'}
)
foreach($Run in $Runs)
{
$ExecutableName=if($Run.WindowStyle-eq'Normal')
{'VoxelForgeGpuFieldStep8VisibleFirst.exe'}else{'VoxelForgeGpuFieldStep8HiddenFirst.exe'}
$ExecutablePath=Join-Path $BinRoot $ExecutableName
Copy-Item -LiteralPath $SourcePath -Destination $ExecutablePath -Force
$Arguments=@('--gpu-far','--blocking-startup-present','--clip-radius=3','--workers=8','--quit-after-clipmap')
$StdoutPath=Join-Path $PSScriptRoot ($Run.Name+'.stdout.log')
$StderrPath=Join-Path $PSScriptRoot ($Run.Name+'.stderr.log')
Write-Output "VISIBILITY_CONTROL command=`"$ExecutablePath`" $($Arguments -join ' ') window_style=$($Run.WindowStyle)"
for($Attempt=0;$Attempt-lt2;$Attempt++)
{
if($Attempt-gt0)
{Write-Output '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 $Arguments -WorkingDirectory $StandaloneRoot `
-WindowStyle $Run.WindowStyle -PassThru -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$PeakPrivateMemory=0L;$OverMemoryLimit=$false
while(-not$Child.HasExited)
{
$Child.Refresh();if($Child.HasExited){break}
$Child.PriorityClass='BelowNormal';$Child.ProcessorAffinity=[IntPtr]0xFF00
$PeakPrivateMemory=[Math]::Max($PeakPrivateMemory,$Child.PrivateMemorySize64)
if($Child.PrivateMemorySize64-ge8GB)
{$OverMemoryLimit=$true;$Child.Kill();$Child.WaitForExit();break}
Start-Sleep -Milliseconds 100
}
$Child.WaitForExit();$Child.Refresh()
$ExitCode=0
if($null-ne$Child.ExitCode){$ExitCode=[int]$Child.ExitCode}
if($ExitCode-eq-1073741502-and$Attempt-eq0){continue}
if($ExitCode-eq-1073741502){throw '0xC0000142 repeated after its single retry; stopping.'}
if($OverMemoryLimit){throw "Viewer stopped at the private-memory limit: $($Run.Name)"}
$LogText=Get-Content -LiteralPath $StdoutPath -Raw
if(-not$LogText.Contains('[InitialClipmapReady]'))
{throw "Clipmap was not ready in $($Run.Name); exit=$ExitCode"}
$PresentLine=Select-String -Path $StdoutPath -Pattern '^\[PresentProfile\] ordinal=1 '
Write-Output "VISIBILITY_EXIT name=$($Run.Name) window_style=$($Run.WindowStyle) pid=$($Child.Id) priority=BelowNormal affinity=0xFF00 peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) first_present=$($PresentLine.Line) exit=$ExitCode"
if($ExitCode-ne0){throw "Run returned $ExitCode`: $($Run.Name)"}
break
}
}
@@ -0,0 +1,16 @@
# Step 8 CPU/GPU capture comparison
Each pair is a 1280x800 back-buffer capture from the fixed camera. `mae_rgb` and `rmse_rgb` are channel errors on the 0 to 255 scale; pixel percentiles use the maximum RGB channel error per pixel.
| View | MAE RGB | RMSE RGB | p95 pixel max | p99 pixel max | Max channel | Changed pixels | >8 | >32 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| chamber | 0.6077 | 1.8665 | 3 | 7 | 123 | 42.64% | 0.60% | 0.02% |
| tunnel | 0.6383 | 3.3799 | 3 | 8 | 130 | 31.73% | 0.87% | 0.28% |
| passage | 1.6092 | 3.6878 | 12 | 20 | 52 | 50.50% | 8.95% | 0.17% |
| far | 0.7909 | 2.1558 | 5 | 9 | 128 | 44.83% | 1.25% | 0.02% |
| passage_floor_closeup | 0.9309 | 2.4737 | 5 | 13 | 100 | 43.02% | 2.53% | 0.05% |
| tunnel_bend | 0.8129 | 2.1599 | 5 | 11 | 55 | 40.96% | 1.80% | 0.07% |
| remote_yaw0 | 4.2933 | 15.9282 | 61 | 91 | 125 | 15.59% | 9.78% | 6.42% |
| remote_yaw90 | 1.5692 | 8.8960 | 2 | 68 | 96 | 6.89% | 4.07% | 2.45% |
| remote_yaw180 | 1.6511 | 8.8623 | 8 | 69 | 102 | 10.33% | 4.90% | 2.69% |
| remote_yaw270 | 1.7465 | 9.3186 | 3 | 68 | 107 | 10.86% | 4.18% | 2.97% |
@@ -0,0 +1,16 @@
# step8_clamped CPU/GPU capture comparison
Each pair is a 1280x800 back-buffer capture from the fixed camera. `mae_rgb` and `rmse_rgb` are channel errors on the 0 to 255 scale; pixel percentiles use the maximum RGB channel error per pixel.
| View | MAE RGB | RMSE RGB | p95 pixel max | p99 pixel max | Max channel | Changed pixels | >8 | >32 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| chamber | 0.6077 | 1.8665 | 3 | 7 | 123 | 42.64% | 0.60% | 0.02% |
| tunnel | 0.6383 | 3.3799 | 3 | 8 | 130 | 31.73% | 0.87% | 0.28% |
| passage | 1.6092 | 3.6878 | 12 | 20 | 52 | 50.50% | 8.95% | 0.17% |
| far | 0.7909 | 2.1558 | 5 | 9 | 128 | 44.83% | 1.25% | 0.02% |
| passage_floor_closeup | 0.9309 | 2.4737 | 5 | 13 | 100 | 43.02% | 2.53% | 0.05% |
| tunnel_bend | 0.8129 | 2.1599 | 5 | 11 | 55 | 40.96% | 1.80% | 0.07% |
| remote_yaw0 | 4.2933 | 15.9282 | 61 | 91 | 125 | 15.59% | 9.78% | 6.42% |
| remote_yaw90 | 1.5692 | 8.8960 | 2 | 68 | 96 | 6.89% | 4.07% | 2.45% |
| remote_yaw180 | 1.6511 | 8.8623 | 8 | 69 | 102 | 10.33% | 4.90% | 2.69% |
| remote_yaw270 | 1.7465 | 9.3186 | 3 | 68 | 107 | 10.86% | 4.18% | 2.97% |
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

@@ -0,0 +1,13 @@
# Step 8 remote yaw 0, isolated LOD render comparison
The viewer generated and fully-resident clip radius 3 for each pair; `--capture-only-lod` filtered drawing only.
| LOD | MAE RGB | RMSE RGB | Changed pixels | Pixels >32 | Max channel |
|---:|---:|---:|---:|---:|---:|
| 2 | 1.3706 | 9.6463 | 3.89% | 1.84% | 125 |
| 3 | 6.6178 | 20.8273 | 15.13% | 9.96% | 124 |
| 4 | 5.0988 | 17.4692 | 11.79% | 8.38% | 87 |
| 5 | 2.6833 | 11.7094 | 8.25% | 4.49% | 87 |
| 6 | 2.8046 | 11.0939 | 8.97% | 5.66% | 69 |
| 7 | 1.0024 | 4.3843 | 10.58% | 0.00% | 28 |
| 8 | 0.2672 | 2.1540 | 1.64% | 0.00% | 21 |
@@ -60,7 +60,9 @@ cbuffer FarConnectorConstants : register(b2)
uint FarConnectorVertexCount;
uint FarConnectorPanelVertexCapacity;
uint FarShapeCount;
uint3 FarConnectorPadding;
uint FarSkirtFaceMask;
float FarSkirtDepthVoxels;
uint FarConnectorPadding;
};
static const int3 CornerOffset[8] = {
@@ -230,6 +232,8 @@ RWStructuredBuffer<FarConnectorRing> FarConnectorRingOutput : register(u7);
RWByteAddressBuffer FarConnectorDrawArgs : register(u4);
RWByteAddressBuffer FarConnectorVertexBytes : register(u5);
RWByteAddressBuffer FarConnectorIndexBytes : register(u6);
RWStructuredBuffer<uint> FarSkirtScratch : register(u0);
ByteAddressBuffer FarSurfaceVertexInput : register(t19);
RWStructuredBuffer<uint> FarFieldEvalCounters : register(u6);
@@ -510,6 +514,7 @@ float FarRockDistanceAt(float3 position,int3 cachedBinCoord,FarFieldSpatialBin c
{
float cave = 100000.0f;
float overlay = -100000.0f;
bool criticalAir=false;
const int3 positionBinCoord=int3(floor(position/max(1.0f,(float)FarSpatialBinSize)));
FarFieldSpatialBin range;
if(all(positionBinCoord==cachedBinCoord))range=cachedRange;
@@ -520,6 +525,25 @@ float FarRockDistanceAt(float3 position,int3 cachedBinCoord,FarFieldSpatialBin c
const VF_FieldPrimitive shape = FarShapes[FarSpatialBinShapeIds[range.offset + item]];
if ((shape.flags & VF_FIELD_PRIMITIVE_CARVE) != 0)
{
// The CPU field forces critical passage air at arbitrary skirt
// probes using the containing integer voxel. Match that decided
// exception here; the ordinary smooth field remains GPU-evaluated.
if((shape.flags&VF_FIELD_PRIMITIVE_CRITICAL)!=0
&&all(position>=float3(shape.minVoxel[0],shape.minVoxel[1],shape.minVoxel[2]))
&&all(position<=float3(shape.maxVoxel[0],shape.maxVoxel[1],shape.maxVoxel[2])))
{
const int3 decidedPosition=int3(position);
float decidedDistance=VF_FieldPrimitiveDistance(shape,
(float)decidedPosition.x,(float)decidedPosition.y,(float)decidedPosition.z);
if((shape.flags&VF_FIELD_PRIMITIVE_HAS_FLOOR)!=0)
{
const float decidedFloor=VF_FieldPrimitiveFloorAt(shape,
(float)decidedPosition.x,(float)decidedPosition.y);
decidedDistance=VF_FieldSmoothMax(decidedDistance,
decidedFloor-(float)decidedPosition.z,shape.floorBlendRadius);
}
criticalAir=criticalAir||decidedDistance<=0.0f;
}
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)
@@ -541,7 +565,174 @@ float FarRockDistanceAt(float3 position,int3 cachedBinCoord,FarFieldSpatialBin c
}
}
const float base = min(position.z - FarStrataLo, FarStrataHi - position.z);
return VF_FieldSmoothMin(base, max(cave, overlay), FarStrataBlend);
float rockDistance=VF_FieldSmoothMin(base,max(cave,overlay),FarStrataBlend);
if(criticalAir)rockDistance=min(rockDistance,-1.0f/16.0f);
return rockDistance;
}
struct FarSkirtVertex
{
float3 position;
float3 normal;
float density;
};
float FarSkirtDensityAtVoxel(float3 position)
{
const int3 binCoord=int3(floor(position/max(1.0f,(float)FarSpatialBinSize)));
const FarFieldSpatialBin range=FarFieldFindBin(position);
const float rockDistance=FarRockDistanceAt(position,binCoord,range);
float density=-rockDistance;
// ShapeWorld clamps arbitrary (off-lattice) density magnitudes near the
// surface to 0.05 voxel. Skirt clipping uses those values for edge-root
// interpolation, so matching only their signs can move a clipped edge.
if(abs(density)<0.05f)density=rockDistance>=0.0f?-0.05f:0.05f;
return density;
}
float FarSkirtDensityAtCm(float3 positionCm)
{
return FarSkirtDensityAtVoxel(positionCm/VoxelSizeCm);
}
FarSkirtVertex FarSkirtInterpolate(FarSkirtVertex a,FarSkirtVertex b,float t)
{
if(t<=1.0e-6f)return a;
if(t>=1.0f-1.0e-6f)return b;
FarSkirtVertex result;
result.position=lerp(a.position,b.position,t);
result.normal=normalize(lerp(a.normal,b.normal,t));
result.density=FarSkirtDensityAtCm(result.position);
return result;
}
void FarSkirtClipTriangle(FarSkirtVertex a,FarSkirtVertex b,FarSkirtVertex c,
out FarSkirtVertex polygon[5],out uint polygonCount)
{
FarSkirtVertex input[3];input[0]=a;input[1]=b;input[2]=c;
polygonCount=0;
[unroll]
for(uint edge=0;edge<3;++edge)
{
FarSkirtVertex start=input[edge];
FarSkirtVertex end=input[(edge+1)%3];
const bool startSolid=start.density<=0.0f;
const bool endSolid=end.density<=0.0f;
if(startSolid!=endSolid)
{
const float denominator=start.density-end.density;
const float t=abs(denominator)>1.0e-8f
?clamp(start.density/denominator,0.0f,1.0f):0.5f;
polygon[polygonCount++]=FarSkirtInterpolate(start,end,t);
}
if(endSolid)polygon[polygonCount++]=end;
}
}
uint FarSkirtSourceTriangleOutputCount(FarSkirtVertex a,FarSkirtVertex b,FarSkirtVertex c)
{
FarSkirtVertex polygon[5];uint polygonCount=0;
FarSkirtClipTriangle(a,b,c,polygon,polygonCount);
if(polygonCount<3)return 0;
uint vertexCount=0;
[loop]
for(uint fan=1;fan+1<polygonCount;++fan)
{
const float3 centerVoxel=(polygon[0].position+polygon[fan].position
+polygon[fan+1].position)/(3.0f*VoxelSizeCm);
if(FarSkirtDensityAtVoxel(centerVoxel)<=0.0f)vertexCount+=6;
}
return vertexCount;
}
uint FarSkirtBoundaryFaceMask(float3 a,float3 b)
{
const float3 tileMin=float3(OriginX,OriginY,OriginZ)*VoxelSizeCm;
const float3 tileMax=(float3(OriginX,OriginY,OriginZ)
+float(CellsPerAxis*StepVoxels))*VoxelSizeCm;
uint mask=0;
if(a.x==tileMin.x&&b.x==tileMin.x)mask|=1u<<0;
if(a.x==tileMax.x&&b.x==tileMax.x)mask|=1u<<1;
if(a.y==tileMin.y&&b.y==tileMin.y)mask|=1u<<2;
if(a.y==tileMax.y&&b.y==tileMax.y)mask|=1u<<3;
if(a.z==tileMin.z&&b.z==tileMin.z)mask|=1u<<4;
if(a.z==tileMax.z&&b.z==tileMax.z)mask|=1u<<5;
return mask;
}
FarSkirtVertex FarSkirtSurfaceVertex(uint vertexId)
{
const uint byteAddress=(OutputBaseVertex+vertexId)*32;
FarSkirtVertex vertex;
vertex.position=asfloat(FarSurfaceVertexInput.Load3(byteAddress));
vertex.normal=asfloat(FarSurfaceVertexInput.Load3(byteAddress+12));
vertex.density=0.0f;
return vertex;
}
FarSkirtVertex FarSkirtExtrudedVertex(FarSkirtVertex source)
{
FarSkirtVertex vertex=source;
vertex.position-=source.normal*(max(1.0f,FarSkirtDepthVoxels)*VoxelSizeCm);
vertex.density=FarSkirtDensityAtCm(vertex.position);
return vertex;
}
uint FarSkirtEdgeOutputCount(uint edgeVertexId)
{
const uint surfaceVertexCount=min(FarConnectorDrawArgs.Load(OutputArgsByteOffset),OutputVertexCapacity);
if(edgeVertexId>=surfaceVertexCount)return 0;
const uint triangleBase=edgeVertexId-(edgeVertexId%3);
const uint edge=edgeVertexId%3;
const FarSkirtVertex a=FarSkirtSurfaceVertex(triangleBase+edge);
const FarSkirtVertex b=FarSkirtSurfaceVertex(triangleBase+(edge+1)%3);
if((FarSkirtBoundaryFaceMask(a.position,b.position)&FarSkirtFaceMask)==0)return 0;
const FarSkirtVertex a2=FarSkirtExtrudedVertex(a);
const FarSkirtVertex b2=FarSkirtExtrudedVertex(b);
return FarSkirtSourceTriangleOutputCount(a,b,b2)
+FarSkirtSourceTriangleOutputCount(a,b2,a2);
}
void FarSkirtStoreVertex(uint outputId,FarSkirtVertex vertex,uint panelBaseVertex)
{
GpuVertex output;
output.position=vertex.position;
output.normal=vertex.normal;
output.uv=vertex.position.xy/VoxelSizeCm;
const uint index=panelBaseVertex+outputId;
const uint byteAddress=index*32;
FarConnectorVertexBytes.Store3(byteAddress,asuint(output.position));
FarConnectorVertexBytes.Store3(byteAddress+12,asuint(output.normal));
FarConnectorVertexBytes.Store2(byteAddress+24,asuint(output.uv));
FarConnectorIndexBytes.Store(index*4,index);
}
void FarSkirtEmitTriangle(FarSkirtVertex a,FarSkirtVertex b,FarSkirtVertex c,
uint panelBaseVertex,uint edgeBase,inout uint edgeCursor)
{
FarSkirtStoreVertex(edgeBase+edgeCursor++,a,panelBaseVertex);
FarSkirtStoreVertex(edgeBase+edgeCursor++,b,panelBaseVertex);
FarSkirtStoreVertex(edgeBase+edgeCursor++,c,panelBaseVertex);
FarSkirtStoreVertex(edgeBase+edgeCursor++,a,panelBaseVertex);
FarSkirtStoreVertex(edgeBase+edgeCursor++,c,panelBaseVertex);
FarSkirtStoreVertex(edgeBase+edgeCursor++,b,panelBaseVertex);
}
void FarSkirtEmitSourceTriangle(FarSkirtVertex a,FarSkirtVertex b,FarSkirtVertex c,
uint panelBaseVertex,uint edgeBase,inout uint edgeCursor)
{
FarSkirtVertex polygon[5];uint polygonCount=0;
FarSkirtClipTriangle(a,b,c,polygon,polygonCount);
if(polygonCount<3)return;
[loop]
for(uint fan=1;fan+1<polygonCount;++fan)
{
const float3 centerVoxel=(polygon[0].position+polygon[fan].position
+polygon[fan+1].position)/(3.0f*VoxelSizeCm);
if(FarSkirtDensityAtVoxel(centerVoxel)<=0.0f)
FarSkirtEmitTriangle(polygon[0],polygon[fan],polygon[fan+1],
panelBaseVertex,edgeBase,edgeCursor);
}
}
float FarConnectorRadius(float3 center, float3 direction, float nominalRadius)
@@ -704,6 +895,7 @@ void BuildFarConnectorRingsCS(uint3 groupId : SV_GroupID,
FarConnectorDrawArgs.Store(OutputArgsByteOffset + 28, panelBaseVertex);
FarConnectorDrawArgs.Store(OutputArgsByteOffset + 32, 0);
FarConnectorDrawArgs.Store(OutputArgsByteOffset + 36, 0);
FarConnectorDrawArgs.Store(OutputArgsByteOffset + 40, 0);
}
}
GroupMemoryBarrierWithGroupSync();
@@ -880,6 +1072,7 @@ void ScanGroupsCS(uint3 dispatchId : SV_DispatchThreadID)
DrawArgs.Store(OutputArgsByteOffset + 28, 0);
DrawArgs.Store(OutputArgsByteOffset + 32, 0);
DrawArgs.Store(OutputArgsByteOffset + 36, 0);
DrawArgs.Store(OutputArgsByteOffset + 40, 0);
}
StructuredBuffer<float> Density : register(t2);
@@ -927,6 +1120,63 @@ void EmitCS(uint3 dispatchId : SV_DispatchThreadID)
}
}
[numthreads(64,1,1)]
void ClassifyFarSkirtsCS(uint3 dispatchId : SV_DispatchThreadID)
{
const uint edgeVertexId=dispatchId.x;
if(edgeVertexId>=OutputVertexCapacity)return;
FarSkirtScratch[edgeVertexId]=FarSkirtEdgeOutputCount(edgeVertexId);
}
[numthreads(1,1,1)]
void ScanFarSkirtsCS(uint3 dispatchId : SV_DispatchThreadID)
{
const uint surfaceVertexCount=min(FarConnectorDrawArgs.Load(OutputArgsByteOffset),
OutputVertexCapacity);
uint totalSkirtVertices=0;
[loop]
for(uint edgeVertexId=0;edgeVertexId<surfaceVertexCount;++edgeVertexId)
{
const uint count=FarSkirtScratch[edgeVertexId];
FarSkirtScratch[edgeVertexId]=totalSkirtVertices;
totalSkirtVertices+=count;
}
const uint panelVertexCount=FarConnectorDrawArgs.Load(OutputArgsByteOffset+20);
if(panelVertexCount+totalSkirtVertices>FarConnectorPanelVertexCapacity)
{
FarConnectorDrawArgs.Store(OutputArgsByteOffset+40,1);
return;
}
FarConnectorDrawArgs.Store(OutputArgsByteOffset+40,0);
FarConnectorDrawArgs.Store(OutputArgsByteOffset+20,panelVertexCount+totalSkirtVertices);
FarConnectorDrawArgs.Store(OutputArgsByteOffset+24,
panelVertexCount+totalSkirtVertices>0?1:0);
}
[numthreads(64,1,1)]
void EmitFarSkirtsCS(uint3 dispatchId : SV_DispatchThreadID)
{
const uint edgeVertexId=dispatchId.x;
if(edgeVertexId>=OutputVertexCapacity||FarSkirtFaceMask==0
||FarConnectorDrawArgs.Load(OutputArgsByteOffset+40)!=0)return;
const uint surfaceVertexCount=min(FarConnectorDrawArgs.Load(OutputArgsByteOffset),
OutputVertexCapacity);
if(edgeVertexId>=surfaceVertexCount)return;
if(FarSkirtEdgeOutputCount(edgeVertexId)==0)return;
const uint triangleBase=edgeVertexId-(edgeVertexId%3);
const uint edge=edgeVertexId%3;
const FarSkirtVertex a=FarSkirtSurfaceVertex(triangleBase+edge);
const FarSkirtVertex b=FarSkirtSurfaceVertex(triangleBase+(edge+1)%3);
if((FarSkirtBoundaryFaceMask(a.position,b.position)&FarSkirtFaceMask)==0)return;
const FarSkirtVertex a2=FarSkirtExtrudedVertex(a);
const FarSkirtVertex b2=FarSkirtExtrudedVertex(b);
const uint panelBaseVertex=FarConnectorDrawArgs.Load(OutputArgsByteOffset+28);
const uint edgeBase=FarConnectorVertexCount+FarSkirtScratch[edgeVertexId];
uint edgeCursor=0;
FarSkirtEmitSourceTriangle(a,b,b2,panelBaseVertex,edgeBase,edgeCursor);
FarSkirtEmitSourceTriangle(a,b2,a2,panelBaseVertex,edgeBase,edgeCursor);
}
// 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; };