diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFirstSubmitRepro.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFirstSubmitRepro.ps1 new file mode 100644 index 0000000..08d27e6 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFirstSubmitRepro.ps1 @@ -0,0 +1,44 @@ +[CmdletBinding()] +param([string]$OutputName='VoxelForgeGpuFirstSubmitRepro.exe') + +$ErrorActionPreference='Stop' +$HostProcess=Get-Process -Id $PID +$HostProcess.PriorityClass='BelowNormal' +$HostProcess.ProcessorAffinity=[IntPtr]0xFF00 +$StandaloneRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..')) +if([IO.Path]::GetFileName($OutputName)-ne$OutputName-or[IO.Path]::GetExtension($OutputName)-ne'.exe') +{throw 'OutputName must be an executable filename.'} +$BuildDir=Join-Path $StandaloneRoot 'Build\GpuFirstSubmitRepro' +$BinDir=Join-Path $StandaloneRoot 'bin' +$Source=Join-Path $PSScriptRoot 'GpuFirstSubmitRepro.cpp' +$Output=Join-Path $BinDir $OutputName +$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($BinDir)|Out-Null +$Rsp=Join-Path $BuildDir 'compile.rsp' +$Lines=@('/nologo','/std:c++20','/O2','/EHsc','/MP8','/D_CRT_SECURE_NO_WARNINGS', + ('/Fe"{0}"' -f $Output),('"{0}"' -f $Source),'/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib') +[IO.File]::WriteAllLines($Rsp,$Lines,[Text.UTF8Encoding]::new($false)) +Push-Location $BuildDir +try +{ + $Command='call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$Rsp + $Attempt=0 + do + { + & $env:ComSpec /d /s /c $Command + $Code=$LASTEXITCODE + if($Code-eq-1073741502-and$Attempt-eq0) + { + 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 one retry; stopping.'} + if($Code-ne0){throw "GPU first-submit repro build failed with exit code $Code"} + break + }while($true) +} +finally{Pop-Location} +if(-not(Test-Path -LiteralPath $Output -PathType Leaf)){throw "Repro executable missing: $Output"} +Write-Host "Built $Output" diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep9Captures.py b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep9Captures.py new file mode 100644 index 0000000..efce642 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep9Captures.py @@ -0,0 +1,89 @@ +from pathlib import Path + +import numpy as np +from PIL import Image + + +ROOT = Path(__file__).resolve().parent +PAIRS = ( + ("chamber", "step9final_cpu_chamber.png", "step9final_gpu_chamber.png"), + ("tunnel", "step9final_cpu_tunnel.png", "step9final_gpu_tunnel.png"), + ("passage", "step9final_cpu_passage.png", "step9final_gpu_passage.png"), + ("far", "step9final_cpu_far.png", "step9final_gpu_far.png"), + ( + "passage_floor_closeup", + "step9final_cpu_passage_floor_closeup.png", + "step9final_gpu_passage_floor_closeup.png", + ), + ("tunnel_bend", "step9final_cpu_tunnel_bend.png", "step9final_gpu_tunnel_bend.png"), + ("remote_yaw0", "step9final_cpu_remote_yaw0.png", "step9final_gpu_remote_yaw0.png"), + ("remote_yaw90", "step9final_cpu_remote_yaw90.png", "step9final_gpu_remote_yaw90.png"), + ("remote_yaw180", "step9final_cpu_remote_yaw180.png", "step9final_gpu_remote_yaw180.png"), + ("remote_yaw270", "step9final_cpu_remote_yaw270.png", "step9final_gpu_remote_yaw270.png"), + ("control_tile_4_3_-1_lod3", "step9fresh_tile43m1_cpu.png", "step9fresh_tile43m1_gpu.png"), + ( + "direct_gpu_candidate_6_2_0_lod3", + "step9_tileprobe_6_2_0_lod3_cpu.png", + "step9_tileprobe_6_2_0_lod3_gpu.png", + ), + ( + "direct_gpu_vs_forced_cpu_candidate_6_2_0_lod3", + "step9_route_6_2_0_gpu.png", + "step9_route_6_2_0_forced_cpu_gpu.png", + ), + ( + "cpu_reference_vs_forced_cpu_candidate_6_2_0_lod3", + "step9_tileprobe_6_2_0_lod3_cpu.png", + "step9_route_6_2_0_forced_cpu_gpu.png", + ), + ( + "final_candidate_tile_6_2_0_lod3", + "step9final_tile_6_2_0_cpu.png", + "step9final_tile_6_2_0_gpu.png", + ), + ("final_remote_lod3", "step9final_lod3_remote_cpu.png", "step9final_lod3_remote_gpu.png"), + ("final_remote_lod4", "step9final_lod4_remote_cpu.png", "step9final_lod4_remote_gpu.png"), +) + +rows = [] +for name, cpu_name, gpu_name in PAIRS: + cpu_path = ROOT / cpu_name + gpu_path = ROOT / gpu_name + 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 {name}: {cpu.shape} vs {gpu.shape}") + diff = np.abs(cpu - gpu) + pixel_max = diff.max(axis=2) + rows.append( + { + "name": name, + "size": f"{cpu.shape[1]}x{cpu.shape[0]}", + "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 9 CPU/GPU capture comparisons", + "", + "CPU and GPU captures are compared numerically as RGB arrays. The view matrix was taken only after each run logged `[InitialClipmapReady]`; the render-only tile probes retained the full desired target set.", + "", + "`MAE` and `RMSE` use the 0-255 RGB channel scale. `Changed` and `>32` are the percent of pixels whose maximum channel difference is nonzero or above 32.", + "", + "| Pair | Size | MAE RGB | RMSE RGB | Changed | >32 | Max channel |", + "|---|---:|---:|---:|---:|---:|---:|", +] +for row in rows: + lines.append( + f"| {row['name']} | {row['size']} | {row['mae']:.4f} | {row['rmse']:.4f} | " + f"{row['changed']:.2f}% | {row['over32']:.2f}% | {row['max']} |" + ) + +report_path = ROOT / "step9final_capture_comparison.md" +report_path.write_text("\n".join(lines) + "\n", encoding="utf-8") +print("\n".join(lines)) +print(f"\nWrote {report_path}") diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFirstSubmitRepro.cpp b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFirstSubmitRepro.cpp new file mode 100644 index 0000000..5ac1507 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/GpuFirstSubmitRepro.cpp @@ -0,0 +1,339 @@ +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; +using Clock = std::chrono::steady_clock; + +namespace +{ + constexpr UINT PoolSlots = 1438; + constexpr UINT SlotsPerChunk = 128; + constexpr UINT MaxVerticesPerTile = 19456; + constexpr UINT PanelVerticesPerTile = 32768; + constexpr UINT VertexStride = 32; + + void Check(HRESULT Result, const char* Operation) + { + if (FAILED(Result)) + { + char Message[256]{}; + std::snprintf(Message, sizeof(Message), "%s failed: hr=0x%08lx", Operation, + static_cast(Result)); + throw std::runtime_error(Message); + } + } + + double Milliseconds(Clock::time_point Start) + { + return std::chrono::duration(Clock::now() - Start).count(); + } + + struct FPoolChunk + { + ComPtr Vertices, Indices, Args, PanelVertices, PanelIndices; + ComPtr VertexUav, IndexUav, ArgsUav, PanelVertexUav, PanelIndexUav; + ComPtr VertexSrv; + }; + + LRESULT CALLBACK ProbeWindowProc(HWND Window, UINT Message, WPARAM WParam, LPARAM LParam) + { + return DefWindowProcW(Window, Message, WParam, LParam); + } + + HWND CreateProbeWindow(HINSTANCE Instance) + { + WNDCLASSEXW Class{}; + Class.cbSize = sizeof(Class); + Class.lpfnWndProc = ProbeWindowProc; + Class.hInstance = Instance; + Class.lpszClassName = L"VoxelForgeGpuFirstSubmitRepro"; + if (!RegisterClassExW(&Class) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) + throw std::runtime_error("RegisterClassExW failed"); + HWND Window = CreateWindowExW(0, Class.lpszClassName, Class.lpszClassName, + WS_OVERLAPPEDWINDOW, 0, 0, 640, 480, nullptr, nullptr, Instance, nullptr); + if (!Window) throw std::runtime_error("CreateWindowExW failed"); + return Window; + } + + ComPtr CreateRawBuffer(ID3D11Device* Device, UINT ByteWidth, + UINT BindFlags, UINT ExtraMiscFlags = 0) + { + D3D11_BUFFER_DESC Desc{}; + Desc.ByteWidth = ByteWidth; + Desc.Usage = D3D11_USAGE_DEFAULT; + Desc.BindFlags = BindFlags; + Desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS | ExtraMiscFlags; + ComPtr Buffer; + Check(Device->CreateBuffer(&Desc, nullptr, &Buffer), "CreateBuffer(pool)"); + return Buffer; + } + + ComPtr CreateRawUav(ID3D11Device* Device, 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(pool)"); + return View; + } + + ComPtr CreateRawSrv(ID3D11Device* Device, 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 View; + Check(Device->CreateShaderResourceView(Buffer, &Desc, &View), "CreateShaderResourceView(pool)"); + return View; + } + + std::vector ReserveViewerPools(ID3D11Device* Device) + { + std::vector Chunks; + const UINT ChunkCount = (PoolSlots + SlotsPerChunk - 1) / SlotsPerChunk; + Chunks.reserve(ChunkCount); + for (UINT ChunkIndex = 0; ChunkIndex < ChunkCount; ++ChunkIndex) + { + const UINT Slots = std::min(SlotsPerChunk, PoolSlots - ChunkIndex * SlotsPerChunk); + const UINT VertexBytes = Slots * MaxVerticesPerTile * VertexStride; + const UINT IndexBytes = Slots * MaxVerticesPerTile * sizeof(UINT); + const UINT ArgsBytes = Slots * 11u * sizeof(UINT); + const UINT PanelVertexBytes = Slots * PanelVerticesPerTile * VertexStride; + const UINT PanelIndexBytes = Slots * PanelVerticesPerTile * sizeof(UINT); + FPoolChunk Chunk; + Chunk.Vertices = CreateRawBuffer(Device, VertexBytes, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_SHADER_RESOURCE); + Chunk.Indices = CreateRawBuffer(Device, IndexBytes, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_INDEX_BUFFER); + Chunk.Args = CreateRawBuffer(Device, ArgsBytes, D3D11_BIND_UNORDERED_ACCESS, + D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS); + Chunk.PanelVertices = CreateRawBuffer(Device, PanelVertexBytes, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_VERTEX_BUFFER); + Chunk.PanelIndices = CreateRawBuffer(Device, PanelIndexBytes, + D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_INDEX_BUFFER); + Chunk.VertexUav = CreateRawUav(Device, Chunk.Vertices.Get(), VertexBytes / 4); + Chunk.VertexSrv = CreateRawSrv(Device, Chunk.Vertices.Get(), VertexBytes / 4); + Chunk.IndexUav = CreateRawUav(Device, Chunk.Indices.Get(), IndexBytes / 4); + Chunk.ArgsUav = CreateRawUav(Device, Chunk.Args.Get(), ArgsBytes / 4); + Chunk.PanelVertexUav = CreateRawUav(Device, Chunk.PanelVertices.Get(), PanelVertexBytes / 4); + Chunk.PanelIndexUav = CreateRawUav(Device, Chunk.PanelIndices.Get(), PanelIndexBytes / 4); + Chunks.push_back(std::move(Chunk)); + } + return Chunks; + } + + void TouchPoolOnce(ID3D11Device* Device, ID3D11DeviceContext* Context, + const std::vector& Chunks) + { + static constexpr char ShaderText[] = + "RWByteAddressBuffer A:register(u0); RWByteAddressBuffer B:register(u1);" + "RWByteAddressBuffer C:register(u2); RWByteAddressBuffer D:register(u3);" + "RWByteAddressBuffer E:register(u4);" + "[numthreads(1,1,1)] void CS(uint3 id:SV_DispatchThreadID){" + "A.Store(0,1);B.Store(0,2);C.Store(0,3);D.Store(0,4);E.Store(0,5);}"; + ComPtr Bytecode, Errors; + Check(D3DCompile(ShaderText, sizeof(ShaderText) - 1, nullptr, nullptr, nullptr, + "CS", "cs_5_0", 0, 0, &Bytecode, &Errors), "D3DCompile(trivial shader)"); + ComPtr Shader; + Check(Device->CreateComputeShader(Bytecode->GetBufferPointer(), Bytecode->GetBufferSize(), + nullptr, &Shader), "CreateComputeShader(trivial shader)"); + Context->CSSetShader(Shader.Get(), nullptr, 0); + for (const FPoolChunk& Chunk : Chunks) + { + ID3D11UnorderedAccessView* Views[] = { + Chunk.VertexUav.Get(), Chunk.IndexUav.Get(), Chunk.ArgsUav.Get(), + Chunk.PanelVertexUav.Get(), Chunk.PanelIndexUav.Get() }; + Context->CSSetUnorderedAccessViews(0, 5, Views, nullptr); + Context->Dispatch(1, 1, 1); + } + ID3D11UnorderedAccessView* NullViews[5]{}; + Context->CSSetUnorderedAccessViews(0, 5, NullViews, nullptr); + Context->CSSetShader(nullptr, nullptr, 0); + } + + void TouchSmallBuffer(ID3D11Device* Device, ID3D11DeviceContext* Context, + ID3D11UnorderedAccessView* Uav) + { + static constexpr char ShaderText[] = + "RWByteAddressBuffer A:register(u0);" + "[numthreads(1,1,1)] void CS(uint3 id:SV_DispatchThreadID){A.Store(0,1);}"; + ComPtr Bytecode, Errors; + Check(D3DCompile(ShaderText, sizeof(ShaderText) - 1, nullptr, nullptr, nullptr, + "CS", "cs_5_0", 0, 0, &Bytecode, &Errors), "D3DCompile(small control shader)"); + ComPtr Shader; + Check(Device->CreateComputeShader(Bytecode->GetBufferPointer(), Bytecode->GetBufferSize(), + nullptr, &Shader), "CreateComputeShader(small control shader)"); + Context->CSSetShader(Shader.Get(), nullptr, 0); + Context->CSSetUnorderedAccessViews(0, 1, &Uav, nullptr); + Context->Dispatch(1, 1, 1); + ID3D11UnorderedAccessView* NullUav = nullptr; + Context->CSSetUnorderedAccessViews(0, 1, &NullUav, nullptr); + Context->CSSetShader(nullptr, nullptr, 0); + } + + double WaitForGpu(ID3D11Device* Device, ID3D11DeviceContext* Context) + { + D3D11_QUERY_DESC Desc{D3D11_QUERY_EVENT, 0}; + ComPtr Query; + Check(Device->CreateQuery(&Desc, &Query), "CreateQuery(event)"); + Context->End(Query.Get()); + Context->Flush(); + const auto Start = Clock::now(); + for (;;) + { + const HRESULT Result = Context->GetData(Query.Get(), nullptr, 0, + D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (Result == S_OK) break; + if (FAILED(Result)) Check(Result, "GetData(event)"); + if (Milliseconds(Start) > 30000.0) throw std::runtime_error("GPU event query timed out"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return Milliseconds(Start); + } + + bool QueryLocalMemory(ID3D11Device* Device, UINT64& UsageBytes, UINT64& BudgetBytes) + { + ComPtr DxgiDevice; + if (FAILED(Device->QueryInterface(IID_PPV_ARGS(&DxgiDevice)))) return false; + ComPtr Adapter; + if (FAILED(DxgiDevice->GetAdapter(&Adapter))) return false; + ComPtr Adapter3; + if (FAILED(Adapter.As(&Adapter3))) return false; + DXGI_QUERY_VIDEO_MEMORY_INFO Info{}; + if (FAILED(Adapter3->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &Info))) return false; + UsageBytes = Info.CurrentUsage; + BudgetBytes = Info.Budget; + return true; + } + + void Run(std::wstring_view Mode, bool SingleThreadedDevice) + { + HWND Window = CreateProbeWindow(GetModuleHandleW(nullptr)); + DXGI_SWAP_CHAIN_DESC SwapDesc{}; + SwapDesc.BufferCount = 1; + SwapDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + SwapDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + SwapDesc.OutputWindow = Window; + SwapDesc.SampleDesc.Count = 1; + SwapDesc.Windowed = TRUE; + SwapDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + const UINT DeviceFlags = SingleThreadedDevice ? D3D11_CREATE_DEVICE_SINGLETHREADED : 0; + ComPtr SwapChain; + ComPtr Device; + ComPtr Context; + D3D_FEATURE_LEVEL Feature{}; + const auto DeviceStart = Clock::now(); + Check(D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, + DeviceFlags, nullptr, 0, D3D11_SDK_VERSION, &SwapDesc, &SwapChain, &Device, + &Feature, &Context), "D3D11CreateDeviceAndSwapChain"); + const double DeviceMs = Milliseconds(DeviceStart); + + ComPtr BackBuffer; + Check(SwapChain->GetBuffer(0, IID_PPV_ARGS(&BackBuffer)), "GetBuffer(back buffer)"); + ComPtr RenderTarget; + Check(Device->CreateRenderTargetView(BackBuffer.Get(), nullptr, &RenderTarget), + "CreateRenderTargetView"); + + UINT64 UsageBefore = 0, Budget = 0; + const bool HasMemoryInfo = QueryLocalMemory(Device.Get(), UsageBefore, Budget); + std::vector Pool; + double PoolAllocMs = 0.0; + if (Mode == L"alloc" || Mode == L"dispatch") + { + const auto PoolStart = Clock::now(); + Pool = ReserveViewerPools(Device.Get()); + PoolAllocMs = Milliseconds(PoolStart); + } + double DispatchCpuMs = 0.0, QueryWaitMs = 0.0; + if (Mode == L"dispatch") + { + const auto DispatchStart = Clock::now(); + TouchPoolOnce(Device.Get(), Context.Get(), Pool); + DispatchCpuMs = Milliseconds(DispatchStart); + QueryWaitMs = WaitForGpu(Device.Get(), Context.Get()); + } + else if (Mode == L"small-dispatch") + { + D3D11_BUFFER_DESC Desc{}; + Desc.ByteWidth = 4096; + Desc.Usage = D3D11_USAGE_DEFAULT; + Desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS; + Desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS; + ComPtr Buffer; + Check(Device->CreateBuffer(&Desc, nullptr, &Buffer), "CreateBuffer(small control)"); + auto Uav = CreateRawUav(Device.Get(), Buffer.Get(), Desc.ByteWidth / 4); + const auto DispatchStart = Clock::now(); + TouchSmallBuffer(Device.Get(), Context.Get(), Uav.Get()); + DispatchCpuMs = Milliseconds(DispatchStart); + QueryWaitMs = WaitForGpu(Device.Get(), Context.Get()); + } + + const float Clear[4] = {0.05f, 0.07f, 0.10f, 1.0f}; + Context->OMSetRenderTargets(1, RenderTarget.GetAddressOf(), nullptr); + Context->ClearRenderTargetView(RenderTarget.Get(), Clear); + const UINT64 UsageAfter = [&]() + { + UINT64 Usage = 0, LocalBudget = 0; + QueryLocalMemory(Device.Get(), Usage, LocalBudget); + return Usage; + }(); + const auto PresentStart = Clock::now(); + const HRESULT PresentResult = SwapChain->Present(0, DXGI_PRESENT_DO_NOT_WAIT); + const double PresentMs = Milliseconds(PresentStart); + std::printf("[FirstSubmitRepro] mode=%.*ls device_flags=%s feature_level=0x%04x device_ms=%.3f pool_slots=%u pool_chunks=%zu pool_alloc_ms=%.3f dispatch_cpu_ms=%.3f query_wait_ms=%.3f present_ms=%.3f present_hr=0x%08lx local_mb_before=%.1f local_mb_after=%.1f local_budget_mb=%.1f\n", + static_cast(Mode.size()), Mode.data(), SingleThreadedDevice ? "singlethreaded" : "none", + static_cast(Feature), DeviceMs, Pool.empty() ? 0u : PoolSlots, Pool.size(), PoolAllocMs, + DispatchCpuMs, QueryWaitMs, PresentMs, static_cast(PresentResult), + HasMemoryInfo ? UsageBefore / 1048576.0 : -1.0, + HasMemoryInfo ? UsageAfter / 1048576.0 : -1.0, + HasMemoryInfo ? Budget / 1048576.0 : -1.0); + std::fflush(stdout); + DestroyWindow(Window); + if (FAILED(PresentResult) && PresentResult != DXGI_ERROR_WAS_STILL_DRAWING) + Check(PresentResult, "Present"); + } +} + +int wmain(int Argc, wchar_t** Argv) +{ + try + { + std::wstring_view Mode = L"alloc"; + bool SingleThreadedDevice = false; + for (int Index = 1; Index < Argc; ++Index) + { + const std::wstring_view Arg = Argv[Index]; + if (Arg.starts_with(L"--mode=")) Mode = Arg.substr(7); + else if (Arg == L"--singlethreaded") SingleThreadedDevice = true; + } + if (Mode != L"none" && Mode != L"alloc" && Mode != L"dispatch" && Mode != L"small-dispatch") + throw std::runtime_error("mode must be none, alloc, dispatch, or small-dispatch"); + Run(Mode, SingleThreadedDevice); + return 0; + } + catch (const std::exception& Error) + { + std::fprintf(stderr, "[FirstSubmitReproError] %s\n", Error.what()); + return 1; + } +} diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/RunStep9NormalWindowInterleaved.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/RunStep9NormalWindowInterleaved.ps1 new file mode 100644 index 0000000..d5a1601 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/RunStep9NormalWindowInterleaved.ps1 @@ -0,0 +1,60 @@ +[CmdletBinding()] +param([string]$ExecutableName='VoxelForgeGpuFieldStep9Measured.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='step9visible_cpu_A_fresh'}, + @{Mode='gpu';Name='step9visible_gpu_A_fresh'}, + @{Mode='gpu';Name='step9visible_gpu_B_warmcache'}, + @{Mode='cpu';Name='step9visible_cpu_B_warmcache'}, + @{Mode='cpu';Name='step9visible_cpu_C_warmcache'}, + @{Mode='gpu';Name='step9visible_gpu_C_warmcache'} +) +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 "NORMAL_WINDOW_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 Normal -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 50 + } + $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)"} + $Present=[regex]::Match($LogText,'(?m)^\[WholeViewPresented\] at_ms=([0-9.]+) present_ms=([0-9.]+) sync=\d+ hr=0x([0-9A-Fa-f]{8})') + if(-not$Present.Success){throw "Visible present marker missing in $($Run.Name)"} + $HResult=$Present.Groups[3].Value.ToUpperInvariant() + Write-Output "RUN_EXIT mode=$($Run.Mode) log=$($Run.Name) pid=$($Child.Id) priority=BelowNormal affinity=0xFF00 peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) exit=$($Child.ExitCode) at_ms=$($Present.Groups[1].Value) present_ms=$($Present.Groups[2].Value) present_hr=0x$HResult" + if($Child.ExitCode-ne0){throw "Viewer returned $($Child.ExitCode): $($Run.Name)"} + break + } +} diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/STEP9_REPORT.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/STEP9_REPORT.md new file mode 100644 index 0000000..6b43bc1 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/STEP9_REPORT.md @@ -0,0 +1,64 @@ +# GPU field port: step 9 + +Checkpoint: `f2de8fe` on `standalone/world`. The measured viewer and the far agreement runner built successfully with `/MP8`. No commit, push, or stash was made. The CPU field, layout, and mesh reference code are unchanged. + +## Revalidation + +Fresh `--stream-benchmark --shape-world --force-generate --stream-hash --clip-radius=3` runs at one and eight workers both generated all 2,226 tiles and passed walkability at **5,642/5,642**. Their full clipmap hashes are identical: + +| Workers | Headless CPU clipmap generation | +|---:|---:| +| 1 | 4.258 s | +| 8 | 0.793 s | + +Density `0xdb2991c73215af4c`, mesh `0x8236c5edd7fc8e4f`, payload `0x4c48fa75a543d58e`, topology `0xbea22e76b34a4dfc`. These also match the saved step 8 reference. + +The rebuilt far audit covered **1,150 eligible tiles** at the remote camera and skipped 116 near-transition tiles. CPU and WARP each had **zero sign/topology differences** from the RX 7900 path. It audited 145,494 protected exceptions and 88 thin-feature cells with zero differences; repeated GPU outputs were bit-identical. The viewer's GPU-mesh survival audit passed: 7,200/7,200 boundary surfaces, 10,976 connectors, 178,351 connector samples, and 710,047 rays, with no missing samples, holes, or unsampled connectors. + +Logs: `step9measured_headless_cpu_w1.stdout.log`, `step9measured_headless_cpu_w8.stdout.log`, `step9measured_whole_far_remote.stdout.log`, and `step9measured_gpu_far_survival.stdout.log`. + +## Remote LOD3/4 band + +The one-tile render probe uses the full **2,226-tile target set** and filters only drawing. The `(4,3,-1,lod3)` control differs by just 0.35% of pixels, with a maximum channel difference of 14, so that tile did not explain the band. + +The controlled `(6,2,0,lod3)` direct-GPU render did: against the CPU reference it differs on 1.46% of pixels (RGB MAE 0.9228, max channel 82), with the missing foreground on the CPU side and no GPU-only foreground. Forcing that same tile through the CPU mesh path makes it pixel-identical to the CPU reference. The inspected draw arguments were valid: face mask `0x24`, output slot 518, 2,040 surface indices, 192 skirt-panel indices, expected start index 196,608, and no overflow. This proves a **coverage loss in the direct GPU LOD3 route**; it does not identify the internal shader/emitter cause. + +The viewer now routes all remote LOD3 and LOD4 tiles through CPU meshing: 208 LOD3 and 144 LOD4 tiles in the default clipmap. With that route, the isolated remote LOD3 and LOD4 captures and the `(6,2,0,lod3)` tile control are pixel-identical to CPU. The other LODs remain eligible for GPU work. + +All ten final CPU/GPU view pairs were recaptured after `[InitialClipmapReady]`. The six fixed views remain close to CPU (RGB MAE 0.6076–1.6092). The four full remote yaw views have MAE 0.1994, 0.4083, 0.0484, and 0.2355 for yaw 0, 90, 180, and 270 respectively. Small image differences remain outside the isolated LOD3/4 checks. Detailed numerical comparisons, including the tile controls, are in [step9final_capture_comparison.md](step9final_capture_comparison.md); the measurements are reproducible with [CompareStep9Captures.py](CompareStep9Captures.py). + +## First GPU submission + +The earlier no-hold viewer controls in [step8_report.md](step8_report.md) measured a 2.36–2.59 s first GPU-backed `Present`; those calls returned `DXGI_STATUS_OCCLUDED`. The new standalone control in `GpuFirstSubmitRepro.cpp` allocates the raw-buffer/UAV pools, optionally touches their resources with a trivial dispatch, and varies `D3D11_CREATE_DEVICE_SINGLETHREADED`. + +The repro did **not** reproduce a multi-second delay. Pool allocation took about 4.0 ms and `Present` took 0.87–0.96 ms. Touching each of the five pool resources per chunk moved sampled local usage to 2,590 MB; its event wait was 83 ms (73 ms with the single-threaded flag), and `Present` was still about 0.9–1.1 ms. This rules out pool allocation, this limited first-touch dispatch, and the tested device flag as a standalone explanation. It does not isolate what the full field/mesh workload or viewer queue pump is doing. + +In the measured viewer runs, warm GPU queue envelopes were about 3.59 s, while timestamped GPU work was 0.66–0.71 s. The viewer's pump CPU maximum was about 2.56–2.58 s, and estimated idle/poll time was 2.89–2.94 s. That locates a large part of the delay in the viewer's GPU pumping/submission path, but does not identify an internal driver operation. The front-buffer hold hides the early wait from the first CPU-ready frame; it does not shorten time to the completed GPU view. + +## Whole-view race + +Headless generation has a CPU measurement, not an equivalent end-to-end GPU mode. For the audited far subset, CPU `generateTile` summed to 2.090 s, while RX 7900 field plus meshing timestamps summed to 0.125 s (field dispatch alone: 0.014 s). That is a real far-tile compute advantage, but it excludes the rest of the clipmap, CPU setup, submission, and presentation. + +The interleaved process-start race used new processes for every run. The first sequence was CPU A, GPU A, GPU B, CPU B, CPU C, GPU C: + +| Run | Process start to full clipmap readiness and first following `Present` return | +|---|---:| +| CPU A, first launch | 1.516 s | +| GPU A, first launch | 5.146 s | +| GPU B, later launch | 4.704 s | +| CPU B, later launch | 1.438 s | +| CPU C, later launch | 1.437 s | +| GPU C, later launch | 4.702 s | + +The CPU median is **1.438 s**; the GPU median is **4.704 s**, about **3.27x slower**. A second normal-window launch sequence also stayed slower: CPU was 1.409–1.416 s and GPU was 4.022–4.262 s. Those six calls also returned `DXGI_STATUS_OCCLUDED`. Therefore these automated runs measure full clipmap readiness plus the subsequent `Present` return, but they do not prove a physical on-screen scanout. Their CPU results are consistent with the previously observed ~1.49 s viewer result; the GPU path is still several seconds behind on the readiness clock. The GPU accumulated 0.64–0.79 s of timestamped dispatch work per viewer run; CPU setup, input preparation, uploads, and rendering also remain on the path, with some stages overlapping. + +There is no current successful movement-crossing latency sample. The final-build crossing probe completed the initial clipmap, then logged `[GpuFarPoolError] no free far-output slot for queued tile` after the target set moved. It disabled the GPU path and never emitted `[FlightBenchmarkComplete]`; the run was stopped. The earlier 22.931 ms nearest-LOD0 crossing measurement in step 6 predates this build and is not a current result. The dynamic far-output pool needs a reuse/overlap fix before claiming a GPU movement-readiness benefit. + +## Limits and artifacts + +- Direct GPU rendering still has an unresolved LOD3/4 coverage bug; the CPU fallback costs GPU coverage at those levels. +- The first-submit repro narrows the stall but does not identify the driver's internal work. +- Automated `Present` calls returned `DXGI_STATUS_OCCLUDED`, including the normal-window attempt, so the current process race is not a verified on-screen timing. +- Capture runs peaked at about 3.53 GB private memory. In the interleaved timing runs, private memory peaked around 3.34 GB and sampled DXGI local usage at 2.404 GB, a conservative sum of about 5.75 GB. No run exceeded the 8 GB limit or hit `0xC0000142`; started viewers were closed. + +Primary artifacts: `step9measured_*` logs, `step9firstsubmit_*` logs, `step9final_*` capture pairs, [CompareStep9Captures.py](CompareStep9Captures.py), and [RunStep9NormalWindowInterleaved.ps1](RunStep9NormalWindowInterleaved.ps1). No capture was opened in an image viewer; image comparisons were numerical. diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_capture_comparison.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_capture_comparison.md new file mode 100644 index 0000000..824527f --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_capture_comparison.md @@ -0,0 +1,25 @@ +# Step 9 CPU/GPU capture comparisons + +CPU and GPU captures are compared numerically as RGB arrays. The view matrix was taken only after each run logged `[InitialClipmapReady]`; the render-only tile probes retained the full desired target set. + +`MAE` and `RMSE` use the 0-255 RGB channel scale. `Changed` and `>32` are the percent of pixels whose maximum channel difference is nonzero or above 32. + +| Pair | Size | MAE RGB | RMSE RGB | Changed | >32 | Max channel | +|---|---:|---:|---:|---:|---:|---:| +| chamber | 1280x800 | 0.6076 | 1.8655 | 42.64% | 0.02% | 124 | +| tunnel | 1280x800 | 0.6383 | 3.3799 | 31.73% | 0.28% | 130 | +| passage | 1280x800 | 1.6092 | 3.6878 | 50.50% | 0.17% | 52 | +| far | 1280x800 | 0.7909 | 2.1558 | 44.83% | 0.02% | 128 | +| passage_floor_closeup | 1280x800 | 0.9314 | 2.4800 | 43.02% | 0.05% | 100 | +| tunnel_bend | 1280x800 | 0.8129 | 2.1599 | 40.96% | 0.07% | 55 | +| remote_yaw0 | 1280x800 | 0.1994 | 2.6632 | 4.25% | 0.24% | 125 | +| remote_yaw90 | 1280x800 | 0.4083 | 4.3552 | 2.24% | 0.69% | 94 | +| remote_yaw180 | 1280x800 | 0.0484 | 1.3206 | 2.98% | 0.03% | 102 | +| remote_yaw270 | 1280x800 | 0.2355 | 3.1104 | 5.56% | 0.41% | 107 | +| control_tile_4_3_-1_lod3 | 1280x800 | 0.0311 | 0.6024 | 0.35% | 0.00% | 14 | +| direct_gpu_candidate_6_2_0_lod3 | 1280x800 | 0.9228 | 7.9137 | 1.46% | 1.46% | 82 | +| direct_gpu_vs_forced_cpu_candidate_6_2_0_lod3 | 1280x800 | 0.9228 | 7.9137 | 1.46% | 1.46% | 82 | +| cpu_reference_vs_forced_cpu_candidate_6_2_0_lod3 | 1280x800 | 0.0000 | 0.0000 | 0.00% | 0.00% | 0 | +| final_candidate_tile_6_2_0_lod3 | 1280x800 | 0.0000 | 0.0000 | 0.00% | 0.00% | 0 | +| final_remote_lod3 | 1280x800 | 0.0000 | 0.0000 | 0.00% | 0.00% | 0 | +| final_remote_lod4 | 1280x800 | 0.0000 | 0.0000 | 0.00% | 0.00% | 0 | diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw0.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw0.png new file mode 100644 index 0000000..ea00fb6 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw0.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw180.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw180.png new file mode 100644 index 0000000..84d0a2a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw180.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw270.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw270.png new file mode 100644 index 0000000..7c705f3 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw270.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw90.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw90.png new file mode 100644 index 0000000..b793ef6 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_cpu_remote_yaw90.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw0.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw0.png new file mode 100644 index 0000000..8961384 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw0.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw180.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw180.png new file mode 100644 index 0000000..8e19971 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw180.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw270.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw270.png new file mode 100644 index 0000000..340154a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw270.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw90.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw90.png new file mode 100644 index 0000000..cfcf7be Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_gpu_remote_yaw90.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod3_remote_cpu.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod3_remote_cpu.png new file mode 100644 index 0000000..2a46e7e Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod3_remote_cpu.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod3_remote_gpu.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod3_remote_gpu.png new file mode 100644 index 0000000..2a46e7e Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod3_remote_gpu.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod4_remote_cpu.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod4_remote_cpu.png new file mode 100644 index 0000000..4be052e Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod4_remote_cpu.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod4_remote_gpu.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod4_remote_gpu.png new file mode 100644 index 0000000..4be052e Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_lod4_remote_gpu.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_remote_cpu.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_remote_cpu.png new file mode 100644 index 0000000..ea00fb6 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_remote_cpu.png differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_remote_gpu.png b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_remote_gpu.png new file mode 100644 index 0000000..8961384 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step9final_remote_gpu.png differ diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 4f49cc5..7281ddc 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -1323,6 +1323,7 @@ namespace ID3D11Buffer* VertexBuffer=nullptr; ID3D11Buffer* IndexBuffer=nullptr; UINT IndexCount=0; + UINT DrawOnlyIndexStart=0,DrawOnlyIndexCount=0; uint64 VertexCount=0; }; struct FCrossingWork @@ -1534,6 +1535,7 @@ namespace bool bAutoCaptureWritten=false; bool bFullDetailReported=false; bool bInitialClipmapReported=false; + bool bWholeViewPresented=false; bool bProfileAttribution=false; bool bProfileDetailBand=false; bool bCompareEvaluators=false; @@ -1567,6 +1569,8 @@ namespace int32 CaptureOnlyLod=-1; bool bCaptureOnlyTile=false; int32 CaptureOnlyTileX=0,CaptureOnlyTileY=0,CaptureOnlyTileZ=0,CaptureOnlyTileLod=0; + bool bDrawOnlyTile=false; + int32 DrawOnlyTileX=0,DrawOnlyTileY=0,DrawOnlyTileZ=0,DrawOnlyTileLod=0; int32 CaptureOutputSlot=-1; int32 Width=1280,Height=800; float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f; @@ -1683,6 +1687,12 @@ namespace V3 Forward() const {return Normalize({std::sin(Yaw)*std::cos(Pitch),std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)});} + bool IsDrawOnlyTile(const FVoxelTileKey& Key) const + { + return !bDrawOnlyTile||(Key.Level==DrawOnlyTileLod&&Key.Coord.X==DrawOnlyTileX + &&Key.Coord.Y==DrawOnlyTileY&&Key.Coord.Z==DrawOnlyTileZ); + } + FIntVector CameraChunk() const {return StandaloneWorldMesher::CameraChunk(FVector(Position.X*4.0,Position.Y*4.0,Position.Z*4.0));} @@ -2104,7 +2114,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::chrono::duration(std::chrono::steady_clock::now()-ColdProcessStart).count()); const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels); - bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false; + bFullDetailReported=false;bInitialClipmapReported=false;bWholeViewPresented=false;bAutoCaptureWritten=false; if(bProfileAttribution) { VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Attribution,128); @@ -2174,6 +2184,20 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } DesiredTileSet.clear();DesiredTileSet.reserve(DesiredTiles.size()*2+1); for(const FVoxelTileKey& Key:DesiredTiles)DesiredTileSet.insert(Key); + if(bDrawOnlyTile) + { + const FVoxelTileKey DrawKey(FIntVector(DrawOnlyTileX,DrawOnlyTileY,DrawOnlyTileZ), + DrawOnlyTileLod); + if(!DesiredTileSet.contains(DrawKey)) + { + Error="the requested draw-only tile is not in the full desired clipmap"; + std::fprintf(stderr,"World load failed: %s\n",Error.c_str()); + PostMessageA(Window,WM_CLOSE,0,0); + return; + } + std::printf("[DrawOnlyTile] key=(%d,%d,%d,lod%d) desired_targets=%zu filter=draw_only\n", + DrawOnlyTileX,DrawOnlyTileY,DrawOnlyTileZ,DrawOnlyTileLod,DesiredTiles.size()); + } for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0; for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level]; const int32 StreamWorkers=bShapePipelineActive?(WorkerCount>=8?2:1):WorkerCount; @@ -2434,10 +2458,21 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::vector CpuTiles;CpuTiles.reserve(Keys.size()); std::vector Eligible;Eligible.reserve(Keys.size()); uint64 SkirtTransitionFallbackTiles=0; + uint64 RemoteLod3CpuFallbackTiles=0,RemoteLod4CpuFallbackTiles=0; for(const FVoxelTileKey& Key:Keys) { const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level}; const bool NearGpuEligible=Key.LevelFullResClipLevels; + if(Key.Level==3||Key.Level==4) + { + CpuTiles.push_back(Key); + if(Key.Level==3)++RemoteLod3CpuFallbackTiles; + else ++RemoteLod4CpuFallbackTiles; + if(bDrawOnlyTile&&IsDrawOnlyTile(Key)) + std::printf("[DrawOnlyTileRoute] key=(%d,%d,%d,lod%d) route=cpu_remote_band_fallback\n", + Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level); + continue; + } if(NearGpuEligible||ShapeWorld->isFarFieldTileEligible(CoreKey)) { const uint8 VisibleSkirtFaces=World->bGenerateSkirts @@ -2464,7 +2499,15 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; else Eligible.push_back(Key); } else CpuTiles.push_back(Key); + if(bDrawOnlyTile&&IsDrawOnlyTile(Key)) + std::printf("[DrawOnlyTileRoute] key=(%d,%d,%d,lod%d) route=%s\n", + Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level, + Eligible.empty()||Eligible.back()!=Key?"cpu_fallback":"gpu_eligible"); } + if(RemoteLod3CpuFallbackTiles+RemoteLod4CpuFallbackTiles>0) + std::printf("[GpuRemoteBandFallback] lod3_cpu_tiles=%llu lod4_cpu_tiles=%llu reason=remote_capture_pixel_coverage\n", + static_cast(RemoteLod3CpuFallbackTiles), + static_cast(RemoteLod4CpuFallbackTiles)); if(Eligible.size()>GpuFarOutputCapacitySlots) { for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second); @@ -3027,9 +3070,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; {std::fprintf(stderr,"LOD %d batch exceeded D3D11 limits.\n",Key.Level);return false;} std::vector Vertices;Vertices.reserve(VertexCount); std::vector Indices;Indices.reserve(IndexCount); + UINT DrawOnlyIndexStart=0,DrawOnlyIndexCount=0; for(const FVoxelTileKey& Tile:Members) { const auto It=ResidentTiles.find(Tile);if(It==ResidentTiles.end())continue; + const size_t TileIndexStart=Indices.size(); const uint32 Base=static_cast(Vertices.size()); const FResidentTileMesh& Resident=It->second; for(const FStandaloneCpuVertex& V:Resident.Vertices) @@ -3047,6 +3092,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; Indices.push_back(Base+Resident.Indices[Start+1]); Indices.push_back(Base+Resident.Indices[Start+2]); } + if(bDrawOnlyTile&&IsDrawOnlyTile(Tile)) + { + DrawOnlyIndexStart=static_cast(TileIndexStart); + DrawOnlyIndexCount=static_cast(Indices.size()-TileIndexStart); + } } FStreamingGpuBatch NewBatch; const bool bVertex=MakeBuffer(&NewBatch.VertexBuffer,Vertices.data(),Vertices.size()*sizeof(FGpuVertex),D3D11_BIND_VERTEX_BUFFER); @@ -3058,6 +3108,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; return false; } NewBatch.IndexCount=static_cast(Indices.size());NewBatch.VertexCount=Vertices.size(); + NewBatch.DrawOnlyIndexStart=DrawOnlyIndexStart; + NewBatch.DrawOnlyIndexCount=DrawOnlyIndexCount; if(Existing!=StreamingBatches.end()) { TotalVertices-=Existing->second.VertexCount;TotalIndices-=Existing->second.IndexCount; @@ -3459,9 +3511,13 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const FStreamingGpuBatch& L=Pair.second; if(!bShowLevel[Pair.first.Level] ||(CaptureOnlyLod>=0&&Pair.first.Level!=CaptureOnlyLod) - ||!L.VertexBuffer||!L.IndexBuffer||!L.IndexCount)continue; + ||!L.VertexBuffer||!L.IndexBuffer||!L.IndexCount + ||(bDrawOnlyTile&&L.DrawOnlyIndexCount==0))continue; const UINT Stride=sizeof(FGpuVertex),Offset=0;Context->IASetVertexBuffers(0,1,&L.VertexBuffer,&Stride,&Offset); - Context->IASetIndexBuffer(L.IndexBuffer,DXGI_FORMAT_R32_UINT,0);Context->DrawIndexed(L.IndexCount,0,0); + Context->IASetIndexBuffer(L.IndexBuffer,DXGI_FORMAT_R32_UINT,0); + if(bDrawOnlyTile) + Context->DrawIndexed(L.DrawOnlyIndexCount,L.DrawOnlyIndexStart,0); + else Context->DrawIndexed(L.IndexCount,0,0); } // Avoid issuing hundreds of tiny indirect draw calls while the // initial far batches are still being built. Once the clipmap is @@ -3474,7 +3530,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; for(auto& Pair:GpuFarTiles) { FGpuFarTile& Tile=Pair.second; - if(!bShowLevel[Pair.first.Level] + if(!IsDrawOnlyTile(Pair.first)||!bShowLevel[Pair.first.Level] ||(CaptureOnlyLod>=0&&Pair.first.Level!=CaptureOnlyLod) ||!Tile.VertexBuffer||!Tile.IndexBuffer||!Tile.IndirectArgs)continue; if(!bInitialClipmapReported&&Pair.first.Level>=World->FullResClipLevels)continue; @@ -3580,6 +3636,15 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const auto PresentCallStart=std::chrono::steady_clock::now(); const HRESULT PresentResult=SwapChain->Present(PresentSyncInterval,PresentFlags); const auto PresentCallEnd=std::chrono::steady_clock::now(); + if(bInitialClipmapReported&&!bWholeViewPresented) + { + bWholeViewPresented=true; + std::printf("[WholeViewPresented] at_ms=%.3f present_ms=%.3f sync=%u hr=0x%08lx\n", + std::chrono::duration(PresentCallEnd-ColdProcessStart).count(), + std::chrono::duration(PresentCallEnd-PresentCallStart).count(), + PresentSyncInterval,static_cast(PresentResult)); + std::fflush(stdout); + } if(bGpuWarmupHoldReported&&!bGpuWarmupReleaseReported) { bGpuWarmupReleaseReported=true; @@ -5533,6 +5598,14 @@ int main(int Argc,char** Argv) App.bCaptureOnlyTile=true; else {std::fprintf(stderr,"Invalid --capture-only-tile value: %s\n",Values.c_str());return 2;} } + else if(Arg.starts_with("--draw-only-tile=")) + { + const std::string Values(Arg.substr(std::string_view("--draw-only-tile=").size())); + if(std::sscanf(Values.c_str(),"%d,%d,%d,%d",&App.DrawOnlyTileX, + &App.DrawOnlyTileY,&App.DrawOnlyTileZ,&App.DrawOnlyTileLod)==4) + App.bDrawOnlyTile=true; + else {std::fprintf(stderr,"Invalid --draw-only-tile value: %s\n",Values.c_str());return 2;} + } else if(Arg.starts_with("--capture-output-slot=")) App.CaptureOutputSlot=std::max(-1,std::atoi( std::string(Arg.substr(std::string_view("--capture-output-slot=").size())).c_str()));