diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep15Captures.py b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep15Captures.py index 8b21919..e8607d5 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep15Captures.py +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep15Captures.py @@ -21,6 +21,15 @@ VIEWS = ( "remote_yaw180", "remote_yaw270", ) +ROCK = np.array((96, 86, 68)) +BACKGROUND = np.array((14, 19, 27)) + + +def mask_bounds(mask): + ys, xs = np.nonzero(mask) + if not len(xs): + return "none" + return f"({xs.min()},{ys.min()})-({xs.max()},{ys.max()})" rows = [] for view in VIEWS: @@ -32,6 +41,10 @@ for view in VIEWS: raise RuntimeError(f"dimension mismatch for {view}: {cpu.shape} vs {gpu.shape}") diff = np.abs(cpu - gpu) pixel_max = diff.max(axis=2) + cpu_rock = np.all(cpu == ROCK, axis=2) + gpu_rock = np.all(gpu == ROCK, axis=2) + cpu_to_gpu_background = cpu_rock & np.all(gpu == BACKGROUND, axis=2) + gpu_to_cpu_background = gpu_rock & np.all(cpu == BACKGROUND, axis=2) rows.append( ( view, @@ -41,6 +54,9 @@ for view in VIEWS: float(np.count_nonzero(pixel_max) * 100.0 / pixel_max.size), float(np.count_nonzero(pixel_max > 32) * 100.0 / pixel_max.size), int(pixel_max.max()), + int(np.count_nonzero(cpu_to_gpu_background)), + int(np.count_nonzero(gpu_to_cpu_background)), + mask_bounds(cpu_to_gpu_background), ) ) @@ -49,13 +65,15 @@ lines = [ "", "All ten CPU/GPU pairs were captured after `[InitialClipmapReady]`. The files were compared numerically as RGB arrays and were not opened in an image viewer.", "", - "| View | Size | RGB MAE | RGB RMSE | Changed pixels | Pixels max channel >32 | Max channel |", - "|---|---:|---:|---:|---:|---:|---:|", + "`CPU rock to GPU background` isolates the prior missing-coverage signature. Captures were loaded as RGB arrays only; no full-size image was opened in a viewer.", + "", + "| View | Size | RGB MAE | RGB RMSE | Changed pixels | Pixels max channel >32 | Max channel | CPU rock to GPU background | GPU rock to CPU background | Missing-pixel bounds |", + "|---|---:|---:|---:|---:|---:|---:|---:|---:|---|", ] for row in rows: - view, size, mae, rmse, changed, over32, maximum = row + view, size, mae, rmse, changed, over32, maximum, missing, extra, bounds = row lines.append( - f"| {view} | {size} | {mae:.4f} | {rmse:.4f} | {changed:.4f}% | {over32:.4f}% | {maximum} |" + f"| {view} | {size} | {mae:.4f} | {rmse:.4f} | {changed:.4f}% | {over32:.4f}% | {maximum} | {missing} | {extra} | {bounds} |" ) report = ROOT / f"{PREFIX}_capture_comparison.md" diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep17TileControls.py b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep17TileControls.py new file mode 100644 index 0000000..643720a --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep17TileControls.py @@ -0,0 +1,85 @@ +import argparse +from pathlib import Path + +import numpy as np +from PIL import Image + + +ROOT = Path(__file__).resolve().parent +DEFAULT_CPU_PATH = "step17_tilecontrol_cpu.png" +DEFAULT_CONTROLS = ( + ("GPU indirect draw", "step17_tilecontrol_gpu.png"), + ("GPU direct draw", "step17_directdraw_gpu_direct_draw.png"), + ("GPU readback copy (cm)", "step17_readbackcopy_gpu_readback_copy.png"), + ("GPU readback copy (m)", "step17_metercopy_gpu_meter_copy.png"), + ("CPU mesh in GPU draw loop", "step17_cpumeshcopy_gpu_cpu_mesh_copy.png"), + ("GPU depth test disabled", "step17_tilecontrol_gpu_depth_disabled.png"), + ("GPU skirts disabled", "step17_tilecontrol_gpu_skirts_off.png"), + ("GPU connectors disabled", "step17_tilecontrol_gpu_connectors_off.png"), + ("GPU skirt-depth fix", "step17_r19_fixed_gpu.png"), + ( + "GPU skirts and connectors disabled", + "step17_tilecontrol_gpu_skirts_connectors_off.png", + ), +) + +parser = argparse.ArgumentParser() +parser.add_argument("--cpu", default=DEFAULT_CPU_PATH) +parser.add_argument("--control", action="append", default=[], metavar="LABEL=PNG") +parser.add_argument("--report", default="step17_tilecontrol_comparison.md") +parser.add_argument("--include-existing-controls", action="store_true") +args = parser.parse_args() + +if Path(args.cpu).name != args.cpu or Path(args.report).name != args.report: + raise RuntimeError("CPU image and report must be filenames within this directory") +controls = list(DEFAULT_CONTROLS) if args.include_existing_controls else [] +for item in args.control: + label, separator, filename = item.partition("=") + if not separator or not label or Path(filename).name != filename: + raise RuntimeError(f"invalid --control value: {item!r}") + controls.append((label, filename)) + +CPU_PATH = ROOT / args.cpu + +cpu = np.asarray(Image.open(CPU_PATH).convert("RGB"), dtype=np.int16) +cpu_rock = np.all(cpu == (96, 86, 68), axis=2) +background = np.array((14, 19, 27)) +rows = [] +for label, filename in controls: + path = ROOT / filename + gpu = np.asarray(Image.open(path).convert("RGB"), dtype=np.int16) + if cpu.shape != gpu.shape: + raise RuntimeError(f"dimension mismatch for {label}: {cpu.shape} vs {gpu.shape}") + diff = np.abs(cpu - gpu) + pixel_max = diff.max(axis=2) + missing_cpu_rock = cpu_rock & np.all(gpu == background, axis=2) + rows.append( + ( + label, + f"{gpu.shape[1]}x{gpu.shape[0]}", + float(diff.mean()), + float(np.sqrt(np.square(diff, dtype=np.float64).mean())), + float(np.count_nonzero(pixel_max) * 100.0 / pixel_max.size), + int(np.count_nonzero(missing_cpu_rock)), + int(pixel_max.max()), + ) + ) + +lines = [ + "# Step 17 LOD3 tile control comparison", + "", + "The `(6,2,0,lod3)` target was the only drawn tile, with the full desired clipmap retained. Captures were loaded as RGB arrays and compared numerically; full-size images were not opened in a viewer.", + "", + "`CPU rock to GPU background` counts pixels where the CPU target is rock `(96,86,68)` and the control is background `(14,19,27)`, the prior missing-coverage signature.", + "", + "| Control | Size | RGB MAE | RGB RMSE | Changed pixels | CPU rock to GPU background | Max channel |", + "|---|---:|---:|---:|---:|---:|---:|", +] +for label, size, mae, rmse, changed, missing, maximum in rows: + lines.append( + f"| {label} | {size} | {mae:.4f} | {rmse:.4f} | {changed:.4f}% | {missing} | {maximum} |" + ) + +report = ROOT / args.report +report.write_text("\n".join(lines) + "\n", encoding="utf-8") +print(report.read_text(encoding="utf-8"), end="") diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompileStep14Shader.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompileStep14Shader.ps1 new file mode 100644 index 0000000..f6d8727 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompileStep14Shader.ps1 @@ -0,0 +1,29 @@ +[CmdletBinding()] +param([string]$Entry='ApplyNearFieldDetailCS') + +$ErrorActionPreference='Stop' +$BuildHostProcess=Get-Process -Id $PID +$BuildHostProcess.PriorityClass='BelowNormal' +$BuildHostProcess.ProcessorAffinity=[IntPtr]0xFF00 +$StandaloneRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..')) +$ShaderSource=Join-Path $StandaloneRoot 'Research\OwnerShapeWorld\gpu_first_port\VoxelForgeGpuMesher.hlsl' +$OutputPath=Join-Path $PSScriptRoot ('precompiled\'+$Entry+'.cso') +if(-not(Test-Path -LiteralPath $ShaderSource -PathType Leaf)){throw "Shader source missing: $ShaderSource"} +if($Entry-notin@('ApplyNearFieldDetailCS')){throw "Unsupported Step 14 shader entry: $Entry"} +$FxCRoot='C:\Program Files (x86)\Windows Kits\10\bin' +$FxC=Get-ChildItem -LiteralPath $FxCRoot -Filter fxc.exe -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\x64\\fxc\.exe$' } | + Sort-Object -Property FullName -Descending | Select-Object -First 1 +if(-not$FxC){throw 'fxc.exe was not found.'} +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} + & $FxC.FullName /nologo /T cs_5_0 /E $Entry /O3 /Gis /Ges /Fo $OutputPath $ShaderSource + $BuildExit=$LASTEXITCODE + if($BuildExit-eq-1073741502-and$Attempt-eq0){continue} + if($BuildExit-eq-1073741502){throw '0xC0000142 repeated after its single retry; stopping.'} + if($BuildExit-ne0){throw "fxc failed for $Entry with exit code $BuildExit"} + break +} +Write-Output "STEP14_SHADER_BUILD entry=$Entry output=$OutputPath exit=0" diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h index 3b52a79..0a21685 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h @@ -15,7 +15,16 @@ namespace VoxelForgeGpuFarViewer { inline constexpr std::uint32_t PooledOutputVertexCapacityPerTile=19456; struct CpuVertex { float position[3]{}, normal[3]{}; }; - struct CpuMesh { std::vector vertices; std::vector indices; }; + struct CpuMesh + { + std::vector vertices; + std::vector indices; + std::uint32_t surfaceVertexCount=0; + std::uint32_t connectorPanelVertexCount=0; + std::uint32_t skirtVertexCount=0; + bool sequentialIndices=false; + bool indirectArgsValid=false; + }; struct GpuTimingQueries { ID3D11Query* start=nullptr; diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp index 3bcb3b3..d459a5c 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp @@ -2352,7 +2352,8 @@ namespace } std::vector SelectFarTiles(World& world,const WorldConfiguration& config, - Vec3d camera,bool wholeFar,bool wholeNear) + Vec3d camera,bool wholeFar,bool wholeNear, + bool findThinFeature) { std::vector result; const auto desired=GetFullDesired(world,config,camera); @@ -2386,6 +2387,43 @@ namespace static_cast(perLod[1])); return result; } + if(findThinFeature) + { + std::uint64_t examined=0,eligible=0; + for(const TileKey& key:desired) + { + if(key.lodlastLod)continue; + ++examined; + const auto origin=TileOrigin(key); + const std::int32_t cells=std::max(1,config.coarseTileCells); + const std::int32_t step=std::max(1,(32<( + Clock::now()-exportStart).count(); + if(!input) + { + if(error.find("near analytic/detail region")!=std::string::npos)continue; + throw std::runtime_error("thin-feature search far field export failed: "+error); + } + if(input->protectedThinSampleCount==0)continue; + std::printf("[ThinFeatureTileSelected] key=(%d,%d,%d) lod=%d protected_thin_samples=%llu examined=%llu eligible=%llu\n", + key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod, + static_cast(input->protectedThinSampleCount), + static_cast(examined), + static_cast(eligible)); + result.push_back({key,std::move(*input),exportPreparationMs}); + return result; + } + std::printf("[ThinFeatureSearch] examined=%llu eligible=%llu result=no_protected_thin_samples_found\n", + static_cast(examined), + static_cast(eligible)); + return result; + } for(int32_t lod=std::max(config.fullResolutionLodCount,2);lod<=lastLod;++lod) { std::vector candidates; @@ -2978,6 +3016,7 @@ namespace const std::filesystem::path shaderPath = root / "Research" / "OwnerShapeWorld" / "gpu_first_port" / "VoxelForgeGpuMesher.hlsl"; bool wholeFar=false; bool wholeNear=false; + bool findThinFeature=false; bool profileGpuKernels=false; bool skirtAuditOnly=false; int clipRadiusOverride=-1; @@ -2990,6 +3029,7 @@ namespace 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; + if(std::string_view(argv[i])=="--find-thin-feature")findThinFeature=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())); @@ -3068,7 +3108,8 @@ namespace noiseWarp.maximum, static_cast(noiseWarp.differingBits)); std::vector selected = skirtAuditOnly - ?std::vector{}:SelectFarTiles(world, config, camera,wholeFar,wholeNear); + ?std::vector{}:SelectFarTiles(world, config, camera,wholeFar,wholeNear, + findThinFeature); if(skirtAuditOnly) { if(!config.generateSkirts) @@ -3411,6 +3452,12 @@ namespace VoxelForgeGpuFarViewer auditReadback!=nullptr,auditReadback!=nullptr,outputSlot,true,captureGpuTiming,batchTimingMode); if(auditReadback) { + auditReadback->surfaceVertexCount=static_cast(result.drawArgs[0]); + auditReadback->connectorPanelVertexCount=static_cast( + result.vertices.size()-std::min(result.vertices.size(),result.drawArgs[0])); + auditReadback->skirtVertexCount=static_cast(result.skirtVertices.size()); + auditReadback->sequentialIndices=result.sequentialIndices; + auditReadback->indirectArgsValid=result.indexedIndirectArgsValid; const std::size_t completeGpuVertexCount=result.vertices.size()+result.skirtVertices.size(); auditReadback->vertices.resize(completeGpuVertexCount); for(std::size_t i=0;i