From ada3b4fe0961fbd014ab88f00a075f8f5147775f Mon Sep 17 00:00:00 2001 From: durswd Date: Tue, 23 Dec 2025 21:49:57 +0900 Subject: [PATCH 01/16] Fix command lists --- src/Utils/LLGI.CommandListPool.h | 10 ++++- src_test/capture.cpp | 2 +- src_test/test_clear.cpp | 24 +++-------- src_test/test_mipmap.cpp | 11 ++--- src_test/test_renderPass.cpp | 51 ++++++---------------- src_test/test_simple_render.cpp | 74 ++++++++------------------------ src_test/test_textures.cpp | 11 ++--- 7 files changed, 55 insertions(+), 128 deletions(-) diff --git a/src/Utils/LLGI.CommandListPool.h b/src/Utils/LLGI.CommandListPool.h index 696755d3..286db6d8 100644 --- a/src/Utils/LLGI.CommandListPool.h +++ b/src/Utils/LLGI.CommandListPool.h @@ -39,11 +39,19 @@ class CommandListPool SafeRelease(graphics_); } + void WaitUntilCompleted() + { + for (auto commandList : commandLists_) + { + commandList->WaitUntilCompleted(); + } + } + CommandList* Get(bool addRef = false) { CommandList* commandList = nullptr; - commandLists_[current_]->WaitUntilCompleted(); + WaitUntilCompleted(); commandList = commandLists_[current_]; diff --git a/src_test/capture.cpp b/src_test/capture.cpp index beb0043e..46145cff 100644 --- a/src_test/capture.cpp +++ b/src_test/capture.cpp @@ -175,7 +175,7 @@ void test_capture(LLGI::DeviceType deviceType, LLGI::Vec2I windowSize) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); diff --git a/src_test/test_clear.cpp b/src_test/test_clear.cpp index eeea9039..ccd35d0b 100644 --- a/src_test/test_clear.cpp +++ b/src_test/test_clear.cpp @@ -1,6 +1,6 @@ #include "TestHelper.h" #include "test.h" -#include +#include void test_clear_update(LLGI::DeviceType deviceType) { @@ -15,9 +15,7 @@ void test_clear_update(LLGI::DeviceType deviceType) auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); while (count < 60) { @@ -32,8 +30,7 @@ void test_clear_update(LLGI::DeviceType deviceType) color.B = 0; color.A = 255; - auto commandList = commandLists[count % commandLists.size()]; - commandList->WaitUntilCompleted(); + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass( @@ -48,7 +45,7 @@ void test_clear_update(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(color, true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -61,8 +58,6 @@ void test_clear_update(LLGI::DeviceType deviceType) graphics->WaitFinish(); LLGI::SafeRelease(sfMemoryPool); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); } @@ -80,9 +75,7 @@ void test_clear(LLGI::DeviceType deviceType) auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); LLGI::Color8 color; color.R = 255; @@ -99,8 +92,7 @@ void test_clear(LLGI::DeviceType deviceType) // It need to create a command buffer between NewFrame and Present. // Because get current screen returns other values by every frame. - auto commandList = commandLists[count % commandLists.size()]; - commandList->WaitUntilCompleted(); + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass( @@ -115,7 +107,7 @@ void test_clear(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(color, true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -128,8 +120,6 @@ void test_clear(LLGI::DeviceType deviceType) graphics->WaitFinish(); LLGI::SafeRelease(sfMemoryPool); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); } diff --git a/src_test/test_mipmap.cpp b/src_test/test_mipmap.cpp index ade5f4ce..f787ab0e 100644 --- a/src_test/test_mipmap.cpp +++ b/src_test/test_mipmap.cpp @@ -3,7 +3,6 @@ #include "test.h" #include -#include #include #include #include @@ -23,9 +22,7 @@ void test_mipmap(LLGI::DeviceType deviceType) auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); LLGI::TextureInitializationParameter texParam_mipmap; @@ -196,7 +193,7 @@ void test_mipmap(LLGI::DeviceType deviceType) pips[renderPassPipelineState] = LLGI::CreateSharedPtr(pip); } - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->GenerateMipMap(textureDrawnMipmap); @@ -243,7 +240,7 @@ void test_mipmap(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto textureMipmap = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(textureMipmap); @@ -261,8 +258,6 @@ void test_mipmap(LLGI::DeviceType deviceType) LLGI::SafeRelease(textureDrawnMipmap); LLGI::SafeRelease(shader_vs); LLGI::SafeRelease(shader_ps); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); diff --git a/src_test/test_renderPass.cpp b/src_test/test_renderPass.cpp index 9c7ae401..1c55e0e2 100644 --- a/src_test/test_renderPass.cpp +++ b/src_test/test_renderPass.cpp @@ -1,6 +1,6 @@ #include "TestHelper.h" #include "test.h" -#include +#include #include enum class RenderPassTestMode @@ -33,9 +33,7 @@ void test_renderPass(LLGI::DeviceType deviceType, RenderPassTestMode mode) auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); LLGI::RenderTextureInitializationParameter params; params.Size = LLGI::Vec2I(256, 256); @@ -173,7 +171,7 @@ void test_renderPass(LLGI::DeviceType deviceType, RenderPassTestMode mode) color2.B = 0; color2.A = 255; - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass(renderPass); @@ -264,7 +262,7 @@ void test_renderPass(LLGI::DeviceType deviceType, RenderPassTestMode mode) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto screenTex = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(screenTex); @@ -293,10 +291,7 @@ void test_renderPass(LLGI::DeviceType deviceType, RenderPassTestMode mode) } } - for (size_t i = 0; i < commandLists.size(); i++) - { - commandLists[i]->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); graphics->WaitFinish(); pips.clear(); @@ -308,10 +303,6 @@ void test_renderPass(LLGI::DeviceType deviceType, RenderPassTestMode mode) LLGI::SafeRelease(depthTextureDst); LLGI::SafeRelease(renderPass); LLGI::SafeRelease(texture); - for (size_t i = 0; i < commandLists.size(); i++) - { - LLGI::SafeRelease(commandLists[i]); - } LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); } @@ -332,9 +323,7 @@ void test_copyTextureToScreen(LLGI::DeviceType deviceType) auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); LLGI::RenderTextureInitializationParameter params; params.Size = LLGI::Vec2I(1280, 720); @@ -369,7 +358,7 @@ void test_copyTextureToScreen(LLGI::DeviceType deviceType) color2.B = 0; color2.A = 255; - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass(renderPass); commandList->EndRenderPass(); @@ -385,7 +374,7 @@ void test_copyTextureToScreen(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -394,17 +383,12 @@ void test_copyTextureToScreen(LLGI::DeviceType deviceType) } } - for (size_t i = 0; i < commandLists.size(); i++) - { - commandLists[i]->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); graphics->WaitFinish(); LLGI::SafeRelease(sfMemoryPool); LLGI::SafeRelease(renderTexture); LLGI::SafeRelease(renderPass); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); } @@ -424,9 +408,7 @@ void test_multiRenderPass(LLGI::DeviceType deviceType) auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics.get(), sfMemoryPool, 3); LLGI::TextureInitializationParameter texParam; texParam.Size = LLGI::Vec2I(256, 256); @@ -512,7 +494,7 @@ void test_multiRenderPass(LLGI::DeviceType deviceType) color2.B = 0; color2.A = 255; - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass(renderPass); @@ -595,7 +577,7 @@ void test_multiRenderPass(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto screenTexture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(screenTexture); Bitmap2D(data, screenTexture->GetSizeAs2D().X, screenTexture->GetSizeAs2D().Y, screenTexture->GetFormat()) @@ -603,10 +585,7 @@ void test_multiRenderPass(LLGI::DeviceType deviceType) } } - for (size_t i = 0; i < commandLists.size(); i++) - { - commandLists[i]->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); graphics->WaitFinish(); pips.clear(); @@ -616,10 +595,6 @@ void test_multiRenderPass(LLGI::DeviceType deviceType) LLGI::SafeRelease(renderTexture2); LLGI::SafeRelease(renderPass); LLGI::SafeRelease(texture); - for (size_t i = 0; i < commandLists.size(); i++) - { - LLGI::SafeRelease(commandLists[i]); - } LLGI::SafeRelease(compiler); } diff --git a/src_test/test_simple_render.cpp b/src_test/test_simple_render.cpp index 9c6904ab..0fda2e9f 100644 --- a/src_test/test_simple_render.cpp +++ b/src_test/test_simple_render.cpp @@ -134,7 +134,7 @@ void test_simple_rectangle(LLGI::DeviceType deviceType, SingleRectangleTestMode if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -157,11 +157,7 @@ void test_simple_rectangle(LLGI::DeviceType deviceType, SingleRectangleTestMode } } - for (int i = 0; i < 3; i++) - { - auto commandList = commandListPool->Get(); - commandList->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); pips.clear(); @@ -187,9 +183,7 @@ void test_index_offset(LLGI::DeviceType deviceType) auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); std::shared_ptr shader_vs = nullptr; std::shared_ptr shader_ps = nullptr; @@ -243,7 +237,7 @@ void test_index_offset(LLGI::DeviceType deviceType) pips[renderPassPipelineState] = LLGI::CreateSharedPtr(pip); } - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass(renderPass); commandList->SetVertexBuffer(vb.get(), sizeof(SimpleVertex), 0); @@ -260,7 +254,7 @@ void test_index_offset(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); Bitmap2D(data, texture->GetSizeAs2D().X, texture->GetSizeAs2D().Y, texture->GetFormat()) @@ -269,19 +263,13 @@ void test_index_offset(LLGI::DeviceType deviceType) } } - for (size_t i = 0; i < commandLists.size(); i++) - { - auto commandList = commandLists[i]; - commandList->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); pips.clear(); graphics->WaitFinish(); LLGI::SafeRelease(sfMemoryPool); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); } @@ -354,9 +342,7 @@ void main() auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); LLGI::Buffer* cb_vs = nullptr; LLGI::Buffer* cb_ps = nullptr; @@ -517,7 +503,7 @@ void main() pips[renderPassPipelineState] = LLGI::CreateSharedPtr(pip); } - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass(renderPass); @@ -544,7 +530,7 @@ void main() if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -563,11 +549,7 @@ void main() } } - for (int i = 0; i < commandLists.size(); i++) - { - auto commandList = commandLists[i]; - commandList->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); graphics->WaitFinish(); pips.clear(); @@ -577,8 +559,6 @@ void main() LLGI::SafeRelease(cb_ps); LLGI::SafeRelease(shader_vs); LLGI::SafeRelease(shader_ps); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); @@ -644,9 +624,7 @@ void main() auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); LLGI::TextureInitializationParameter texParam; @@ -788,7 +766,7 @@ void main() pips[renderPassPipelineState] = LLGI::CreateSharedPtr(pip); } - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->BeginRenderPass(renderPass); // commandList->SetConstantBuffer(dummy_cb.get(), LLGI::ShaderStageType::Vertex); @@ -807,7 +785,7 @@ void main() if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -832,11 +810,7 @@ void main() } } - for (size_t i = 0; i < commandLists.size(); i++) - { - auto commandList = commandLists[i]; - commandList->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); graphics->WaitFinish(); pips.clear(); @@ -845,8 +819,6 @@ void main() LLGI::SafeRelease(textureDrawn); LLGI::SafeRelease(shader_vs); LLGI::SafeRelease(shader_ps); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); @@ -954,7 +926,7 @@ void test_instancing(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -964,11 +936,7 @@ void test_instancing(LLGI::DeviceType deviceType) } } - for (int i = 0; i < 3; i++) - { - auto commandList = commandListPool->Get(); - commandList->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); graphics->WaitFinish(); pips.clear(); } @@ -1092,7 +1060,7 @@ void test_vertex_structured(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -1206,7 +1174,7 @@ void test_vtf(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto texture = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(texture); @@ -1216,11 +1184,7 @@ void test_vtf(LLGI::DeviceType deviceType) } } - for (int i = 0; i < 3; i++) - { - auto commandList = commandListPool->Get(); - commandList->WaitUntilCompleted(); - } + commandListPool->WaitUntilCompleted(); graphics->WaitFinish(); pips.clear(); } diff --git a/src_test/test_textures.cpp b/src_test/test_textures.cpp index 975ffa96..843d42fe 100644 --- a/src_test/test_textures.cpp +++ b/src_test/test_textures.cpp @@ -3,7 +3,6 @@ #include "test.h" #include -#include #include #include #include @@ -23,9 +22,7 @@ void test_textures(LLGI::DeviceType deviceType) auto graphics = platform->CreateGraphics(); auto sfMemoryPool = graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128); - std::array commandLists; - for (size_t i = 0; i < commandLists.size(); i++) - commandLists[i] = graphics->CreateCommandList(sfMemoryPool); + auto commandListPool = std::make_shared(graphics, sfMemoryPool, 3); // Create textures LLGI::TextureInitializationParameter texParamSrc1; @@ -196,7 +193,7 @@ void test_textures(LLGI::DeviceType deviceType) pips[renderPassPipelineState] = LLGI::CreateSharedPtr(pip); } - auto commandList = commandLists[count % commandLists.size()]; + auto commandList = commandListPool->Get(); commandList->Begin(); commandList->CopyTexture(texSrc1.get(), texDst1.get(), {0, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, 0); @@ -221,7 +218,7 @@ void test_textures(LLGI::DeviceType deviceType) if (TestHelper::GetIsCaptureRequired() && count == 30) { - commandList->WaitUntilCompleted(); + commandListPool->WaitUntilCompleted(); auto textureMipmap = platform->GetCurrentScreen(LLGI::Color8(), true)->GetRenderTexture(0); auto data = graphics->CaptureRenderTarget(textureMipmap); @@ -237,8 +234,6 @@ void test_textures(LLGI::DeviceType deviceType) LLGI::SafeRelease(sfMemoryPool); LLGI::SafeRelease(shader_vs); LLGI::SafeRelease(shader_ps); - for (size_t i = 0; i < commandLists.size(); i++) - LLGI::SafeRelease(commandLists[i]); LLGI::SafeRelease(graphics); LLGI::SafeRelease(platform); From 985c96999cceae875e94a69be6832bb10d6ae832 Mon Sep 17 00:00:00 2001 From: durswd Date: Sun, 4 Jan 2026 16:13:11 +0900 Subject: [PATCH 02/16] Fix to compile. Improve error message. --- src/DX12/LLGI.GraphicsDX12.cpp | 19 +++++++++---------- tools/ShaderTranspiler/main.cpp | 9 +++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/DX12/LLGI.GraphicsDX12.cpp b/src/DX12/LLGI.GraphicsDX12.cpp index d26e98c3..a4b76351 100644 --- a/src/DX12/LLGI.GraphicsDX12.cpp +++ b/src/DX12/LLGI.GraphicsDX12.cpp @@ -4,10 +4,10 @@ #include "LLGI.CommandListDX12.h" #include "LLGI.PipelineStateDX12.h" #include "LLGI.PlatformDX12.h" +#include "LLGI.QueryDX12.h" #include "LLGI.ShaderDX12.h" #include "LLGI.SingleFrameMemoryPoolDX12.h" #include "LLGI.TextureDX12.h" -#include "LLGI.QueryDX12.h" namespace LLGI { @@ -310,6 +310,12 @@ std::vector GraphicsDX12::CaptureRenderTarget(Texture* renderTarget) auto dstFootprint = texture->GetFootprint().Footprint; + ID3D12CommandAllocator* commandAllocator = nullptr; + ID3D12GraphicsCommandList* commandList = nullptr; + D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{}; + UINT64 totalSize{}; + D3D12_RESOURCE_DESC textureDesc{}; + BufferDX12 dstBuffer; if (!dstBuffer.Initialize(this, BufferUsageType::CopyDst | BufferUsageType::MapRead, dstFootprint.RowPitch * dstFootprint.Height)) { @@ -317,8 +323,6 @@ std::vector GraphicsDX12::CaptureRenderTarget(Texture* renderTarget) ::LLGI::Log(::LLGI::LogType::Error, msg.c_str()); goto FAILED_EXIT; } - ID3D12CommandAllocator* commandAllocator = nullptr; - ID3D12GraphicsCommandList* commandList = nullptr; auto hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); if (FAILED(hr)) @@ -339,9 +343,7 @@ std::vector GraphicsDX12::CaptureRenderTarget(Texture* renderTarget) goto FAILED_EXIT; } - D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint; - UINT64 totalSize; - auto textureDesc = texture->Get()->GetDesc(); + textureDesc = texture->Get()->GetDesc(); device->GetCopyableFootprints(&textureDesc, 0, 1, 0, &footprint, nullptr, nullptr, &totalSize); src.pResource = texture->Get(); @@ -408,9 +410,6 @@ Query* GraphicsDX12::CreateQuery(QueryType queryType, int32_t queryCount) return obj; } -uint64_t GraphicsDX12::TimestampToMicroseconds(uint64_t timestamp) const -{ - return timestamp * 1000000 / timestampFrequency_; -} +uint64_t GraphicsDX12::TimestampToMicroseconds(uint64_t timestamp) const { return timestamp * 1000000 / timestampFrequency_; } } // namespace LLGI diff --git a/tools/ShaderTranspiler/main.cpp b/tools/ShaderTranspiler/main.cpp index 98be23e5..b2025c3c 100644 --- a/tools/ShaderTranspiler/main.cpp +++ b/tools/ShaderTranspiler/main.cpp @@ -113,14 +113,14 @@ int main(int argc, char* argv[]) { if (i == args.size() - 1) { - std::cout << "Invald input" << std::endl; + std::cout << "Invald input : arg is none" << std::endl; return 0; } std::ifstream ifs(args[i + 1]); if (ifs.fail()) { - std::cout << "Invald input" << std::endl; + std::cout << "Invald input : unknown file " << args[i + 1] << std::endl; return 0; } code = std::string((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -131,7 +131,7 @@ int main(int argc, char* argv[]) { if (i == args.size() - 1) { - std::cout << "Invald output" << std::endl; + std::cout << "Invald output : arg is none" << std::endl; return 0; } @@ -180,7 +180,8 @@ int main(int argc, char* argv[]) auto generator = std::make_shared(loadFunc); - auto spirv = generator->Generate(inputPath.c_str(), code.c_str(), includeDir, macros, shaderStage, outputType == OutputType::VULKAN_GLSL); + auto spirv = + generator->Generate(inputPath.c_str(), code.c_str(), includeDir, macros, shaderStage, outputType == OutputType::VULKAN_GLSL); if (spirv->GetData().size() == 0) { From 9ef31c236e98f9cb93c95a199f4c16ea2aeef165 Mon Sep 17 00:00:00 2001 From: durswd Date: Sun, 8 Mar 2026 16:12:17 +0900 Subject: [PATCH 03/16] Improve readme and fix compile on Linux --- README.md | 226 ++++++++++++++++++++++++--- examples/GPUParticle/GPUParticle.cpp | 29 +++- src_test/TestHelper.cpp | 29 +++- 3 files changed, 248 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 9eee953c..50cbb870 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,222 @@ # LLGI -How to build ----------- +LLGI is a cross-platform graphics abstraction library that provides a common +API for DirectX 12, Metal, and Vulkan. -### Windows +This repository currently contains: +- `LLGI`: the core static library +- `LLGI_Test`: rendering and shader tests (`BUILD_TEST=ON`) +- `example_glfw`, `example_imgui`, `example_GPUParticle`: sample applications + (`BUILD_EXAMPLE=ON`) +- `ShaderTranspiler`: shader conversion tool (`BUILD_TOOL=ON`) + +## Backend and platform support + +| Platform | Default backend | Optional backends | Notes | +| --- | --- | --- | --- | +| Windows | DirectX 12 | Vulkan | CI builds both x86 and x64 configurations. | +| macOS | Metal | - | CI builds with `CMAKE_OSX_DEPLOYMENT_TARGET=10.15`. | +| iOS | Metal | - | CI includes an iOS build configuration. | +| Linux | Vulkan | - | `BUILD_VULKAN` is forced to `ON` on Linux. | + +## Repository layout + +| Path | Description | +| --- | --- | +| `src/` | LLGI library sources and public headers | +| `src_test/` | Test executable and shader test assets | +| `examples/` | GLFW, Dear ImGui, and GPU particle samples | +| `tools/` | Shader transpiler sources | +| `scripts/transpile.py` | Helper script to batch-convert shader assets | +| `thirdparty/` | Bundled third-party dependencies used by Vulkan compiler/tool builds | + +## Getting the source + +```bash +git clone https://github.com/effekseer/LLGI.git +cd LLGI +git submodule update --init --recursive ``` -$ git clone https://github.com/altseed/LLGI.git -$ cd LLGI -$ git submodule update --init -$ cmake -S . -B build -DBUILD_TEST=ON -$ cmake --build build + +## Build requirements + +- CMake 3.15 or newer +- A C++ toolchain for your platform (`Visual Studio`, `Xcode`, `clang`, or `gcc`) +- On Linux, Vulkan and X11 development packages +- When `BUILD_VULKAN_COMPILER` or `BUILD_TOOL` is enabled, the bundled + `glslang` and `SPIRV-Cross` submodules are used by default + +Linux packages used in CI: + +```bash +sudo apt-get update +sudo apt-get install -y \ + libx11-dev \ + libxrandr-dev \ + libxi-dev \ + libxinerama-dev \ + libxcursor-dev \ + libudev-dev \ + libx11-xcb-dev \ + libglu1-mesa-dev \ + mesa-common-dev \ + libvulkan-dev ``` -### macOS +## Common CMake options + +| Option | Default | Description | +| --- | --- | --- | +| `BUILD_TEST` | `OFF` | Build `LLGI_Test` | +| `BUILD_EXAMPLE` | `OFF` | Build sample applications | +| `BUILD_TOOL` | `OFF` | Build `ShaderTranspiler` | +| `BUILD_VULKAN` | `OFF` (`ON` on Linux) | Enable the Vulkan backend | +| `BUILD_VULKAN_COMPILER` | `OFF` | Enable Vulkan shader compilation support in `LLGI::CreateCompiler` | +| `USE_CREATE_COMPILER_FUNCTION` | `ON` | Keep `LLGI::CreateCompiler` enabled | +| `USE_MSVC_RUNTIME_LIBRARY_DLL` | `ON` | Use the DLL MSVC runtime (`/MD`) | + +## Build +### Windows (DirectX 12) + +```bash +cmake -S . -B build -DBUILD_TEST=ON -DBUILD_EXAMPLE=ON +cmake --build build --config Release +``` + +For a 32-bit build: + +```bash +cmake -S . -B build -A Win32 -DBUILD_TEST=ON +cmake --build build --config Release +``` + +### Windows (DirectX 12 + Vulkan tools) + +```bash +cmake -S . -B build \ + -DBUILD_TEST=ON \ + -DBUILD_EXAMPLE=ON \ + -DBUILD_TOOL=ON \ + -DBUILD_VULKAN=ON \ + -DBUILD_VULKAN_COMPILER=ON +cmake --build build --config Release ``` -$ git clone https://github.com/altseed/LLGI.git -$ cd LLGI -$ git submodule update --init -$ cmake -S . -B build -G "Xcode" -DBUILD_TEST=ON -$ cmake --build build + +### macOS + +```bash +cmake -S . -B build -G Xcode \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ + -DBUILD_TEST=ON \ + -DBUILD_EXAMPLE=ON +cmake --build build --config Release ``` -### Vulkan(Window, Linux) +### iOS +```bash +cmake -S . -B build-ios -G Xcode \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \ + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" +cmake --build build-ios --config Release ``` -$ git clone https://github.com/altseed/LLGI.git -$ cd LLGI -$ git submodule update --init -$ cmake -S . -B build -DBUILD_VULKAN=ON -DBUILD_TEST=ON -$ cmake --build build + +### Linux (Vulkan) + +```bash +cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TEST=ON \ + -DBUILD_EXAMPLE=ON \ + -DBUILD_TOOL=ON \ + -DBUILD_VULKAN_COMPILER=ON +cmake --build build ``` -Test ----------- +On Linux, `BUILD_VULKAN` is enabled automatically by the top-level +`CMakeLists.txt`. -Run with Vulkan +## Install + +```bash +cmake --install build --prefix ``` -./LLGI_Test --vulkan + +For multi-config generators such as Visual Studio or Xcode, add +`--config Release`. + +The install step exports the `LLGI` static library, public headers, and CMake +package files under `lib/cmake`. + +## Running tests + +`LLGI_Test` is available when `BUILD_TEST=ON`. + +Default device selection: + +- Windows: DirectX 12 +- macOS/iOS: Metal +- Linux: Vulkan + +Examples: + +```bash +# Linux / macOS +./build/src_test/LLGI_Test +./build/src_test/LLGI_Test --filter=SimpleRender.* + +# Windows +build\src_test\Release\LLGI_Test.exe +build\src_test\Release\LLGI_Test.exe --filter=Compile.* +build\src_test\Release\LLGI_Test.exe --vulkan ``` -Run with single test +If you want Vulkan shader compilation through +`LLGI::CreateCompiler(DeviceType::Vulkan)` or the `Compile.*` tests on Vulkan, +configure with `-DBUILD_VULKAN_COMPILER=ON`. + +## Examples + +When `BUILD_EXAMPLE=ON`, the following targets are built: + +- `example_glfw`: minimal clear/present flow using GLFW +- `example_imgui`: Dear ImGui integration on top of LLGI and GLFW +- `example_GPUParticle`: GPU particle sample +The smallest end-to-end sample is in `examples/glfw/main.cpp`. + +## Shader tools + +When `BUILD_TOOL=ON`, the repository builds `ShaderTranspiler`. + +The helper script below uses the built tool to batch-convert shader assets: + +```bash +python scripts/transpile.py src_test/Shaders/ +python scripts/transpile.py examples/GPUParticle/Shaders/ ``` -./LLGI_Test --filter= + +Additional notes are available in [tools/README.md](tools/README.md). + +## Minimal usage + +```cpp +auto window = std::unique_ptr(LLGI::CreateWindow("LLGI", {1280, 720})); + +LLGI::PlatformParameter parameter; +parameter.Device = LLGI::DeviceType::Default; + +auto platform = LLGI::CreateSharedPtr(LLGI::CreatePlatform(parameter, window.get())); +auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); +auto memoryPool = LLGI::CreateSharedPtr(graphics->CreateSingleFrameMemoryPool(1024 * 1024, 128)); +auto commandList = LLGI::CreateSharedPtr(graphics->CreateCommandList(memoryPool.get())); ``` + +For a complete runnable example, see `examples/glfw/main.cpp`. + +## License + +LLGI is distributed under the zlib license. See [LICENSE](LICENSE). diff --git a/examples/GPUParticle/GPUParticle.cpp b/examples/GPUParticle/GPUParticle.cpp index 1064c554..102404b1 100644 --- a/examples/GPUParticle/GPUParticle.cpp +++ b/examples/GPUParticle/GPUParticle.cpp @@ -200,12 +200,31 @@ std::vector Shader::LoadData(const char* path) return ret; } - fseek(fp, 0, SEEK_END); - auto size = ftell(fp); - fseek(fp, 0, SEEK_SET); + if (fseek(fp, 0, SEEK_END) != 0) + { + fclose(fp); + return ret; + } + + const auto size = ftell(fp); + if (size <= 0) + { + fclose(fp); + return ret; + } - ret.resize(size); - fread(ret.data(), 1, size, fp); + if (fseek(fp, 0, SEEK_SET) != 0) + { + fclose(fp); + return ret; + } + + ret.resize(static_cast(size)); + const auto readSize = fread(ret.data(), 1, ret.size(), fp); + if (readSize != ret.size()) + { + ret.resize(readSize); + } fclose(fp); return ret; diff --git a/src_test/TestHelper.cpp b/src_test/TestHelper.cpp index bf60cdf9..2e2a86b3 100644 --- a/src_test/TestHelper.cpp +++ b/src_test/TestHelper.cpp @@ -32,12 +32,31 @@ class DefaultTestFileReader : public TestFileReader return ret; } - fseek(fp, 0, SEEK_END); - auto size = ftell(fp); - fseek(fp, 0, SEEK_SET); + if (fseek(fp, 0, SEEK_END) != 0) + { + fclose(fp); + return ret; + } + + const auto size = ftell(fp); + if (size <= 0) + { + fclose(fp); + return ret; + } - ret.resize(size); - fread(ret.data(), 1, size, fp); + if (fseek(fp, 0, SEEK_SET) != 0) + { + fclose(fp); + return ret; + } + + ret.resize(static_cast(size)); + const auto readSize = fread(ret.data(), 1, ret.size(), fp); + if (readSize != ret.size()) + { + ret.resize(readSize); + } fclose(fp); return ret; From 3d1bb9bbb55c503510e91c85dc89b64c9eb92af7 Mon Sep 17 00:00:00 2001 From: durswd Date: Sun, 8 Mar 2026 16:58:07 +0900 Subject: [PATCH 04/16] Fix bugs in Vulkan and DX12 --- src/DX12/LLGI.CommandListDX12.cpp | 5 +- src/Vulkan/LLGI.GraphicsVulkan.cpp | 12 +++- src/Vulkan/LLGI.PipelineStateVulkan.cpp | 2 +- src/Vulkan/LLGI.PlatformVulkan.cpp | 84 ++++++++++++++++++++----- src/Vulkan/LLGI.PlatformVulkan.h | 5 +- 5 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/DX12/LLGI.CommandListDX12.cpp b/src/DX12/LLGI.CommandListDX12.cpp index 0bb6a187..f5243823 100644 --- a/src/DX12/LLGI.CommandListDX12.cpp +++ b/src/DX12/LLGI.CommandListDX12.cpp @@ -1010,8 +1010,11 @@ void CommandListDX12::ClearDepth() return; } + auto depthTexture = static_cast(rt->GetDepthTexture()); + const auto clearFlags = HasStencil(depthTexture->GetFormat()) ? D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL : D3D12_CLEAR_FLAG_DEPTH; + auto handle = rt->GetHandleDSV(); - currentCommandList_->ClearDepthStencilView(handle[0], D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr); + currentCommandList_->ClearDepthStencilView(handle[0], clearFlags, 1.0f, 0, 0, nullptr); } ID3D12GraphicsCommandList* CommandListDX12::GetCommandList() const { return commandList_.get(); } diff --git a/src/Vulkan/LLGI.GraphicsVulkan.cpp b/src/Vulkan/LLGI.GraphicsVulkan.cpp index 68215b5b..e205ac60 100644 --- a/src/Vulkan/LLGI.GraphicsVulkan.cpp +++ b/src/Vulkan/LLGI.GraphicsVulkan.cpp @@ -7,6 +7,7 @@ #include "LLGI.SingleFrameMemoryPoolVulkan.h" #include "LLGI.TextureVulkan.h" #include "LLGI.QueryVulkan.h" +#include "../LLGI.Platform.h" namespace LLGI { @@ -47,7 +48,16 @@ GraphicsVulkan::~GraphicsVulkan() SafeRelease(owner_); } -void GraphicsVulkan::SetWindowSize(const Vec2I& windowSize) { throw "Not inplemented"; } +void GraphicsVulkan::SetWindowSize(const Vec2I& windowSize) +{ + if (auto platform = dynamic_cast(owner_)) + { + platform->SetWindowSize(windowSize); + return; + } + + Graphics::SetWindowSize(windowSize); +} void GraphicsVulkan::Execute(CommandList* commandList) { diff --git a/src/Vulkan/LLGI.PipelineStateVulkan.cpp b/src/Vulkan/LLGI.PipelineStateVulkan.cpp index 296838fa..7634a274 100644 --- a/src/Vulkan/LLGI.PipelineStateVulkan.cpp +++ b/src/Vulkan/LLGI.PipelineStateVulkan.cpp @@ -400,7 +400,7 @@ bool PipelineStateVulkan::CreateGraphicsPipeline() blendFuncs[static_cast(BlendFuncType::DstColor)] = vk::BlendFactor::eDstColor; blendFuncs[static_cast(BlendFuncType::OneMinusDstColor)] = vk::BlendFactor::eOneMinusDstColor; blendFuncs[static_cast(BlendFuncType::DstAlpha)] = vk::BlendFactor::eDstAlpha; - blendFuncs[static_cast(BlendFuncType::OneMinusDstAlpha)] = vk::BlendFactor::eDstAlpha; + blendFuncs[static_cast(BlendFuncType::OneMinusDstAlpha)] = vk::BlendFactor::eOneMinusDstAlpha; blendInfo.srcColorBlendFactor = blendFuncs[static_cast(BlendSrcFunc)]; blendInfo.dstColorBlendFactor = blendFuncs[static_cast(BlendDstFunc)]; diff --git a/src/Vulkan/LLGI.PlatformVulkan.cpp b/src/Vulkan/LLGI.PlatformVulkan.cpp index 870bf154..75ecb077 100644 --- a/src/Vulkan/LLGI.PlatformVulkan.cpp +++ b/src/Vulkan/LLGI.PlatformVulkan.cpp @@ -76,10 +76,13 @@ bool PlatformVulkan::CreateSwapChain(Vec2I windowSize, bool waitVSync) { disposeOldSwapchain(); swapchain_ = nullptr; + swapchainSize_ = {0, 0}; frameIndex = 0; } else { + swapchainSize_ = {static_cast(swapchainExtent.width), static_cast(swapchainExtent.height)}; + // select sync or vsync vk::PresentModeKHR swapchainPresentMode = vk::PresentModeKHR::eFifo; if (!waitVSync) @@ -158,7 +161,7 @@ bool PlatformVulkan::CreateSwapChain(Vec2I windowSize, bool waitVSync) swapBuffers[i].fence = vk::Fence(); swapBuffers[i].texture = new TextureVulkan(); - if (!swapBuffers[i].texture->InitializeAsScreen(swapBuffers[i].image, swapBuffers[i].view, surfaceFormat, windowSize)) + if (!swapBuffers[i].texture->InitializeAsScreen(swapBuffers[i].image, swapBuffers[i].view, surfaceFormat, swapchainSize_)) { Log(LogType::Error, "failed to create a texture while creating swap buffers."); throw "failed to create a texture while creating swap buffers."; @@ -196,6 +199,30 @@ bool PlatformVulkan::CreateDepthBuffer(Vec2I windowSize) return false; } +bool PlatformVulkan::RecreateSwapchain(const Vec2I& windowSize) +{ + if (!CreateSwapChain(windowSize, waitVSync_)) + { + return false; + } + + renderPasses_.clear(); + + if (!IsSwapchainValid()) + { + SafeRelease(depthStencilTexture_); + return true; + } + + if (!CreateDepthBuffer(swapchainSize_)) + { + return false; + } + + CreateRenderPass(); + return true; +} + void PlatformVulkan::CreateRenderPass() { renderPasses_.clear(); @@ -212,13 +239,16 @@ void PlatformVulkan::CreateRenderPass() } } -uint32_t PlatformVulkan::AcquireNextImage(vk::Semaphore& semaphore) +vk::Result PlatformVulkan::AcquireNextImage(vk::Semaphore& semaphore) { auto resultValue = vkDevice_.acquireNextImageKHR(swapchain_, UINT64_MAX, semaphore, vk::Fence()); - assert(resultValue.result == vk::Result::eSuccess); - frameIndex = resultValue.value; - return frameIndex; + if (resultValue.result == vk::Result::eSuccess || resultValue.result == vk::Result::eSuboptimalKHR) + { + frameIndex = resultValue.value; + } + + return resultValue.result; } vk::Fence PlatformVulkan::GetSubmitFence(bool destroy) @@ -671,7 +701,7 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) vkCmdBuffers = vkDevice_.allocateCommandBuffers(allocInfo); // create depth buffer - if (!CreateDepthBuffer(window->GetWindowSize())) + if (IsSwapchainValid() && !CreateDepthBuffer(swapchainSize_)) { exitWithError(); return false; @@ -707,7 +737,28 @@ bool PlatformVulkan::NewFrame() if (IsSwapchainValid()) { - AcquireNextImage(vkPresentComplete_); + auto acquireResult = AcquireNextImage(vkPresentComplete_); + if (acquireResult == vk::Result::eErrorOutOfDateKHR) + { + vkDevice_.waitIdle(); + if (!RecreateSwapchain(windowSize_)) + { + return false; + } + + if (IsSwapchainValid()) + { + acquireResult = AcquireNextImage(vkPresentComplete_); + } + } + + if (IsSwapchainValid() && acquireResult != vk::Result::eSuccess && acquireResult != vk::Result::eSuboptimalKHR) + { + std::stringstream ss; + ss << "Failed to acquire next image : " << VulkanHelper::getResultName(static_cast(acquireResult)); + Log(LogType::Error, ss.str()); + return false; + } } executedCommandCount = 0; return true; @@ -764,12 +815,13 @@ void PlatformVulkan::Present() const auto result = Present(vkRenderComplete_); // TODO optimize it - if (result == vk::Result::eErrorOutOfDateKHR) + if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) { vkDevice_.waitIdle(); - CreateSwapChain(windowSize_, waitVSync_); - CreateDepthBuffer(windowSize_); - CreateRenderPass(); + if (!RecreateSwapchain(windowSize_)) + { + Log(LogType::Error, "Failed to recreate swapchain after present."); + } } } @@ -781,11 +833,11 @@ void PlatformVulkan::SetWindowSize(const Vec2I& windowSize) } vkDevice_.waitIdle(); - CreateSwapChain(windowSize, waitVSync_); - - CreateDepthBuffer(windowSize); - - CreateRenderPass(); + if (!RecreateSwapchain(windowSize)) + { + Log(LogType::Error, "Failed to recreate swapchain after resize."); + return; + } windowSize_ = windowSize; } diff --git a/src/Vulkan/LLGI.PlatformVulkan.h b/src/Vulkan/LLGI.PlatformVulkan.h index ed9b9ada..a029eb44 100644 --- a/src/Vulkan/LLGI.PlatformVulkan.h +++ b/src/Vulkan/LLGI.PlatformVulkan.h @@ -56,6 +56,7 @@ class PlatformVulkan : public Platform int32_t queueFamilyIndex_ = 0; Vec2I windowSize_; + Vec2I swapchainSize_; //! to check to finish present vk::Semaphore vkPresentComplete_; @@ -97,13 +98,15 @@ class PlatformVulkan : public Platform bool CreateDepthBuffer(Vec2I windowSize); + bool RecreateSwapchain(const Vec2I& windowSize); + void CreateRenderPass(); /*! @brief get swap buffer index @param semaphore the signaling semaphore to be waited for other functions */ - uint32_t AcquireNextImage(vk::Semaphore& semaphore); + vk::Result AcquireNextImage(vk::Semaphore& semaphore); vk::Fence GetSubmitFence(bool destroy = false); From 4674d39a156c8a8fbb747c964754f40eeaa8ab6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=AE=87?= <63290381+xiaoyv404@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:11:08 +0800 Subject: [PATCH 05/16] fix Vulkan on mac (#1) * fix Vulkan on mac * applying suggestion from copilot --- src/Mac/LLGI.WindowMac.mm | 10 +++++++++- src/Metal/LLGI.BufferMetal.mm | 11 ++++++----- src/Vulkan/LLGI.BaseVulkan.h | 4 ++++ src/Vulkan/LLGI.PlatformVulkan.cpp | 27 ++++++++++++++++++++++++--- src/Vulkan/LLGI.PlatformVulkan.h | 2 ++ 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Mac/LLGI.WindowMac.mm b/src/Mac/LLGI.WindowMac.mm index 510e80b9..264024fb 100644 --- a/src/Mac/LLGI.WindowMac.mm +++ b/src/Mac/LLGI.WindowMac.mm @@ -165,7 +165,15 @@ bool newFrame() bool WindowMac::OnNewFrame() { return DoEvent(); } -void* WindowMac::GetNativePtr(int32_t index) { return GetNSWindowAsVoidPtr(); } +void* WindowMac::GetNativePtr(int32_t index) +{ + if (index == 1) + { + return impl_->window_.contentView; + } + + return GetNSWindowAsVoidPtr(); +} Vec2I WindowMac::GetWindowSize() const { return windowSize_; } diff --git a/src/Metal/LLGI.BufferMetal.mm b/src/Metal/LLGI.BufferMetal.mm index 1e71194c..3273f4e7 100644 --- a/src/Metal/LLGI.BufferMetal.mm +++ b/src/Metal/LLGI.BufferMetal.mm @@ -15,14 +15,15 @@ BufferMetal::BufferMetal() { - + } BufferMetal::~BufferMetal() { if (isExternalResource_) + { return; - + } if (buffer_ != nullptr) { [buffer_ release]; @@ -38,7 +39,7 @@ } auto g = static_cast(graphics); - + if(BitwiseContains(usage, BufferUsageType::MapWrite) || BitwiseContains(usage, BufferUsageType::MapRead)) { buffer_ = [g->GetDevice() newBufferWithLength:size options:MTLResourceStorageModeShared]; @@ -47,9 +48,9 @@ { buffer_ = [g->GetDevice() newBufferWithLength:size options:MTLResourceStorageModePrivate]; } - + size_ = size; - + return true; } diff --git a/src/Vulkan/LLGI.BaseVulkan.h b/src/Vulkan/LLGI.BaseVulkan.h index 22906875..84f75b5a 100644 --- a/src/Vulkan/LLGI.BaseVulkan.h +++ b/src/Vulkan/LLGI.BaseVulkan.h @@ -8,6 +8,10 @@ #ifdef _WIN32 #define VK_PROTOTYPES #define VK_USE_PLATFORM_WIN32_KHR +#elif defined(__APPLE__) +#define VK_PROTOTYPES +#define VK_ENABLE_BETA_EXTENSIONS +#define VK_USE_PLATFORM_MACOS_MVK #else #define VK_PROTOTYPES #define VK_USE_PLATFORM_XCB_KHR diff --git a/src/Vulkan/LLGI.PlatformVulkan.cpp b/src/Vulkan/LLGI.PlatformVulkan.cpp index 75ecb077..5a2a7594 100644 --- a/src/Vulkan/LLGI.PlatformVulkan.cpp +++ b/src/Vulkan/LLGI.PlatformVulkan.cpp @@ -45,7 +45,8 @@ bool PlatformVulkan::CreateSwapChain(Vec2I windowSize, bool waitVSync) { auto oldSwapChain = swapchain_; - const auto disposeOldSwapchain = [&]() { + const auto disposeOldSwapchain = [&]() + { if (oldSwapChain) { for (uint32_t i = 0; i < swapBuffers.size(); i++) @@ -489,6 +490,11 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) VK_KHR_SURFACE_EXTENSION_NAME, #ifdef _WIN32 VK_KHR_WIN32_SURFACE_EXTENSION_NAME, +#elif defined(__APPLE__) + VK_MVK_MACOS_SURFACE_EXTENSION_NAME, +#if defined(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) + VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, +#endif #else VK_KHR_XCB_SURFACE_EXTENSION_NAME, #endif @@ -498,7 +504,8 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) #endif }; - auto exitWithError = [this]() -> void { + auto exitWithError = [this]() -> void + { Reset(); SafeRelease(depthStencilTexture_); @@ -523,6 +530,9 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) instanceCreateInfo.pApplicationInfo = &appInfo; instanceCreateInfo.enabledExtensionCount = static_cast(extensions.size()); instanceCreateInfo.ppEnabledExtensionNames = extensions.data(); +#if defined(__APPLE__) && defined(VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR) + instanceCreateInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif #if !defined(NDEBUG) uint32_t layerCount = 0; @@ -549,6 +559,12 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) // get physics device auto physicalDevices = vkInstance_.enumeratePhysicalDevices(); + if (physicalDevices.empty()) + { + Log(LogType::Error, "No Vulkan physical devices found."); + exitWithError(); + return false; + } vkPhysicalDevice = physicalDevices[0]; struct Version @@ -569,6 +585,10 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) surfaceCreateInfo.hinstance = (HINSTANCE)window->GetNativePtr(1); surfaceCreateInfo.hwnd = (HWND)window->GetNativePtr(0); surface_ = vkInstance_.createWin32SurfaceKHR(surfaceCreateInfo); +#elif defined(__APPLE__) + vk::MacOSSurfaceCreateInfoMVK surfaceCreateInfo; + surfaceCreateInfo.pView = window->GetNativePtr(1); + surface_ = vkInstance_.createMacOSSurfaceMVK(surfaceCreateInfo); #else vk::XcbSurfaceCreateInfoKHR surfaceCreateInfo; surfaceCreateInfo.connection = XGetXCBConnection((Display*)window->GetNativePtr(0)); @@ -843,7 +863,8 @@ void PlatformVulkan::SetWindowSize(const Vec2I& windowSize) Graphics* PlatformVulkan::CreateGraphics() { - auto addCommand = [this](vk::CommandBuffer commandBuffer, vk::Fence fence) -> void { + auto addCommand = [this](vk::CommandBuffer commandBuffer, vk::Fence fence) -> void + { std::array copySubmitInfos; copySubmitInfos[0].commandBufferCount = 1; copySubmitInfos[0].pCommandBuffers = &commandBuffer; diff --git a/src/Vulkan/LLGI.PlatformVulkan.h b/src/Vulkan/LLGI.PlatformVulkan.h index a029eb44..abfbe2aa 100644 --- a/src/Vulkan/LLGI.PlatformVulkan.h +++ b/src/Vulkan/LLGI.PlatformVulkan.h @@ -7,6 +7,8 @@ #ifdef _WIN32 #include "../Win/LLGI.WindowWin.h" +#elif defined(__APPLE__) +#include "../Mac/LLGI.WindowMac.h" #else #include "../Linux/LLGI.WindowLinux.h" #endif From 200e58c7e984cc8ea2d3cdf511a83818aa9bcf52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=AE=87?= <63290381+xiaoyv404@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:30:37 +0800 Subject: [PATCH 06/16] Adding bc7 compresstion support (#2) * adding bc7 compresstion support * Applying suggestion from copilot * use std::size for iterator * add bc1,2,3,7 support in GetTextureMemorySize * revert unrelated changes * add GetTextureRowPitch and GetTextureRowCount for better mem resize * add R16_FLOAT R32_FLOAT R32G32_FLOAT and case return vaule explicitly --- examples/thirdparty/glfw | 2 +- src/DX12/LLGI.BaseDX12.cpp | 12 ++++ src/DX12/LLGI.GraphicsDX12.cpp | 7 ++- src/DX12/LLGI.TextureDX12.cpp | 6 +- src/LLGI.Base.h | 108 ++++++++++++++++++++++++++++----- src/Metal/LLGI.Metal_Impl.mm | 18 +++++- src/Vulkan/LLGI.BaseVulkan.cpp | 7 ++- 7 files changed, 134 insertions(+), 26 deletions(-) diff --git a/examples/thirdparty/glfw b/examples/thirdparty/glfw index 7b6aead9..1bd0a55a 160000 --- a/examples/thirdparty/glfw +++ b/examples/thirdparty/glfw @@ -1 +1 @@ -Subproject commit 7b6aead9fb88b3623e3b3725ebb42670cbe4c579 +Subproject commit 1bd0a55aa76ceeda0537f7483bda1163405aa571 diff --git a/src/DX12/LLGI.BaseDX12.cpp b/src/DX12/LLGI.BaseDX12.cpp index 8e5e856e..d80e1ff6 100644 --- a/src/DX12/LLGI.BaseDX12.cpp +++ b/src/DX12/LLGI.BaseDX12.cpp @@ -179,6 +179,9 @@ DXGI_FORMAT ConvertFormat(TextureFormatType format) if (format == TextureFormatType::BC3) return DXGI_FORMAT_BC3_UNORM; + if (format == TextureFormatType::BC7) + return DXGI_FORMAT_BC7_UNORM; + if (format == TextureFormatType::BC1_SRGB) return DXGI_FORMAT_BC1_UNORM_SRGB; @@ -188,6 +191,9 @@ DXGI_FORMAT ConvertFormat(TextureFormatType format) if (format == TextureFormatType::BC3_SRGB) return DXGI_FORMAT_BC3_UNORM_SRGB; + if (format == TextureFormatType::BC7_SRGB) + return DXGI_FORMAT_BC7_UNORM_SRGB; + if (format == TextureFormatType::D32) return DXGI_FORMAT_D32_FLOAT; @@ -238,6 +244,9 @@ TextureFormatType ConvertFormat(DXGI_FORMAT format) if (format == DXGI_FORMAT_BC3_UNORM) return TextureFormatType::BC3; + if (format == DXGI_FORMAT_BC7_UNORM) + return TextureFormatType::BC7; + if (format == DXGI_FORMAT_BC1_UNORM_SRGB) return TextureFormatType::BC1_SRGB; @@ -247,6 +256,9 @@ TextureFormatType ConvertFormat(DXGI_FORMAT format) if (format == DXGI_FORMAT_BC3_UNORM_SRGB) return TextureFormatType::BC3_SRGB; + if (format == DXGI_FORMAT_BC7_UNORM_SRGB) + return TextureFormatType::BC7_SRGB; + if (format == DXGI_FORMAT_D32_FLOAT) return TextureFormatType::D32; diff --git a/src/DX12/LLGI.GraphicsDX12.cpp b/src/DX12/LLGI.GraphicsDX12.cpp index a4b76351..3a9a0fd0 100644 --- a/src/DX12/LLGI.GraphicsDX12.cpp +++ b/src/DX12/LLGI.GraphicsDX12.cpp @@ -376,11 +376,12 @@ std::vector GraphicsDX12::CaptureRenderTarget(Texture* renderTarget) if (GetTextureMemorySize(renderTarget->GetFormat(), rtSize3) != dstBuffer.GetSize()) { result.resize(GetTextureMemorySize(renderTarget->GetFormat(), rtSize3)); + const auto rowPitch = GetTextureRowPitch(renderTarget->GetFormat(), rtSize3); + const auto rowCount = GetTextureRowCount(renderTarget->GetFormat(), rtSize3); - for (int32_t y = 0; y < renderTarget->GetSizeAs2D().Y; y++) + for (int32_t y = 0; y < rowCount; y++) { - auto pitch = GetTextureMemorySize(renderTarget->GetFormat(), rtSize3) / renderTarget->GetSizeAs2D().Y; - memcpy(result.data() + pitch * y, raw + dstFootprint.RowPitch * y, pitch); + memcpy(result.data() + rowPitch * y, raw + dstFootprint.RowPitch * y, rowPitch); } } else diff --git a/src/DX12/LLGI.TextureDX12.cpp b/src/DX12/LLGI.TextureDX12.cpp index 69e11886..7f210635 100644 --- a/src/DX12/LLGI.TextureDX12.cpp +++ b/src/DX12/LLGI.TextureDX12.cpp @@ -195,7 +195,7 @@ void TextureDX12::CreateUploadReadbackBuffer() 1); assert(buffer_for_readback_ != nullptr); - if (static_cast(footprint_.Footprint.RowPitch) != cpu_memory_size_ / (texture_size_.Y * texture_size_.Z)) + if (static_cast(footprint_.Footprint.RowPitch) != GetTextureRowPitch(format_, texture_size_)) { locked_buffer_.resize(cpu_memory_size_); } @@ -222,11 +222,11 @@ void TextureDX12::Unlock() uint8_t* ptr = nullptr; buffer_for_upload_->Map(0, nullptr, (void**)&ptr); - int32_t rowCount = texture_size_.Y * texture_size_.Z; + const int32_t rowCount = GetTextureRowCount(format_, texture_size_); + const int32_t rowPitch = GetTextureRowPitch(format_, texture_size_); for (int32_t i = 0; i < rowCount; i++) { auto p = ptr + i * footprint_.Footprint.RowPitch; - auto rowPitch = cpu_memory_size_ / rowCount; memcpy(p, locked_buffer_.data() + rowPitch * i, rowPitch); } diff --git a/src/LLGI.Base.h b/src/LLGI.Base.h index f5bfa7cf..1c713f21 100644 --- a/src/LLGI.Base.h +++ b/src/LLGI.Base.h @@ -259,11 +259,13 @@ enum class TextureFormatType BC1, BC2, BC3, + BC7, R8G8B8A8_UNORM_SRGB, B8G8R8A8_UNORM_SRGB, BC1_SRGB, BC2_SRGB, BC3_SRGB, + BC7_SRGB, D32, D24S8, D32S8, @@ -509,6 +511,8 @@ inline std::string to_string(TextureFormatType format) return "BC2"; case TextureFormatType::BC3: return "BC3"; + case TextureFormatType::BC7: + return "BC7"; case TextureFormatType::R8G8B8A8_UNORM_SRGB: return "R8G8B8A8_UNORM_SRGB"; case TextureFormatType::B8G8R8A8_UNORM_SRGB: @@ -519,6 +523,8 @@ inline std::string to_string(TextureFormatType format) return "BC2_SRGB"; case TextureFormatType::BC3_SRGB: return "BC3_SRGB"; + case TextureFormatType::BC7_SRGB: + return "BC7_SRGB"; case TextureFormatType::D32: return "D32"; case TextureFormatType::D32S8: @@ -530,39 +536,109 @@ inline std::string to_string(TextureFormatType format) } } -inline int32_t GetTextureMemorySize(TextureFormatType format, Vec3I size) +inline bool IsBlockCompressedFormat(TextureFormatType format) { + switch (format) + { + case TextureFormatType::BC1: + return true; + case TextureFormatType::BC2: + return true; + case TextureFormatType::BC3: + return true; + case TextureFormatType::BC7: + return true; + case TextureFormatType::BC1_SRGB: + return true; + case TextureFormatType::BC2_SRGB: + return true; + case TextureFormatType::BC3_SRGB: + return true; + case TextureFormatType::BC7_SRGB: + return true; + default: + return false; + } +} + +inline int32_t GetTextureRowPitch(TextureFormatType format, Vec3I size) +{ + if (size.X <= 0 || size.Y <= 0 || size.Z <= 0) + return 0; + + const auto blockCountX = (size.X + 3) / 4; + switch (format) { case TextureFormatType::R8G8B8A8_UNORM: - return size.X * size.Y * size.Z * 4; + return size.X * 4; case TextureFormatType::B8G8R8A8_UNORM: - return size.X * size.Y * size.Z * 4; - case TextureFormatType::R8_UNORM: - return size.X * size.Y * size.Z * 1; + return size.X * 4; case TextureFormatType::R16G16_FLOAT: - return size.X * size.Y * size.Z * 4; - case TextureFormatType::R16G16B16A16_FLOAT: - return size.X * size.Y * size.Z * 8; - case TextureFormatType::R32G32B32A32_FLOAT: - return size.X * size.Y * size.Z * 16; + return size.X * 4; case TextureFormatType::R8G8B8A8_UNORM_SRGB: - return size.X * size.Y * size.Z * 4; + return size.X * 4; case TextureFormatType::B8G8R8A8_UNORM_SRGB: - return size.X * size.Y * size.Z * 4; + return size.X * 4; case TextureFormatType::D32: - return size.X * size.Y * size.Z * 4; + return size.X * 4; case TextureFormatType::D24S8: - return size.X * size.Y * size.Z * 4; + return size.X * 4; + case TextureFormatType::R8_UNORM: + return size.X; + case TextureFormatType::R16_FLOAT: + return size.X * 2; + case TextureFormatType::R32_FLOAT: + return size.X * 4; + case TextureFormatType::R32G32_FLOAT: + return size.X * 8; + case TextureFormatType::R16G16B16A16_FLOAT: + return size.X * 8; + case TextureFormatType::R32G32B32A32_FLOAT: + return size.X * 16; case TextureFormatType::D32S8: - return size.X * size.Y * size.Z * 5; + return size.X * 5; + case TextureFormatType::BC1: + return blockCountX * 8; + case TextureFormatType::BC1_SRGB: + return blockCountX * 8; + case TextureFormatType::BC2: + return blockCountX * 16; + case TextureFormatType::BC3: + return blockCountX * 16; + case TextureFormatType::BC7: + return blockCountX * 16; + case TextureFormatType::BC2_SRGB: + return blockCountX * 16; + case TextureFormatType::BC3_SRGB: + return blockCountX * 16; + case TextureFormatType::BC7_SRGB: + return blockCountX * 16; default: auto str = to_string(format); - Log(LogType::Error, str + " : GetTextureMemorySize is not supported"); + Log(LogType::Error, str + " : GetTextureRowPitch is not supported"); return 0; } } +inline int32_t GetTextureRowCount(TextureFormatType format, Vec3I size) +{ + if (size.Y <= 0 || size.Z <= 0) + return 0; + + if (IsBlockCompressedFormat(format)) + { + return ((size.Y + 3) / 4) * size.Z; + } + + return size.Y * size.Z; +} + +inline int32_t GetTextureMemorySize(TextureFormatType format, Vec3I size) +{ + return GetTextureRowPitch(format, size) * GetTextureRowCount(format, size); +} + inline uint32_t GetMaximumMipLevels(const Vec2I& size) { // (std::max) HACK for MSVC diff --git a/src/Metal/LLGI.Metal_Impl.mm b/src/Metal/LLGI.Metal_Impl.mm index fc4888dc..a2984025 100644 --- a/src/Metal/LLGI.Metal_Impl.mm +++ b/src/Metal/LLGI.Metal_Impl.mm @@ -79,6 +79,14 @@ MTLPixelFormat ConvertFormat(TextureFormatType format) { return MTLPixelFormatBC3_RGBA_sRGB; } + else if (format == TextureFormatType::BC7) + { + return MTLPixelFormatBC7_RGBAUnorm; + } + else if (format == TextureFormatType::BC7_SRGB) + { + return MTLPixelFormatBC7_RGBAUnorm_sRGB; + } else if (format == TextureFormatType::D24S8) { return MTLPixelFormatDepth24Unorm_Stencil8; @@ -155,6 +163,14 @@ TextureFormatType ConvertFormat(MTLPixelFormat format) { return TextureFormatType::BC3_SRGB; } + else if (format == MTLPixelFormatBC7_RGBAUnorm) + { + return TextureFormatType::BC7; + } + else if (format == MTLPixelFormatBC7_RGBAUnorm_sRGB) + { + return TextureFormatType::BC7_SRGB; + } else if (format == MTLPixelFormatDepth24Unorm_Stencil8) { return TextureFormatType::D24S8; @@ -176,4 +192,4 @@ TextureFormatType ConvertFormat(MTLPixelFormat format) return TextureFormatType::Unknown; } -} +} // namespace LLGI diff --git a/src/Vulkan/LLGI.BaseVulkan.cpp b/src/Vulkan/LLGI.BaseVulkan.cpp index f89d4506..8e888592 100644 --- a/src/Vulkan/LLGI.BaseVulkan.cpp +++ b/src/Vulkan/LLGI.BaseVulkan.cpp @@ -1,5 +1,6 @@ #include "LLGI.BaseVulkan.h" #include "LLGI.GraphicsVulkan.h" +#include namespace LLGI { @@ -70,11 +71,13 @@ static FormatConversionItem s_formatConversionTable[] = { {TextureFormatType::BC1, VK_FORMAT_BC1_RGBA_UNORM_BLOCK}, {TextureFormatType::BC2, VK_FORMAT_BC2_UNORM_BLOCK}, {TextureFormatType::BC3, VK_FORMAT_BC3_UNORM_BLOCK}, + {TextureFormatType::BC7, VK_FORMAT_BC7_UNORM_BLOCK}, {TextureFormatType::R8G8B8A8_UNORM_SRGB, VK_FORMAT_R8G8B8A8_SRGB}, {TextureFormatType::B8G8R8A8_UNORM_SRGB, VK_FORMAT_B8G8R8A8_SRGB}, {TextureFormatType::BC1_SRGB, VK_FORMAT_BC1_RGBA_SRGB_BLOCK}, {TextureFormatType::BC2_SRGB, VK_FORMAT_BC2_SRGB_BLOCK}, {TextureFormatType::BC3_SRGB, VK_FORMAT_BC3_SRGB_BLOCK}, + {TextureFormatType::BC7_SRGB, VK_FORMAT_BC7_SRGB_BLOCK}, {TextureFormatType::D32, VK_FORMAT_D32_SFLOAT}, {TextureFormatType::D24S8, VK_FORMAT_D24_UNORM_S8_UINT}, {TextureFormatType::D32S8, VK_FORMAT_D32_SFLOAT_S8_UINT}, @@ -83,7 +86,7 @@ static FormatConversionItem s_formatConversionTable[] = { VkFormat VulkanHelper::TextureFormatToVkFormat(TextureFormatType format) { - for (size_t i = 0; i < sizeof(s_formatConversionTable); i++) + for (size_t i = 0; i < std::size(s_formatConversionTable); i++) { if (s_formatConversionTable[i].format == format) return s_formatConversionTable[i].vulkanFormat; @@ -97,7 +100,7 @@ VkFormat VulkanHelper::TextureFormatToVkFormat(TextureFormatType format) TextureFormatType VulkanHelper::VkFormatToTextureFormat(VkFormat format) { - for (size_t i = 0; i < sizeof(s_formatConversionTable); i++) + for (size_t i = 0; i < std::size(s_formatConversionTable); i++) { if (s_formatConversionTable[i].vulkanFormat == format) return s_formatConversionTable[i].format; From eb48b9bdd17a3879dec13b12b487c56cff953d2e Mon Sep 17 00:00:00 2001 From: swd Date: Sat, 2 May 2026 14:20:40 +0900 Subject: [PATCH 07/16] Support Dawn (#3) Support Dawn --- .gitignore | 1 + CMakeLists.txt | 43 +- README.md | 8 + docs/WebGPU.md | 147 +++++++ scripts/fetch_dawn.py | 80 ++++ scripts/transpile.py | 23 +- src/CMakeLists.txt | 57 ++- src/LLGI.Base.h | 1 + src/PC/LLGI.CreatePC.cpp | 34 +- src/WebGPU/LLGI.BaseWebGPU.cpp | 338 ++++++++++++++ src/WebGPU/LLGI.BaseWebGPU.h | 33 ++ src/WebGPU/LLGI.BufferWebGPU.cpp | 127 ++++++ src/WebGPU/LLGI.BufferWebGPU.h | 37 ++ src/WebGPU/LLGI.CommandListWebGPU.cpp | 412 ++++++++++++++++++ src/WebGPU/LLGI.CommandListWebGPU.h | 51 +++ src/WebGPU/LLGI.CompilerWebGPU.cpp | 88 ++++ src/WebGPU/LLGI.CompilerWebGPU.h | 17 + src/WebGPU/LLGI.GraphicsWebGPU.cpp | 360 +++++++++++++++ src/WebGPU/LLGI.GraphicsWebGPU.h | 67 +++ src/WebGPU/LLGI.PipelineStateWebGPU.cpp | 159 +++++++ src/WebGPU/LLGI.PipelineStateWebGPU.h | 30 ++ src/WebGPU/LLGI.PlatformWebGPU.cpp | 335 ++++++++++++++ src/WebGPU/LLGI.PlatformWebGPU.h | 52 +++ .../LLGI.RenderPassPipelineStateWebGPU.cpp | 19 + .../LLGI.RenderPassPipelineStateWebGPU.h | 21 + src/WebGPU/LLGI.RenderPassWebGPU.cpp | 128 ++++++ src/WebGPU/LLGI.RenderPassWebGPU.h | 23 + src/WebGPU/LLGI.ShaderWebGPU.cpp | 66 +++ src/WebGPU/LLGI.ShaderWebGPU.h | 23 + src/WebGPU/LLGI.TextureWebGPU.cpp | 264 +++++++++++ src/WebGPU/LLGI.TextureWebGPU.h | 37 ++ src_test/Shaders/WebGPU/basic.comp | 42 ++ src_test/Shaders/WebGPU/instancing.vert | 62 +++ src_test/Shaders/WebGPU/readwrite.comp | 42 ++ .../Shaders/WebGPU/readwrite_texture.comp | 26 ++ .../WebGPU/simple_compute_rectangle.frag | 46 ++ .../WebGPU/simple_compute_rectangle.vert | 64 +++ .../WebGPU/simple_constant_rectangle.frag | 38 ++ .../WebGPU/simple_constant_rectangle.vert | 63 +++ .../WebGPU/simple_mrt_texture_rectangle.frag | 59 +++ src_test/Shaders/WebGPU/simple_rectangle.frag | 27 ++ src_test/Shaders/WebGPU/simple_rectangle.vert | 50 +++ .../WebGPU/simple_texture_rectangle.frag | 36 ++ .../WebGPU/simple_texture_rectangle.vert | 57 +++ src_test/Shaders/WebGPU/textures.frag | 47 ++ .../Shaders/WebGPU/vertex_structured.vert | 67 +++ src_test/Shaders/WebGPU/vtf.vert | 60 +++ src_test/Shaders/WebGPU_Compiled/basic.comp | Bin 0 -> 799 bytes .../Shaders/WebGPU_Compiled/instancing.vert | Bin 0 -> 1525 bytes .../Shaders/WebGPU_Compiled/readwrite.comp | Bin 0 -> 793 bytes .../WebGPU_Compiled/readwrite_texture.comp | Bin 0 -> 776 bytes .../simple_compute_rectangle.frag | Bin 0 -> 965 bytes .../simple_compute_rectangle.vert | Bin 0 -> 1399 bytes .../simple_constant_rectangle.frag | Bin 0 -> 799 bytes .../simple_constant_rectangle.vert | Bin 0 -> 1333 bytes .../simple_mrt_texture_rectangle.frag | Bin 0 -> 1244 bytes .../WebGPU_Compiled/simple_rectangle.frag | Bin 0 -> 576 bytes .../WebGPU_Compiled/simple_rectangle.vert | Bin 0 -> 1115 bytes .../simple_texture_rectangle.frag | Bin 0 -> 815 bytes .../simple_texture_rectangle.vert | Bin 0 -> 1231 bytes .../Shaders/WebGPU_Compiled/textures.frag | Bin 0 -> 1292 bytes .../WebGPU_Compiled/vertex_structured.vert | Bin 0 -> 1587 bytes src_test/Shaders/WebGPU_Compiled/vtf.vert | Bin 0 -> 1413 bytes src_test/TestHelper.cpp | 13 + src_test/main.cpp | 4 + src_test/test_compute_shader.cpp | 15 +- src_test/test_mipmap.cpp | 2 +- src_test/test_simple_render.cpp | 4 +- src_test/test_textures.cpp | 2 +- tools/CMakeLists.txt | 4 + tools/ShaderTranspiler/CMakeLists.txt | 6 +- tools/ShaderTranspiler/main.cpp | 54 ++- tools/ShaderTranspilerCore/CMakeLists.txt | 13 +- .../ShaderTranspilerCore.cpp | 109 ++++- .../ShaderTranspilerCore.h | 11 +- 75 files changed, 4062 insertions(+), 42 deletions(-) create mode 100644 docs/WebGPU.md create mode 100644 scripts/fetch_dawn.py create mode 100644 src/WebGPU/LLGI.BaseWebGPU.cpp create mode 100644 src/WebGPU/LLGI.BaseWebGPU.h create mode 100644 src/WebGPU/LLGI.BufferWebGPU.cpp create mode 100644 src/WebGPU/LLGI.BufferWebGPU.h create mode 100644 src/WebGPU/LLGI.CommandListWebGPU.cpp create mode 100644 src/WebGPU/LLGI.CommandListWebGPU.h create mode 100644 src/WebGPU/LLGI.CompilerWebGPU.cpp create mode 100644 src/WebGPU/LLGI.CompilerWebGPU.h create mode 100644 src/WebGPU/LLGI.GraphicsWebGPU.cpp create mode 100644 src/WebGPU/LLGI.GraphicsWebGPU.h create mode 100644 src/WebGPU/LLGI.PipelineStateWebGPU.cpp create mode 100644 src/WebGPU/LLGI.PipelineStateWebGPU.h create mode 100644 src/WebGPU/LLGI.PlatformWebGPU.cpp create mode 100644 src/WebGPU/LLGI.PlatformWebGPU.h create mode 100644 src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.cpp create mode 100644 src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.h create mode 100644 src/WebGPU/LLGI.RenderPassWebGPU.cpp create mode 100644 src/WebGPU/LLGI.RenderPassWebGPU.h create mode 100644 src/WebGPU/LLGI.ShaderWebGPU.cpp create mode 100644 src/WebGPU/LLGI.ShaderWebGPU.h create mode 100644 src/WebGPU/LLGI.TextureWebGPU.cpp create mode 100644 src/WebGPU/LLGI.TextureWebGPU.h create mode 100644 src_test/Shaders/WebGPU/basic.comp create mode 100644 src_test/Shaders/WebGPU/instancing.vert create mode 100644 src_test/Shaders/WebGPU/readwrite.comp create mode 100644 src_test/Shaders/WebGPU/readwrite_texture.comp create mode 100644 src_test/Shaders/WebGPU/simple_compute_rectangle.frag create mode 100644 src_test/Shaders/WebGPU/simple_compute_rectangle.vert create mode 100644 src_test/Shaders/WebGPU/simple_constant_rectangle.frag create mode 100644 src_test/Shaders/WebGPU/simple_constant_rectangle.vert create mode 100644 src_test/Shaders/WebGPU/simple_mrt_texture_rectangle.frag create mode 100644 src_test/Shaders/WebGPU/simple_rectangle.frag create mode 100644 src_test/Shaders/WebGPU/simple_rectangle.vert create mode 100644 src_test/Shaders/WebGPU/simple_texture_rectangle.frag create mode 100644 src_test/Shaders/WebGPU/simple_texture_rectangle.vert create mode 100644 src_test/Shaders/WebGPU/textures.frag create mode 100644 src_test/Shaders/WebGPU/vertex_structured.vert create mode 100644 src_test/Shaders/WebGPU/vtf.vert create mode 100644 src_test/Shaders/WebGPU_Compiled/basic.comp create mode 100644 src_test/Shaders/WebGPU_Compiled/instancing.vert create mode 100644 src_test/Shaders/WebGPU_Compiled/readwrite.comp create mode 100644 src_test/Shaders/WebGPU_Compiled/readwrite_texture.comp create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_compute_rectangle.frag create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_compute_rectangle.vert create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_constant_rectangle.frag create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_constant_rectangle.vert create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_mrt_texture_rectangle.frag create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_rectangle.frag create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_rectangle.vert create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_texture_rectangle.frag create mode 100644 src_test/Shaders/WebGPU_Compiled/simple_texture_rectangle.vert create mode 100644 src_test/Shaders/WebGPU_Compiled/textures.frag create mode 100644 src_test/Shaders/WebGPU_Compiled/vertex_structured.vert create mode 100644 src_test/Shaders/WebGPU_Compiled/vtf.vert diff --git a/.gitignore b/.gitignore index 15a7a582..618aab6e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,6 @@ **/Debug /build /build_clangformat +/thirdparty/dawn/ .DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 5722d944..b2f8279e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,14 @@ if(UNIX AND NOT APPLE) set(LINUX TRUE) endif() +option(BUILD_WEBGPU "build webgpu backend" OFF) +set(WEBGPU_DAWN_SOURCE_DIR + "" + CACHE PATH "Path to a Dawn source checkout used when BUILD_WEBGPU is ON") +option(WEBGPU_DAWN_BUILD_SAMPLES "Build Dawn sample executables" OFF) +option(WEBGPU_DAWN_FORCE_SYSTEM_COMPONENT_LOAD + "Let Dawn load required Windows system components from System32" + ON) option(BUILD_VULKAN "build vulkan" OFF) option(BUILD_VULKAN_COMPILER "build vulkan compiler" OFF) option(BUILD_TEST "build test" OFF) @@ -27,7 +35,7 @@ option(SPIRVCROSS_WITHOUT_INSTALL "Compile with spirv-cross without install" OFF) option(USE_CREATE_COMPILER_FUNCTION "Whether LLGI::CreateCompiler is used." ON) -if(LINUX) +if(LINUX AND NOT BUILD_WEBGPU) set(BUILD_VULKAN TRUE) endif() @@ -246,6 +254,39 @@ if(BUILD_VULKAN) endif() endif() +if(BUILD_WEBGPU) + set(DAWN_FETCH_DEPENDENCIES + OFF + CACHE BOOL + "Dawn dependencies should be fetched before configuring LLGI" + FORCE) + set(DAWN_BUILD_SAMPLES ${WEBGPU_DAWN_BUILD_SAMPLES} + CACHE BOOL "Build Dawn samples" FORCE) + set(DAWN_FORCE_SYSTEM_COMPONENT_LOAD + ${WEBGPU_DAWN_FORCE_SYSTEM_COMPONENT_LOAD} + CACHE BOOL "Allow Dawn to load Windows system components from System32" + FORCE) + set(DAWN_BUILD_TESTS OFF CACHE BOOL "Build Dawn tests" FORCE) + set(DAWN_ENABLE_INSTALL OFF CACHE BOOL "Install Dawn targets" FORCE) + + set(LLGI_DAWN_SOURCE_DIR "") + if(WEBGPU_DAWN_SOURCE_DIR) + set(LLGI_DAWN_SOURCE_DIR "${WEBGPU_DAWN_SOURCE_DIR}") + elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/dawn/CMakeLists.txt") + set(LLGI_DAWN_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/dawn") + endif() + + if(LLGI_DAWN_SOURCE_DIR) + add_subdirectory("${LLGI_DAWN_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/dawn" + EXCLUDE_FROM_ALL) + else() + message( + FATAL_ERROR + "Dawn source was not found. Run `python scripts/fetch_dawn.py`, add thirdparty/dawn, or set WEBGPU_DAWN_SOURCE_DIR to a prepared Dawn checkout." + ) + endif() +endif() + if(APPLE) add_compile_definitions(ENABLE_METAL) endif() diff --git a/README.md b/README.md index 50cbb870..ca7116af 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ sudo apt-get install -y \ | `BUILD_TEST` | `OFF` | Build `LLGI_Test` | | `BUILD_EXAMPLE` | `OFF` | Build sample applications | | `BUILD_TOOL` | `OFF` | Build `ShaderTranspiler` | +| `BUILD_WEBGPU` | `OFF` | Enable the experimental WebGPU backend. See [docs/WebGPU.md](docs/WebGPU.md). | | `BUILD_VULKAN` | `OFF` (`ON` on Linux) | Enable the Vulkan backend | | `BUILD_VULKAN_COMPILER` | `OFF` | Enable Vulkan shader compilation support in `LLGI::CreateCompiler` | | `USE_CREATE_COMPILER_FUNCTION` | `ON` | Keep `LLGI::CreateCompiler` enabled | @@ -139,6 +140,12 @@ cmake --build build On Linux, `BUILD_VULKAN` is enabled automatically by the top-level `CMakeLists.txt`. +### WebGPU (experimental) + +The WebGPU backend uses Dawn and is still experimental. Build instructions, +Dawn setup, test commands, shader notes, and current limitations are documented +in [docs/WebGPU.md](docs/WebGPU.md). + ## Install ```bash @@ -172,6 +179,7 @@ Examples: build\src_test\Release\LLGI_Test.exe build\src_test\Release\LLGI_Test.exe --filter=Compile.* build\src_test\Release\LLGI_Test.exe --vulkan +build\src_test\Release\LLGI_Test.exe --webgpu ``` If you want Vulkan shader compilation through diff --git a/docs/WebGPU.md b/docs/WebGPU.md new file mode 100644 index 00000000..bcf4baa5 --- /dev/null +++ b/docs/WebGPU.md @@ -0,0 +1,147 @@ +# WebGPU Backend + +The WebGPU backend is experimental. It uses Dawn as the native WebGPU +implementation and WGSL as the shader source format. + +## Requirements + +- CMake 3.15 or newer +- A C++17-capable toolchain +- `BUILD_WEBGPU=ON` +- Dawn, prepared with `scripts/fetch_dawn.py` or supplied as an existing checkout +- `BUILD_TOOL=ON` when rebuilding WGSL shader assets with `ShaderTranspiler` + +On Windows, Dawn normally uses D3D12. Vulkan or other Dawn adapters may be +available depending on the local Dawn build and runtime environment. + +## CMake Options + +| Option | Default | Description | +| --- | --- | --- | +| `BUILD_WEBGPU` | `OFF` | Enable the WebGPU backend | +| `WEBGPU_DAWN_SOURCE_DIR` | empty | Use an existing Dawn checkout | +| `WEBGPU_DAWN_BUILD_SAMPLES` | `OFF` | Build Dawn sample executables | +| `WEBGPU_DAWN_FORCE_SYSTEM_COMPONENT_LOAD` | `ON` | Let Dawn load Windows system components such as `d3dcompiler_47.dll` from System32 | + +## Getting Dawn + +### Recommended Flow + +Dawn should be fetched before configuring LLGI. The helper script clones Dawn +into `thirdparty/dawn` and runs Dawn's dependency fetch script: + +```bash +python scripts/fetch_dawn.py +``` + +Pin a Dawn revision for reproducible builds: + +```bash +python scripts/fetch_dawn.py --revision +``` + +Then configure LLGI: + +```bash +cmake -S . -B build-webgpu \ + -DBUILD_WEBGPU=ON \ + -DBUILD_TEST=ON \ + -DBUILD_TOOL=ON +cmake --build build-webgpu --config Release +``` + +### Existing Dawn Checkout + +You can use a separate Dawn checkout if its dependencies have already been +fetched: + +```bash +cmake -S . -B build-webgpu \ + -DBUILD_WEBGPU=ON \ + -DBUILD_TEST=ON \ + -DBUILD_TOOL=ON \ + -DWEBGPU_DAWN_SOURCE_DIR=/path/to/dawn +cmake --build build-webgpu --config Release +``` + +### `thirdparty/dawn` + +The repository checks `thirdparty/dawn` automatically. This is useful when you +want Dawn to live inside the LLGI working tree. Prefer the helper script above, +or run Dawn's official setup manually: + +```bash +cd thirdparty +git clone https://dawn.googlesource.com/dawn dawn +cd dawn +cp scripts/standalone.gclient .gclient +gclient sync +``` + +Then configure LLGI with `-DBUILD_WEBGPU=ON`. + +## Running Tests + +Build `LLGI_Test` with `BUILD_TEST=ON`, then pass `--webgpu`: + +```bash +# Windows +build-webgpu\src_test\Release\LLGI_Test.exe --webgpu +build-webgpu\src_test\Release\LLGI_Test.exe --webgpu --filter=SimpleRender.* + +# Linux / macOS +./build-webgpu/src_test/LLGI_Test --webgpu +./build-webgpu/src_test/LLGI_Test --webgpu --filter=SimpleRender.* +``` + +Some environments need a visible GPU session for Dawn to create a WebGPU device. +Headless CI may need Dawn-specific setup. + +## Shader Generation + +`ShaderTranspiler` can emit WGSL and compiled WGSL blobs for WebGPU tests. + +```bash +cmake -S . -B build-webgpu \ + -DBUILD_TOOL=ON \ + -DBUILD_WEBGPU=ON +cmake --build build-webgpu --config Release --target ShaderTranspiler + +python scripts/transpile.py src_test/Shaders/ +``` + +Generated WebGPU shaders are stored under: + +- `src_test/Shaders/WebGPU/` +- `src_test/Shaders/WebGPU_Compiled/` + +The compiled files use an LLGI header followed by WGSL text. Runtime WebGPU +shader creation expects WGSL or this compiled WGSL format; legacy runtime WGSL +rewrites are not applied. + +## Current Limitations + +- The backend is experimental and is not enabled by default. +- Dawn API and Tint WGSL output can change. Prefer pinning + `scripts/fetch_dawn.py --revision` for stable builds. +- The WebGPU backend currently depends on Dawn CMake targets + `dawn::webgpu_dawn` or `webgpu_dawn`. +- WebGPU shader assets should be regenerated when the shader transpiler or Tint + revision changes. +- `LLGI::CreateCompiler(DeviceType::WebGPU)` is not a runtime HLSL compiler; use + `ShaderTranspiler` to generate WGSL assets. +- External-device construction APIs can run without an owned `wgpu::Instance`. + In that mode, some wait processing is limited to the supplied Dawn objects. + +## Troubleshooting + +- If configure fails because Dawn is missing, run `python scripts/fetch_dawn.py`, + add `thirdparty/dawn`, or set `WEBGPU_DAWN_SOURCE_DIR`. +- If Dawn dependency sync fails, install `depot_tools` and ensure `gclient` is + available on `PATH`, or use a Dawn checkout whose dependencies are already + synced. +- If WebGPU device creation fails on Windows with `d3dcompiler_47.dll`, rebuild + with `WEBGPU_DAWN_FORCE_SYSTEM_COMPONENT_LOAD=ON`. +- If shader creation fails, regenerate WGSL with the current `ShaderTranspiler` + and check that the generated shaders are under `src_test/Shaders/WebGPU/` and + `src_test/Shaders/WebGPU_Compiled/`. diff --git a/scripts/fetch_dawn.py b/scripts/fetch_dawn.py new file mode 100644 index 00000000..203953cd --- /dev/null +++ b/scripts/fetch_dawn.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +import argparse +import os +import subprocess +import sys + + +def run(args, cwd=None): + print("+ " + " ".join(args)) + subprocess.check_call(args, cwd=cwd) + + +def is_dawn_checkout(path): + return os.path.isfile(os.path.join(path, "CMakeLists.txt")) and os.path.isdir( + os.path.join(path, "src", "dawn") + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Fetch Dawn for the LLGI WebGPU backend." + ) + parser.add_argument( + "-d", + "--directory", + default=os.path.join("thirdparty", "dawn"), + help="Dawn checkout directory. Default: thirdparty/dawn", + ) + parser.add_argument( + "--repository", + default="https://dawn.googlesource.com/dawn", + help="Dawn git repository URL.", + ) + parser.add_argument( + "--revision", + default="main", + help="Dawn branch, tag, or commit to checkout. Default: main", + ) + parser.add_argument( + "--skip-dependencies", + action="store_true", + help="Only clone/update Dawn; do not run Dawn dependency fetch.", + ) + args = parser.parse_args() + + dawn_dir = os.path.abspath(args.directory) + + if os.path.exists(dawn_dir): + if not is_dawn_checkout(dawn_dir): + print( + f"error: {dawn_dir} exists but does not look like a Dawn checkout", + file=sys.stderr, + ) + return 1 + run(["git", "fetch", "--tags", "origin"], cwd=dawn_dir) + else: + parent = os.path.dirname(dawn_dir) + if parent: + os.makedirs(parent, exist_ok=True) + run(["git", "clone", args.repository, dawn_dir]) + + run(["git", "checkout", args.revision], cwd=dawn_dir) + + if not args.skip_dependencies: + dependency_script = os.path.join(dawn_dir, "tools", "fetch_dawn_dependencies.py") + if not os.path.isfile(dependency_script): + print( + f"error: dependency script was not found: {dependency_script}", + file=sys.stderr, + ) + return 1 + run([sys.executable, dependency_script], cwd=dawn_dir) + + print(f"Dawn is ready: {dawn_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/transpile.py b/scripts/transpile.py index 38ea685e..0b2dbf9c 100644 --- a/scripts/transpile.py +++ b/scripts/transpile.py @@ -23,9 +23,7 @@ elif os.path.isfile(transpiler_path_make): shutil.copy(transpiler_path_make, "./") -transpiler_call = 'ShaderTranspiler' -if platform.system() == 'Linux': - transpiler_call = './ShaderTranspiler' +transpiler_call = os.path.join('.', transpiler_filename) verts = glob.glob(os.path.join(target_directory, 'HLSL_DX12/*.vert'), recursive=True) frags = glob.glob(os.path.join(target_directory, 'HLSL_DX12/*.frag'), recursive=True) @@ -44,6 +42,25 @@ os.makedirs(os.path.join(target_directory, directory), exist_ok=True) subprocess.call([transpiler_call, kind, target, '--input', f, '--output', os.path.join(target_directory, directory, os.path.basename(f))] + ext) +for kind,paths in [ + ('--vert', verts), + ('--frag', frags), + ('--comp', comps) ]: + for f in paths: + os.makedirs(os.path.join(target_directory, 'WebGPU'), exist_ok=True) + os.makedirs(os.path.join(target_directory, 'WebGPU_Compiled'), exist_ok=True) + subprocess.call([ + transpiler_call, + kind, + '-W', + '--input', + f, + '--output', + os.path.join(target_directory, 'WebGPU', os.path.basename(f)), + '--compiled-output', + os.path.join(target_directory, 'WebGPU_Compiled', os.path.basename(f)) + ]) + verts = glob.glob(os.path.join(target_directory, 'GLSL_VULKAN/*.vert'), recursive=True) frags = glob.glob(os.path.join(target_directory, 'GLSL_VULKAN/*.frag'), recursive=True) comps = glob.glob(os.path.join(target_directory, 'GLSL_VULKAN/*.comp'), recursive=True) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3c9d8305..26b328d1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,7 +17,7 @@ else() list(APPEND files ${files_linux}) endif() -if(MSVC) +if(MSVC AND NOT BUILD_WEBGPU) file(GLOB files_dx12 DX12/*.h DX12/*.cpp) list(APPEND files ${files_dx12}) endif() @@ -28,6 +28,12 @@ if(BUILD_VULKAN) add_definitions(-DENABLE_VULKAN) endif() +if(BUILD_WEBGPU) + file(GLOB files_webgpu WebGPU/*.h WebGPU/*.cpp) + list(APPEND files ${files_webgpu}) + add_definitions(-DENABLE_WEBGPU) +endif() + if(APPLE) file(GLOB files_metal Metal/*.h Metal/*.cpp Metal/*.mm) list(APPEND files ${files_metal}) @@ -67,10 +73,12 @@ add_library(LLGI STATIC ${files}) file(GLOB LOCAL_HEADERS *.h) set_target_properties(LLGI PROPERTIES PUBLIC_HEADER "${LOCAL_HEADERS}") -if(BUILD_VULKAN) -target_compile_features(LLGI PUBLIC cxx_std_17) +if(BUILD_WEBGPU) + target_compile_features(LLGI PUBLIC cxx_std_20) +elseif(BUILD_VULKAN) + target_compile_features(LLGI PUBLIC cxx_std_17) else() -target_compile_features(LLGI PUBLIC cxx_std_14) + target_compile_features(LLGI PUBLIC cxx_std_14) endif() if(BUILD_VULKAN) @@ -84,6 +92,16 @@ if(BUILD_VULKAN) endif() endif() +if(BUILD_WEBGPU) + if(TARGET dawn::webgpu_dawn) + target_link_libraries(LLGI PRIVATE dawn::webgpu_dawn) + elseif(TARGET webgpu_dawn) + target_link_libraries(LLGI PRIVATE webgpu_dawn) + else() + message(FATAL_ERROR "Dawn targets were not found. Expected dawn::webgpu_dawn or webgpu_dawn.") + endif() +endif() + if(WIN32) elseif(APPLE) @@ -101,19 +119,22 @@ endif() # -------------------- # Install -install( - TARGETS LLGI - EXPORT LLGI-export - INCLUDES - DESTINATION include/LLGI - PUBLIC_HEADER DESTINATION include/LLGI - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib) - -install( - EXPORT LLGI-export - FILE LLGI-config.cmake - DESTINATION lib/cmake - EXPORT_LINK_INTERFACE_LIBRARIES) +# Because dawn doesn't specify export +if(NOT BUILD_WEBGPU) + install( + TARGETS LLGI + EXPORT LLGI-export + INCLUDES + DESTINATION include/LLGI + PUBLIC_HEADER DESTINATION include/LLGI + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib) + + install( + EXPORT LLGI-export + FILE LLGI-config.cmake + DESTINATION lib/cmake + EXPORT_LINK_INTERFACE_LIBRARIES) +endif() clang_format(LLGI) diff --git a/src/LLGI.Base.h b/src/LLGI.Base.h index 1c713f21..654b8d73 100644 --- a/src/LLGI.Base.h +++ b/src/LLGI.Base.h @@ -28,6 +28,7 @@ enum class DeviceType DirectX12, Metal, Vulkan, + WebGPU, }; enum class ErrorCode diff --git a/src/PC/LLGI.CreatePC.cpp b/src/PC/LLGI.CreatePC.cpp index e0a72253..e5d6efae 100644 --- a/src/PC/LLGI.CreatePC.cpp +++ b/src/PC/LLGI.CreatePC.cpp @@ -6,10 +6,12 @@ #include "../Vulkan/LLGI.PlatformVulkan.h" #endif -#ifdef _WIN32 +#if defined(_WIN32) && !defined(ENABLE_WEBGPU) #include "../DX12/LLGI.CompilerDX12.h" #include "../DX12/LLGI.PlatformDX12.h" #include "../Win/LLGI.WindowWin.h" +#elif defined(_WIN32) +#include "../Win/LLGI.WindowWin.h" #endif #ifdef __APPLE__ @@ -22,6 +24,11 @@ #include "../Vulkan/LLGI.CompilerVulkan.h" #endif +#ifdef ENABLE_WEBGPU +#include "../WebGPU/LLGI.CompilerWebGPU.h" +#include "../WebGPU/LLGI.PlatformWebGPU.h" +#endif + #ifdef __linux__ #include "../Linux/LLGI.WindowLinux.h" #endif @@ -65,6 +72,19 @@ Platform* CreatePlatform(const PlatformParameter& parameter, Window* window) windowSize.X = 1280; windowSize.Y = 720; +#ifdef ENABLE_WEBGPU + if (parameter.Device == DeviceType::WebGPU) + { + auto platform = new PlatformWebGPU(); + if (!platform->Initialize(window, parameter.WaitVSync)) + { + SafeRelease(platform); + return nullptr; + } + return platform; + } +#endif + #ifdef ENABLE_VULKAN #if defined(__linux__) if (parameter.Device == DeviceType::Vulkan || parameter.Device == DeviceType::Default) @@ -82,7 +102,7 @@ Platform* CreatePlatform(const PlatformParameter& parameter, Window* window) } #endif -#ifdef _WIN32 +#if defined(_WIN32) && !defined(ENABLE_WEBGPU) if (parameter.Device == DeviceType::Default || parameter.Device == DeviceType::DirectX12) { @@ -109,7 +129,7 @@ Compiler* CreateCompiler(DeviceType device) { #ifdef ENABLE_CREATE_COMPILER -#ifdef _WIN32 +#if defined(_WIN32) && !defined(ENABLE_WEBGPU) if (device == DeviceType::Default || device == DeviceType::DirectX12) { auto obj = new CompilerDX12(); @@ -129,6 +149,14 @@ Compiler* CreateCompiler(DeviceType device) } #endif +#ifdef ENABLE_WEBGPU + if (device == DeviceType::WebGPU) + { + auto obj = new CompilerWebGPU(); + return obj; + } +#endif + #ifdef __APPLE__ auto obj = new CompilerMetal(); return obj; diff --git a/src/WebGPU/LLGI.BaseWebGPU.cpp b/src/WebGPU/LLGI.BaseWebGPU.cpp new file mode 100644 index 00000000..2e9a3417 --- /dev/null +++ b/src/WebGPU/LLGI.BaseWebGPU.cpp @@ -0,0 +1,338 @@ +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ + +wgpu::BlendOperation Convert(BlendEquationType type) +{ + if (type == BlendEquationType::Add) + return wgpu::BlendOperation::Add; + if (type == BlendEquationType::Max) + return wgpu::BlendOperation::Max; + if (type == BlendEquationType::Min) + return wgpu::BlendOperation::Min; + if (type == BlendEquationType::ReverseSub) + return wgpu::BlendOperation::ReverseSubtract; + if (type == BlendEquationType::Sub) + return wgpu::BlendOperation::Subtract; + + throw "Not implemented"; +} + +wgpu::BlendFactor Convert(BlendFuncType type) +{ + if (type == BlendFuncType::Zero) + return wgpu::BlendFactor::Zero; + if (type == BlendFuncType::One) + return wgpu::BlendFactor::One; + if (type == BlendFuncType::SrcColor) + return wgpu::BlendFactor::Src; + if (type == BlendFuncType::OneMinusSrcColor) + return wgpu::BlendFactor::OneMinusSrc; + if (type == BlendFuncType::SrcAlpha) + return wgpu::BlendFactor::SrcAlpha; + if (type == BlendFuncType::OneMinusSrcAlpha) + return wgpu::BlendFactor::OneMinusSrcAlpha; + if (type == BlendFuncType::DstAlpha) + return wgpu::BlendFactor::DstAlpha; + if (type == BlendFuncType::OneMinusDstAlpha) + return wgpu::BlendFactor::OneMinusDstAlpha; + if (type == BlendFuncType::DstColor) + return wgpu::BlendFactor::Dst; + if (type == BlendFuncType::OneMinusDstColor) + return wgpu::BlendFactor::OneMinusDst; + + throw "Not implemented"; +} + +wgpu::PrimitiveTopology Convert(TopologyType type) +{ + if (type == TopologyType::Point) + return wgpu::PrimitiveTopology::PointList; + + if (type == TopologyType::Line) + return wgpu::PrimitiveTopology::LineList; + + if (type == TopologyType::Triangle) + return wgpu::PrimitiveTopology::TriangleList; + + throw "Not implemented"; +} + +wgpu::CompareFunction Convert(CompareFuncType type) +{ + if (type == CompareFuncType::Always) + return wgpu::CompareFunction::Always; + + if (type == CompareFuncType::Equal) + return wgpu::CompareFunction::Equal; + + if (type == CompareFuncType::Greater) + return wgpu::CompareFunction::Greater; + + if (type == CompareFuncType::GreaterEqual) + return wgpu::CompareFunction::GreaterEqual; + + if (type == CompareFuncType::Less) + return wgpu::CompareFunction::Less; + + if (type == CompareFuncType::LessEqual) + return wgpu::CompareFunction::LessEqual; + + if (type == CompareFuncType::Never) + return wgpu::CompareFunction::Never; + + if (type == CompareFuncType::NotEqual) + return wgpu::CompareFunction::NotEqual; + + throw "Not implemented"; +} + +wgpu::CompareFunction Convert(DepthFuncType type) +{ + if (type == DepthFuncType::Always) + return wgpu::CompareFunction::Always; + + if (type == DepthFuncType::Equal) + return wgpu::CompareFunction::Equal; + + if (type == DepthFuncType::Greater) + return wgpu::CompareFunction::Greater; + + if (type == DepthFuncType::GreaterEqual) + return wgpu::CompareFunction::GreaterEqual; + + if (type == DepthFuncType::Less) + return wgpu::CompareFunction::Less; + + if (type == DepthFuncType::LessEqual) + return wgpu::CompareFunction::LessEqual; + + if (type == DepthFuncType::Never) + return wgpu::CompareFunction::Never; + + if (type == DepthFuncType::NotEqual) + return wgpu::CompareFunction::NotEqual; + + throw "Not implemented"; +} + + +wgpu::CullMode Convert(CullingMode mode) +{ + if (mode == CullingMode::Clockwise) + return wgpu::CullMode::Back; + + if (mode == CullingMode::CounterClockwise) + return wgpu::CullMode::Front; + + if (mode == CullingMode::DoubleSide) + return wgpu::CullMode::None; + + throw "Not implemented"; +} + +wgpu::VertexFormat Convert(VertexLayoutFormat format) +{ + if (format == VertexLayoutFormat::R32_FLOAT) + return wgpu::VertexFormat::Float32; + + if (format == VertexLayoutFormat::R32G32_FLOAT) + return wgpu::VertexFormat::Float32x2; + + if (format == VertexLayoutFormat::R32G32B32_FLOAT) + return wgpu::VertexFormat::Float32x3; + + if (format == VertexLayoutFormat::R32G32B32_FLOAT) + return wgpu::VertexFormat::Float32x3; + + if (format == VertexLayoutFormat::R8G8B8A8_UNORM) + return wgpu::VertexFormat::Unorm8x4; + + if (format == VertexLayoutFormat::R8G8B8A8_UINT) + return wgpu::VertexFormat::Uint8x4; + + throw "Not implemented"; +} + +wgpu::StencilOperation Convert(StencilOperatorType type) +{ + if (type == StencilOperatorType::Keep) + return wgpu::StencilOperation::Keep; + + if (type == StencilOperatorType::Zero) + return wgpu::StencilOperation::Zero; + + if (type == StencilOperatorType::Replace) + return wgpu::StencilOperation::Replace; + + if (type == StencilOperatorType::Invert) + return wgpu::StencilOperation::Invert; + + if (type == StencilOperatorType::IncClamp) + return wgpu::StencilOperation::IncrementClamp; + + if (type == StencilOperatorType::DecClamp) + return wgpu::StencilOperation::DecrementClamp; + + if (type == StencilOperatorType::IncRepeat) + return wgpu::StencilOperation::IncrementWrap; + + if (type == StencilOperatorType::DecRepeat) + return wgpu::StencilOperation::DecrementWrap; + + throw "Not implemented"; +} + +wgpu::TextureFormat ConvertFormat(TextureFormatType format) +{ + if (format == TextureFormatType::R8G8B8A8_UNORM) + return wgpu::TextureFormat::RGBA8Unorm; + + if (format == TextureFormatType::B8G8R8A8_UNORM) + return wgpu::TextureFormat::BGRA8Unorm; + + if (format == TextureFormatType::R16G16B16A16_FLOAT) + return wgpu::TextureFormat::RGBA16Float; + + if (format == TextureFormatType::R32G32B32A32_FLOAT) + return wgpu::TextureFormat::RGBA32Float; + + if (format == TextureFormatType::R8G8B8A8_UNORM_SRGB) + return wgpu::TextureFormat::RGBA8UnormSrgb; + + if (format == TextureFormatType::B8G8R8A8_UNORM_SRGB) + return wgpu::TextureFormat::BGRA8UnormSrgb; + + if (format == TextureFormatType::R16G16_FLOAT) + return wgpu::TextureFormat::RG16Float; + + if (format == TextureFormatType::R8_UNORM) + return wgpu::TextureFormat::R8Unorm; + + if (format == TextureFormatType::BC1) + return wgpu::TextureFormat::BC1RGBAUnorm; + + if (format == TextureFormatType::BC2) + return wgpu::TextureFormat::BC2RGBAUnorm; + + if (format == TextureFormatType::BC3) + return wgpu::TextureFormat::BC3RGBAUnorm; + + if (format == TextureFormatType::BC1_SRGB) + return wgpu::TextureFormat::BC1RGBAUnormSrgb; + + if (format == TextureFormatType::BC2_SRGB) + return wgpu::TextureFormat::BC2RGBAUnormSrgb; + + if (format == TextureFormatType::BC3_SRGB) + return wgpu::TextureFormat::BC3RGBAUnormSrgb; + + if (format == TextureFormatType::D32) + return wgpu::TextureFormat::Depth32Float; + + if (format == TextureFormatType::D24S8) + return wgpu::TextureFormat::Depth24PlusStencil8; + + if (format == TextureFormatType::D32S8) + return wgpu::TextureFormat::Depth32FloatStencil8; + + if (format == TextureFormatType::Unknown) + return wgpu::TextureFormat::Undefined; + + throw "Not implemented"; +} + +TextureFormatType ConvertFormat(wgpu::TextureFormat format) +{ + if (format == wgpu::TextureFormat::RGBA8Unorm) + return TextureFormatType::R8G8B8A8_UNORM; + + if (format == wgpu::TextureFormat::BGRA8Unorm) + return TextureFormatType::B8G8R8A8_UNORM; + + if (format == wgpu::TextureFormat::RGBA16Float) + return TextureFormatType::R16G16B16A16_FLOAT; + + if (format == wgpu::TextureFormat::RGBA32Float) + return TextureFormatType::R32G32B32A32_FLOAT; + + if (format == wgpu::TextureFormat::RGBA8UnormSrgb) + return TextureFormatType::R8G8B8A8_UNORM_SRGB; + + if (format == wgpu::TextureFormat::BGRA8UnormSrgb) + return TextureFormatType::B8G8R8A8_UNORM_SRGB; + + if (format == wgpu::TextureFormat::RG16Float) + return TextureFormatType::R16G16_FLOAT; + + if (format == wgpu::TextureFormat::R8Unorm) + return TextureFormatType::R8_UNORM; + + if (format == wgpu::TextureFormat::BC1RGBAUnorm) + return TextureFormatType::BC1; + + if (format == wgpu::TextureFormat::BC2RGBAUnorm) + return TextureFormatType::BC2; + + if (format == wgpu::TextureFormat::BC3RGBAUnorm) + return TextureFormatType::BC3; + + if (format == wgpu::TextureFormat::BC1RGBAUnormSrgb) + return TextureFormatType::BC1_SRGB; + + if (format == wgpu::TextureFormat::BC2RGBAUnormSrgb) + return TextureFormatType::BC2_SRGB; + + if (format == wgpu::TextureFormat::BC3RGBAUnormSrgb) + return TextureFormatType::BC3_SRGB; + + if (format == wgpu::TextureFormat::Depth32Float) + return TextureFormatType::D32; + + if (format == wgpu::TextureFormat::Depth24PlusStencil8) + return TextureFormatType::D24S8; + + if (format == wgpu::TextureFormat::Depth32FloatStencil8) + return TextureFormatType::D32S8; + + if (format == wgpu::TextureFormat::Undefined) + return TextureFormatType::Unknown; + + throw "Not implemented"; +} + +int32_t GetSize(VertexLayoutFormat format) +{ + if (format == VertexLayoutFormat::R32G32B32_FLOAT) + { + return sizeof(float) * 3; + } + else if (format == VertexLayoutFormat::R32G32B32A32_FLOAT) + { + return sizeof(float) * 4; + } + else if (format == VertexLayoutFormat::R32_FLOAT) + { + return sizeof(float) * 1; + } + else if (format == VertexLayoutFormat::R32G32_FLOAT) + { + return sizeof(float) * 2; + } + else if (format == VertexLayoutFormat::R8G8B8A8_UINT) + { + return sizeof(float); + } + else if (format == VertexLayoutFormat::R8G8B8A8_UNORM) + { + return sizeof(float); + } + else + { + Log(LogType::Error, "Unimplemented VertexLoayoutFormat"); + return 0; + } +} + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.BaseWebGPU.h b/src/WebGPU/LLGI.BaseWebGPU.h new file mode 100644 index 00000000..5b47d4f2 --- /dev/null +++ b/src/WebGPU/LLGI.BaseWebGPU.h @@ -0,0 +1,33 @@ + +#pragma once + +#include +#include +#include "../LLGI.Base.h" + +namespace LLGI +{ + +wgpu::BlendOperation Convert(BlendEquationType type); + +wgpu::BlendFactor Convert(BlendFuncType type); + +wgpu::PrimitiveTopology Convert(TopologyType type); + +wgpu::CompareFunction Convert(CompareFuncType type); + +wgpu::CompareFunction Convert(DepthFuncType type); + +wgpu::CullMode Convert(CullingMode mode); + +wgpu::VertexFormat Convert(VertexLayoutFormat format); + +wgpu::StencilOperation Convert(StencilOperatorType type); + +wgpu::TextureFormat ConvertFormat(TextureFormatType format); + +TextureFormatType ConvertFormat(wgpu::TextureFormat format); + +int32_t GetSize(VertexLayoutFormat format); + +} // namespace std diff --git a/src/WebGPU/LLGI.BufferWebGPU.cpp b/src/WebGPU/LLGI.BufferWebGPU.cpp new file mode 100644 index 00000000..58272dbd --- /dev/null +++ b/src/WebGPU/LLGI.BufferWebGPU.cpp @@ -0,0 +1,127 @@ +#include "LLGI.BufferWebGPU.h" + +#include +#include + +namespace LLGI +{ + +namespace +{ +int32_t AlignTo(int32_t value, int32_t alignment) +{ + return (value + alignment - 1) / alignment * alignment; +} +} // namespace + +bool BufferWebGPU::Initialize(wgpu::Device& device, const BufferUsageType usage, const int32_t size, wgpu::Instance instance) +{ + device_ = device; + instance_ = instance; + + wgpu::BufferDescriptor desc{}; + allocatedSize_ = BitwiseContains(usage, BufferUsageType::Constant) ? AlignTo(size, 16) : size; + desc.size = allocatedSize_; + if (BitwiseContains(usage, BufferUsageType::MapRead)) + { + desc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst; + } + else + { + desc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc; + } + + if ((usage & BufferUsageType::Vertex) == BufferUsageType::Vertex) + { + desc.usage |= wgpu::BufferUsage::Vertex; + } + + if ((usage & BufferUsageType::Index) == BufferUsageType::Index) + { + desc.usage |= wgpu::BufferUsage::Index; + } + + if ((usage & BufferUsageType::Constant) == BufferUsageType::Constant) + { + desc.usage |= wgpu::BufferUsage::Uniform; + } + + if ((usage & BufferUsageType::ComputeRead) == BufferUsageType::ComputeRead || + (usage & BufferUsageType::ComputeWrite) == BufferUsageType::ComputeWrite) + { + desc.usage |= wgpu::BufferUsage::Storage; + } + + buffer_ = device.CreateBuffer(&desc); + size_ = size; + usage_ = usage; + return buffer_ != nullptr; +} + +void* BufferWebGPU::Lock() { return Lock(0, GetSize()); } + +void* BufferWebGPU::Lock(int32_t offset, int32_t size) +{ + lockedOffset_ = offset; + lockedSize_ = size; + + if (BitwiseContains(usage_, BufferUsageType::MapRead)) + { + bool completed = false; + bool succeeded = false; + auto future = buffer_.MapAsync(wgpu::MapMode::Read, + offset, + size, + instance_ != nullptr ? wgpu::CallbackMode::WaitAnyOnly : wgpu::CallbackMode::AllowProcessEvents, + [&completed, &succeeded](wgpu::MapAsyncStatus status, wgpu::StringView) { + succeeded = status == wgpu::MapAsyncStatus::Success; + completed = true; + }); + + if (instance_ != nullptr) + { + instance_.WaitAny(future, 5ULL * 1000ULL * 1000ULL * 1000ULL); + } + else + { + const auto waitStart = std::chrono::steady_clock::now(); + while (!completed) + { + device_.Tick(); + if (std::chrono::steady_clock::now() - waitStart > std::chrono::seconds(5)) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + return succeeded ? const_cast(buffer_.GetConstMappedRange(offset, size)) : nullptr; + } + + lockedBuffer_.resize(size); + return lockedBuffer_.data(); +} + +void BufferWebGPU::Unlock() +{ + if (lockedBuffer_.empty()) + { + if (BitwiseContains(usage_, BufferUsageType::MapRead)) + { + buffer_.Unmap(); + } + return; + } + + device_.GetQueue().WriteBuffer(buffer_, lockedOffset_, lockedBuffer_.data(), lockedSize_); + lockedBuffer_.clear(); + lockedOffset_ = 0; + lockedSize_ = 0; +} + +int32_t BufferWebGPU::GetSize() { return size_; } + +wgpu::Buffer& BufferWebGPU::GetBuffer() { return buffer_; } + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.BufferWebGPU.h b/src/WebGPU/LLGI.BufferWebGPU.h new file mode 100644 index 00000000..7a4123ce --- /dev/null +++ b/src/WebGPU/LLGI.BufferWebGPU.h @@ -0,0 +1,37 @@ +#pragma once + +#include "../LLGI.Buffer.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ +/** + * TODO : Implement short time buffer +*/ +class BufferWebGPU : public Buffer +{ + wgpu::Buffer buffer_ = nullptr; + wgpu::Device device_ = nullptr; + wgpu::Instance instance_ = nullptr; + std::vector lockedBuffer_; + int32_t lockedOffset_ = 0; + int32_t lockedSize_ = 0; + int32_t size_ = 0; + int32_t allocatedSize_ = 0; + int32_t offset_ = 0; + +public: + bool Initialize(wgpu::Device& device, const BufferUsageType usage, const int32_t size, wgpu::Instance instance = nullptr); + void* Lock() override; + void* Lock(int32_t offset, int32_t size) override; + void Unlock() override; + + int32_t GetSize() override; + + int32_t GetOffset() const { return offset_; } + int32_t GetAllocatedSize() const { return allocatedSize_; } + + wgpu::Buffer& GetBuffer(); +}; + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.CommandListWebGPU.cpp b/src/WebGPU/LLGI.CommandListWebGPU.cpp new file mode 100644 index 00000000..12d97d46 --- /dev/null +++ b/src/WebGPU/LLGI.CommandListWebGPU.cpp @@ -0,0 +1,412 @@ +#include "LLGI.CommandListWebGPU.h" +#include "LLGI.BufferWebGPU.h" +#include "LLGI.PipelineStateWebGPU.h" +#include "LLGI.RenderPassWebGPU.h" +#include "LLGI.TextureWebGPU.h" + +#include + +namespace LLGI +{ + +CommandListWebGPU::CommandListWebGPU(wgpu::Device device) : device_(device) +{ + for (int w = 0; w < 2; w++) + { + for (int f = 0; f < 2; f++) + { + std::array filters; + filters[0] = wgpu::FilterMode::Nearest; + filters[1] = wgpu::FilterMode::Linear; + + std::array am; + am[0] = wgpu::AddressMode::ClampToEdge; + am[1] = wgpu::AddressMode::Repeat; + + wgpu::SamplerDescriptor samplerDesc; + + samplerDesc.magFilter = filters[f]; + samplerDesc.minFilter = filters[f]; + samplerDesc.maxAnisotropy = 1; + samplerDesc.addressModeU = am[w]; + samplerDesc.addressModeV = am[w]; + samplerDesc.addressModeW = am[w]; + samplers_[w][f] = device.CreateSampler(&samplerDesc); + } + } +} + +void CommandListWebGPU::Begin() +{ + wgpu::CommandEncoderDescriptor desc = {}; + commandEncorder_ = device_.CreateCommandEncoder(&desc); + + CommandList::Begin(); +} + +void CommandListWebGPU::End() +{ + commandBuffer_ = commandEncorder_.Finish(); + commandEncorder_ = nullptr; + + CommandList::End(); +} + +void CommandListWebGPU::BeginRenderPass(RenderPass* renderPass) +{ + auto rp = static_cast(renderPass); + const auto& desc = rp->GetDescriptor(); + + renderPassEncorder_ = commandEncorder_.BeginRenderPass(&desc); + renderPassEncorder_.SetViewport(0.0f, 0.0f, static_cast(rp->GetScreenSize().X), static_cast(rp->GetScreenSize().Y), 0.0f, 1.0f); + + CommandList::BeginRenderPass(renderPass); +} + +void CommandListWebGPU::EndRenderPass() +{ + if (renderPassEncorder_ != nullptr) + { + renderPassEncorder_.End(); + renderPassEncorder_ = nullptr; + } + CommandList::EndRenderPass(); +} + +void CommandListWebGPU::BeginComputePass() +{ + wgpu::ComputePassDescriptor desc{}; + computePassEncorder_ = commandEncorder_.BeginComputePass(&desc); +} + +void CommandListWebGPU::EndComputePass() +{ + if (computePassEncorder_ != nullptr) + { + computePassEncorder_.End(); + computePassEncorder_ = nullptr; + } +} + +void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) +{ + BindingVertexBuffer bvb; + BindingIndexBuffer bib; + PipelineState* bpip = nullptr; + + bool isVBDirtied = false; + bool isIBDirtied = false; + bool isPipDirtied = false; + + GetCurrentVertexBuffer(bvb, isVBDirtied); + GetCurrentIndexBuffer(bib, isIBDirtied); + GetCurrentPipelineState(bpip, isPipDirtied); + + assert(bvb.vertexBuffer != nullptr); + assert(bib.indexBuffer != nullptr); + assert(bpip != nullptr); + + auto vb = static_cast(bvb.vertexBuffer); + auto ib = static_cast(bib.indexBuffer); + auto pip = static_cast(bpip); + + if (vb != nullptr) + { + renderPassEncorder_.SetVertexBuffer(0, vb->GetBuffer(), bvb.offset, bvb.vertexBuffer->GetSize() - bvb.offset); + } + + if (ib != nullptr) + { + const auto format = bib.stride == 2 ? wgpu::IndexFormat::Uint16 : wgpu::IndexFormat::Uint32; + renderPassEncorder_.SetIndexBuffer(ib->GetBuffer(), format, bib.offset, ib->GetSize() - bib.offset); + } + + if (pip != nullptr) + { + renderPassEncorder_.SetPipeline(pip->GetRenderPipeline()); + renderPassEncorder_.SetStencilReference(pip->StencilRef); + } + + std::vector constantBindGroupEntries; + + for (size_t unit_ind = 0; unit_ind < constantBuffers_.size(); unit_ind++) + { + auto cb = static_cast(constantBuffers_[unit_ind]); + if (cb == nullptr) + { + continue; + } + + wgpu::BindGroupEntry entry = {}; + entry.binding = static_cast(unit_ind); + entry.buffer = cb->GetBuffer(); + entry.size = cb->GetAllocatedSize() - cb->GetOffset(); + entry.offset = cb->GetOffset(); + constantBindGroupEntries.push_back(entry); + } + + if (!constantBindGroupEntries.empty()) + { + wgpu::BindGroupDescriptor constantBindGroupDesc = {}; + constantBindGroupDesc.layout = pip->GetRenderPipeline().GetBindGroupLayout(0); + constantBindGroupDesc.entries = constantBindGroupEntries.data(); + constantBindGroupDesc.entryCount = constantBindGroupEntries.size(); + auto constantBindGroup = device_.CreateBindGroup(&constantBindGroupDesc); + renderPassEncorder_.SetBindGroup(0, constantBindGroup); + } + + std::vector textureGroupEntries; + std::vector samplerGroupEntries; + + for (int unit_ind = 0; unit_ind < static_cast(currentTextures_.size()); unit_ind++) + { + if (currentTextures_[unit_ind].texture == nullptr) + continue; + auto texture = static_cast(currentTextures_[unit_ind].texture); + auto wm = (int32_t)currentTextures_[unit_ind].wrapMode; + auto mm = (int32_t)currentTextures_[unit_ind].minMagFilter; + + wgpu::BindGroupEntry textureEntry = {}; + textureEntry.binding = unit_ind; + textureEntry.textureView = texture->GetTextureView(); + textureGroupEntries.push_back(textureEntry); + + wgpu::BindGroupEntry samplerEntry = {}; + if (!BitwiseContains(texture->GetParameter().Usage, TextureUsageType::Storage)) + { + samplerEntry.binding = unit_ind; + samplerEntry.sampler = samplers_[wm][mm]; + samplerGroupEntries.push_back(samplerEntry); + } + } + + for (int unit_ind = 0; unit_ind < static_cast(computeBuffers_.size()); unit_ind++) + { + if (computeBuffers_[unit_ind].computeBuffer == nullptr) + { + continue; + } + + auto buffer = static_cast(computeBuffers_[unit_ind].computeBuffer); + wgpu::BindGroupEntry bufferEntry = {}; + bufferEntry.binding = static_cast(unit_ind); + bufferEntry.buffer = buffer->GetBuffer(); + bufferEntry.offset = buffer->GetOffset(); + bufferEntry.size = buffer->GetSize(); + textureGroupEntries.push_back(bufferEntry); + } + + if (!textureGroupEntries.empty()) + { + wgpu::BindGroupDescriptor textureBindGroupDesc = {}; + textureBindGroupDesc.layout = pip->GetRenderPipeline().GetBindGroupLayout(1); + textureBindGroupDesc.entries = textureGroupEntries.data(); + textureBindGroupDesc.entryCount = textureGroupEntries.size(); + auto textureBindGroup = device_.CreateBindGroup(&textureBindGroupDesc); + renderPassEncorder_.SetBindGroup(1, textureBindGroup); + } + + if (!samplerGroupEntries.empty()) + { + wgpu::BindGroupDescriptor samplerBindGroupDesc = {}; + samplerBindGroupDesc.layout = pip->GetRenderPipeline().GetBindGroupLayout(2); + samplerBindGroupDesc.entries = samplerGroupEntries.data(); + samplerBindGroupDesc.entryCount = samplerGroupEntries.size(); + auto samplerBindGroup = device_.CreateBindGroup(&samplerBindGroupDesc); + renderPassEncorder_.SetBindGroup(2, samplerBindGroup); + } + + int indexPerPrim = 0; + + if (pip->Topology == TopologyType::Triangle) + { + indexPerPrim = 3; + } + else if (pip->Topology == TopologyType::Line) + { + indexPerPrim = 2; + } + else if (pip->Topology == TopologyType::Point) + { + indexPerPrim = 1; + } + else + { + assert(0); + } + + renderPassEncorder_.DrawIndexed(primitiveCount * indexPerPrim, instanceCount, 0, 0, 0); + CommandList::Draw(primitiveCount, instanceCount); +} + +void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, int32_t threadX, int32_t threadY, int32_t threadZ) +{ + PipelineState* bpip = nullptr; + bool isPipDirtied = false; + GetCurrentPipelineState(bpip, isPipDirtied); + auto pip = static_cast(bpip); + if (pip == nullptr || computePassEncorder_ == nullptr) + { + return; + } + + computePassEncorder_.SetPipeline(pip->GetComputePipeline()); + + std::vector constantBindGroupEntries; + for (size_t unit_ind = 0; unit_ind < constantBuffers_.size(); unit_ind++) + { + auto cb = static_cast(constantBuffers_[unit_ind]); + if (cb == nullptr) + { + continue; + } + + wgpu::BindGroupEntry entry{}; + entry.binding = static_cast(unit_ind); + entry.buffer = cb->GetBuffer(); + entry.size = cb->GetAllocatedSize() - cb->GetOffset(); + entry.offset = cb->GetOffset(); + constantBindGroupEntries.push_back(entry); + } + + if (!constantBindGroupEntries.empty()) + { + wgpu::BindGroupDescriptor desc{}; + desc.layout = pip->GetComputePipeline().GetBindGroupLayout(0); + desc.entries = constantBindGroupEntries.data(); + desc.entryCount = constantBindGroupEntries.size(); + auto bindGroup = device_.CreateBindGroup(&desc); + computePassEncorder_.SetBindGroup(0, bindGroup); + } + + std::vector textureGroupEntries; + std::vector samplerAndBufferGroupEntries; + + for (int unit_ind = 0; unit_ind < static_cast(currentTextures_.size()); unit_ind++) + { + if (currentTextures_[unit_ind].texture == nullptr) + { + continue; + } + + auto texture = static_cast(currentTextures_[unit_ind].texture); + auto wm = (int32_t)currentTextures_[unit_ind].wrapMode; + auto mm = (int32_t)currentTextures_[unit_ind].minMagFilter; + + wgpu::BindGroupEntry textureEntry{}; + textureEntry.binding = unit_ind; + textureEntry.textureView = texture->GetTextureView(); + textureGroupEntries.push_back(textureEntry); + + if (!BitwiseContains(texture->GetParameter().Usage, TextureUsageType::Storage)) + { + wgpu::BindGroupEntry samplerEntry{}; + samplerEntry.binding = unit_ind; + samplerEntry.sampler = samplers_[wm][mm]; + samplerAndBufferGroupEntries.push_back(samplerEntry); + } + } + + for (int unit_ind = 0; unit_ind < static_cast(computeBuffers_.size()); unit_ind++) + { + if (computeBuffers_[unit_ind].computeBuffer == nullptr) + { + continue; + } + + auto buffer = static_cast(computeBuffers_[unit_ind].computeBuffer); + wgpu::BindGroupEntry entry{}; + entry.binding = static_cast(unit_ind); + entry.buffer = buffer->GetBuffer(); + entry.offset = buffer->GetOffset(); + entry.size = buffer->GetSize(); + samplerAndBufferGroupEntries.push_back(entry); + } + + if (!textureGroupEntries.empty()) + { + wgpu::BindGroupDescriptor desc{}; + desc.layout = pip->GetComputePipeline().GetBindGroupLayout(1); + desc.entries = textureGroupEntries.data(); + desc.entryCount = textureGroupEntries.size(); + auto bindGroup = device_.CreateBindGroup(&desc); + computePassEncorder_.SetBindGroup(1, bindGroup); + } + + if (!samplerAndBufferGroupEntries.empty()) + { + wgpu::BindGroupDescriptor desc{}; + desc.layout = pip->GetComputePipeline().GetBindGroupLayout(2); + desc.entries = samplerAndBufferGroupEntries.data(); + desc.entryCount = samplerAndBufferGroupEntries.size(); + auto bindGroup = device_.CreateBindGroup(&desc); + computePassEncorder_.SetBindGroup(2, bindGroup); + } + + computePassEncorder_.DispatchWorkgroups(groupX, groupY, groupZ); + CommandList::Dispatch(groupX, groupY, groupZ, threadX, threadY, threadZ); +} + +void CommandListWebGPU::SetScissor(int32_t x, int32_t y, int32_t width, int32_t height) +{ + renderPassEncorder_.SetScissorRect(x, y, width, height); +} + +void CommandListWebGPU::CopyTexture(Texture* src, Texture* dst) +{ + auto srcTex = static_cast(src); + CopyTexture(src, dst, {0, 0, 0}, {0, 0, 0}, srcTex->GetParameter().Size, 0, 0); +} + +void CommandListWebGPU::CopyTexture( + Texture* src, Texture* dst, const Vec3I& srcPos, const Vec3I& dstPos, const Vec3I& size, int srcLayer, int dstLayer) +{ + if (isInRenderPass_) + { + Log(LogType::Error, "Please call CopyTexture outside of RenderPass"); + return; + } + + auto srcTex = static_cast(src); + auto dstTex = static_cast(dst); + + wgpu::TexelCopyTextureInfo srcTexCopy; + wgpu::TexelCopyTextureInfo dstTexCopy; + wgpu::Extent3D extend3d; + + srcTexCopy.texture = srcTex->GetTexture(); + srcTexCopy.origin = {static_cast(srcPos.X), static_cast(srcPos.Y), static_cast(srcLayer + srcPos.Z)}; + srcTexCopy.aspect = wgpu::TextureAspect::All; + + dstTexCopy.texture = dstTex->GetTexture(); + dstTexCopy.origin = {static_cast(dstPos.X), static_cast(dstPos.Y), static_cast(dstLayer + dstPos.Z)}; + dstTexCopy.aspect = wgpu::TextureAspect::All; + + extend3d.width = size.X; + extend3d.height = size.Y; + extend3d.depthOrArrayLayers = size.Z; + + commandEncorder_.CopyTextureToTexture(&srcTexCopy, &dstTexCopy, &extend3d); +} + +void CommandListWebGPU::CopyBuffer(Buffer* src, Buffer* dst) +{ + auto srcBuffer = static_cast(src); + auto dstBuffer = static_cast(dst); + if (srcBuffer == nullptr || dstBuffer == nullptr) + { + return; + } + + commandEncorder_.CopyBufferToBuffer(srcBuffer->GetBuffer(), 0, dstBuffer->GetBuffer(), 0, std::min(srcBuffer->GetSize(), dstBuffer->GetSize())); +} + +void CommandListWebGPU::WaitUntilCompleted() +{ + if (device_ != nullptr) + { + device_.Tick(); + } +} + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.CommandListWebGPU.h b/src/WebGPU/LLGI.CommandListWebGPU.h new file mode 100644 index 00000000..64280e27 --- /dev/null +++ b/src/WebGPU/LLGI.CommandListWebGPU.h @@ -0,0 +1,51 @@ +#pragma once + +#include "../LLGI.CommandList.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ + +class CommandListWebGPU : public CommandList +{ + wgpu::Device device_; + wgpu::CommandBuffer commandBuffer_; + wgpu::CommandEncoder commandEncorder_; + wgpu::RenderPassEncoder renderPassEncorder_; + wgpu::ComputePassEncoder computePassEncorder_; + wgpu::Sampler samplers_[2][2]; + +public: + CommandListWebGPU(wgpu::Device device); + + void Begin() override; + + void End() override; + + void BeginRenderPass(RenderPass* renderPass) override; + + void EndRenderPass() override; + + void Draw(int32_t primitiveCount, int32_t instanceCount) override; + + void BeginComputePass() override; + + void EndComputePass() override; + + void Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, int32_t threadX, int32_t threadY, int32_t threadZ) override; + + void SetScissor(int32_t x, int32_t y, int32_t width, int32_t height) override; + + void CopyTexture(Texture* src, Texture* dst) override; + + void CopyTexture( + Texture* src, Texture* dst, const Vec3I& srcPos, const Vec3I& dstPos, const Vec3I& size, int srcLayer, int dstLayer) override; + + void CopyBuffer(Buffer* src, Buffer* dst) override; + + void WaitUntilCompleted() override; + + const wgpu::CommandBuffer& GetCommandBuffer() const { return commandBuffer_; } +}; + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.CompilerWebGPU.cpp b/src/WebGPU/LLGI.CompilerWebGPU.cpp new file mode 100644 index 00000000..6858f9ae --- /dev/null +++ b/src/WebGPU/LLGI.CompilerWebGPU.cpp @@ -0,0 +1,88 @@ +#include "LLGI.CompilerWebGPU.h" +#include + +namespace LLGI +{ +void CompilerWebGPU::Compile(CompilerResult& result, const char* code, ShaderStageType shaderStage) +{ + std::vector buffer; + + // header + buffer.push_back('w'); + buffer.push_back('g'); + buffer.push_back('s'); + buffer.push_back('l'); + buffer.push_back('c'); + buffer.push_back('o'); + buffer.push_back('d'); + buffer.push_back('e'); + + const auto len = strlen(code) + 1; + for (size_t i = 0; i < len; i++) + { + buffer.push_back(code[i]); + } + + result.Binary.resize(1); + result.Binary[0].resize(buffer.size()); + memcpy(result.Binary[0].data(), buffer.data(), buffer.size()); +} +} // namespace LLGI + +/* +#include "LLGI.CompilerMetal.h" + +#import + +namespace LLGI +{ + +void CompilerMetal::Initialize() {} + +void CompilerMetal::Compile(CompilerResult& result, const char* code, ShaderStageType shaderStage) +{ + @autoreleasepool + { + // Metal doesn't support to save a library as binary file (with external tool, it can) + NSString* codeStr = [[[NSString alloc] initWithUTF8String:code] autorelease]; + + id device = MTLCreateSystemDefaultDevice(); + + NSError* libraryError = nil; + id lib = [[device newLibraryWithSource:codeStr options:NULL error:&libraryError] autorelease]; + if (libraryError) + { + result.Message = libraryError.localizedDescription.UTF8String; + } + + if (lib == NULL) + { + return; + } + + std::vector buffer; + + // header + buffer.push_back('m'); + buffer.push_back('t'); + buffer.push_back('l'); + buffer.push_back('c'); + buffer.push_back('o'); + buffer.push_back('d'); + buffer.push_back('e'); + + auto len = strlen(code) + 1; + for (int i = 0; i < len; i++) + { + buffer.push_back(code[i]); + } + + result.Binary.resize(1); + result.Binary[0].resize(buffer.size()); + memcpy(result.Binary[0].data(), buffer.data(), buffer.size()); + } +} + +} + +*/ \ No newline at end of file diff --git a/src/WebGPU/LLGI.CompilerWebGPU.h b/src/WebGPU/LLGI.CompilerWebGPU.h new file mode 100644 index 00000000..fac97434 --- /dev/null +++ b/src/WebGPU/LLGI.CompilerWebGPU.h @@ -0,0 +1,17 @@ +#pragma once + +#include "LLGI.BaseWebGPU.h" +#include "../LLGI.Compiler.h" + +namespace LLGI +{ + +class CompilerWebGPU : public Compiler +{ +public: + void Compile(CompilerResult& result, const char* code, ShaderStageType shaderStage) override; + + DeviceType GetDeviceType() const override { return DeviceType::WebGPU; } +}; + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.GraphicsWebGPU.cpp b/src/WebGPU/LLGI.GraphicsWebGPU.cpp new file mode 100644 index 00000000..ed4b814a --- /dev/null +++ b/src/WebGPU/LLGI.GraphicsWebGPU.cpp @@ -0,0 +1,360 @@ +#include "LLGI.GraphicsWebGPU.h" + +#include "LLGI.BufferWebGPU.h" +#include "LLGI.CommandListWebGPU.h" +#include "LLGI.PipelineStateWebGPU.h" +#include "LLGI.RenderPassPipelineStateWebGPU.h" +#include "LLGI.RenderPassWebGPU.h" +#include "LLGI.ShaderWebGPU.h" +#include "LLGI.TextureWebGPU.h" + +#include +#include +#include +#include + +namespace LLGI +{ + +namespace +{ +uint32_t AlignTo(uint32_t value, uint32_t alignment) +{ + return (value + alignment - 1) / alignment * alignment; +} + +uint32_t GetFormatBytesPerPixel(TextureFormatType format) +{ + switch (format) + { + case TextureFormatType::R8_UNORM: + return 1; + case TextureFormatType::R32G32B32A32_FLOAT: + return 16; + default: + return 4; + } +} +} // namespace + +class SingleFrameMemoryPoolWebGPU : public SingleFrameMemoryPool +{ + wgpu::Device device_; + + Buffer* CreateBufferInternal(int32_t size) override + { + auto obj = new BufferWebGPU(); + if (!obj->Initialize(device_, BufferUsageType::Constant, size)) + { + SafeRelease(obj); + return nullptr; + } + return obj; + } + + Buffer* ReinitializeBuffer(Buffer* cb, int32_t size) override + { + if (cb != nullptr && cb->GetSize() >= size) + { + return cb; + } + return CreateBufferInternal(size); + } + +public: + SingleFrameMemoryPoolWebGPU(wgpu::Device device, int32_t swapBufferCount) + : SingleFrameMemoryPool(swapBufferCount), device_(device) + { + } +}; + +GraphicsWebGPU::GraphicsWebGPU(wgpu::Device device) : device_(device) { queue_ = device.GetQueue(); } + +GraphicsWebGPU::GraphicsWebGPU(wgpu::Device device, wgpu::Instance instance) : device_(device), instance_(instance) +{ + queue_ = device.GetQueue(); +} + +void GraphicsWebGPU::SetWindowSize(const Vec2I& windowSize) {} + +void GraphicsWebGPU::Execute(CommandList* commandList) +{ + auto commandListWgpu = static_cast(commandList); + auto cb = commandListWgpu->GetCommandBuffer(); + queue_.Submit(1, &cb); +} + +void GraphicsWebGPU::WaitFinish() +{ + if (device_ != nullptr) + { + device_.Tick(); + } +} + +Buffer* GraphicsWebGPU::CreateBuffer(BufferUsageType usage, int32_t size) +{ + auto obj = new BufferWebGPU(); + if (!obj->Initialize(GetDevice(), usage, size, instance_)) + { + SafeRelease(obj); + return nullptr; + } + + return obj; +} + +Shader* GraphicsWebGPU::CreateShader(DataStructure* data, int32_t count) +{ + auto obj = new ShaderWebGPU(); + if (!obj->Initialize(GetDevice(), data, count)) + { + SafeRelease(obj); + return nullptr; + } + return obj; +} + +PipelineState* GraphicsWebGPU::CreatePiplineState() +{ + auto pipelineState = new PipelineStateWebGPU(GetDevice()); + + // TODO : error check + return pipelineState; +} + +SingleFrameMemoryPool* GraphicsWebGPU::CreateSingleFrameMemoryPool(int32_t constantBufferPoolSize, int32_t drawingCount) +{ + return new SingleFrameMemoryPoolWebGPU(GetDevice(), 1); +} + +CommandList* GraphicsWebGPU::CreateCommandList(SingleFrameMemoryPool* memoryPool) +{ + auto commandList = new CommandListWebGPU(GetDevice()); + + // TODO : error check + return commandList; +} + +RenderPass* GraphicsWebGPU::CreateRenderPass(Texture** textures, int32_t textureCount, Texture* depthTexture) +{ + assert(textures != nullptr); + if (textures == nullptr) + return nullptr; + + for (int32_t i = 0; i < textureCount; i++) + { + assert(textures[i] != nullptr); + if (textures[i] == nullptr) + return nullptr; + } + + auto dt = static_cast(depthTexture); + + auto renderPass = new RenderPassWebGPU(); + if (!renderPass->Initialize(textures, textureCount, dt, nullptr, nullptr)) + { + SafeRelease(renderPass); + } + + return renderPass; +} + +RenderPass* +GraphicsWebGPU::CreateRenderPass(Texture* texture, Texture* resolvedTexture, Texture* depthTexture, Texture* resolvedDepthTexture) +{ + if (texture == nullptr) + return nullptr; + + auto dt = static_cast(depthTexture); + auto rt = static_cast(resolvedTexture); + auto rdt = static_cast(resolvedDepthTexture); + + auto renderPass = new RenderPassWebGPU(); + if (!renderPass->Initialize((&texture), 1, (TextureWebGPU*)dt, (TextureWebGPU*)rt, (TextureWebGPU*)rdt)) + { + SafeRelease(renderPass); + } + + return renderPass; +} + +Texture* GraphicsWebGPU::CreateTexture(uint64_t id) { return nullptr; } + +Texture* GraphicsWebGPU::CreateTexture(const TextureParameter& parameter) +{ + auto obj = new TextureWebGPU(); + if (!obj->Initialize(GetDevice(), parameter, instance_)) + { + SafeRelease(obj); + return nullptr; + } + return obj; +} + +Texture* GraphicsWebGPU::CreateTexture(const TextureInitializationParameter& parameter) +{ + TextureParameter param; + param.Dimension = 2; + param.Format = parameter.Format; + param.MipLevelCount = parameter.MipMapCount; + param.SampleCount = 1; + param.Size = {parameter.Size.X, parameter.Size.Y, 1}; + return CreateTexture(param); +} + +Texture* GraphicsWebGPU::CreateRenderTexture(const RenderTextureInitializationParameter& parameter) +{ + TextureParameter param; + param.Dimension = 2; + param.Format = parameter.Format; + param.MipLevelCount = 1; + param.SampleCount = parameter.SamplingCount; + param.Size = {parameter.Size.X, parameter.Size.Y, 1}; + param.Usage = TextureUsageType::RenderTarget; + return CreateTexture(param); +} + +Texture* GraphicsWebGPU::CreateDepthTexture(const DepthTextureInitializationParameter& parameter) +{ + auto format = TextureFormatType::D32; + if (parameter.Mode == DepthTextureMode::DepthStencil) + { + format = TextureFormatType::D24S8; + } + + TextureParameter param; + param.Dimension = 2; + param.Format = format; + param.MipLevelCount = 1; + param.SampleCount = parameter.SamplingCount; + param.Size = {parameter.Size.X, parameter.Size.Y, 1}; + param.Usage = TextureUsageType::RenderTarget; + return CreateTexture(param); +} + +std::vector GraphicsWebGPU::CaptureRenderTarget(Texture* renderTarget) +{ + auto texture = static_cast(renderTarget); + if (texture == nullptr) + { + return std::vector(); + } + + const auto size = texture->GetSizeAs2D(); + const auto bytesPerPixel = GetFormatBytesPerPixel(texture->GetFormat()); + const auto unalignedBytesPerRow = static_cast(size.X) * bytesPerPixel; + const auto bytesPerRow = AlignTo(unalignedBytesPerRow, 256); + const auto bufferSize = static_cast(bytesPerRow) * static_cast(size.Y); + + wgpu::BufferDescriptor bufferDesc{}; + bufferDesc.size = bufferSize; + bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead; + auto readbackBuffer = device_.CreateBuffer(&bufferDesc); + + wgpu::CommandEncoderDescriptor encoderDesc{}; + auto encoder = device_.CreateCommandEncoder(&encoderDesc); + + wgpu::TexelCopyTextureInfo src{}; + src.texture = texture->GetTexture(); + src.aspect = wgpu::TextureAspect::All; + + wgpu::TexelCopyBufferInfo dst{}; + dst.buffer = readbackBuffer; + dst.layout.offset = 0; + dst.layout.bytesPerRow = bytesPerRow; + dst.layout.rowsPerImage = static_cast(size.Y); + + wgpu::Extent3D extent{}; + extent.width = static_cast(size.X); + extent.height = static_cast(size.Y); + extent.depthOrArrayLayers = 1; + encoder.CopyTextureToBuffer(&src, &dst, &extent); + + auto commandBuffer = encoder.Finish(); + queue_.Submit(1, &commandBuffer); + + bool completed = false; + bool succeeded = false; + auto future = readbackBuffer.MapAsync(wgpu::MapMode::Read, + 0, + bufferSize, + instance_ != nullptr ? wgpu::CallbackMode::WaitAnyOnly : wgpu::CallbackMode::AllowProcessEvents, + [&completed, &succeeded](wgpu::MapAsyncStatus status, wgpu::StringView) { + succeeded = status == wgpu::MapAsyncStatus::Success; + completed = true; + }); + + if (instance_ != nullptr) + { + instance_.WaitAny(future, 5ULL * 1000ULL * 1000ULL * 1000ULL); + } + else + { + const auto waitStart = std::chrono::steady_clock::now(); + while (!completed) + { + device_.Tick(); + if (std::chrono::steady_clock::now() - waitStart > std::chrono::seconds(5)) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + std::vector ret(static_cast(unalignedBytesPerRow) * static_cast(size.Y)); + if (!succeeded) + { + Log(LogType::Warning, "Timed out or failed while waiting for WebGPU readback."); + return ret; + } + + auto mapped = static_cast(readbackBuffer.GetConstMappedRange(0, bufferSize)); + for (int32_t y = 0; y < size.Y; y++) + { + memcpy(ret.data() + static_cast(y) * unalignedBytesPerRow, + mapped + static_cast(y) * bytesPerRow, + unalignedBytesPerRow); + } + readbackBuffer.Unmap(); + return ret; +} + +RenderPassPipelineState* GraphicsWebGPU::CreateRenderPassPipelineState(RenderPass* renderPass) +{ + return CreateRenderPassPipelineState(renderPass->GetKey()); +} + +RenderPassPipelineState* GraphicsWebGPU::CreateRenderPassPipelineState(const RenderPassPipelineStateKey& key) +{ + // already? + { + auto it = renderPassPipelineStates_.find(key); + + if (it != renderPassPipelineStates_.end()) + { + auto ret = it->second; + + if (ret != nullptr) + { + auto ptr = ret.get(); + SafeAddRef(ptr); + return ptr; + } + } + } + + std::shared_ptr ret = LLGI::CreateSharedPtr<>(new RenderPassPipelineStateWebGPU()); + ret->SetKey(key); + + renderPassPipelineStates_[key] = ret; + + { + auto ptr = ret.get(); + SafeAddRef(ptr); + return ptr; + } +} + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.GraphicsWebGPU.h b/src/WebGPU/LLGI.GraphicsWebGPU.h new file mode 100644 index 00000000..f5c638e8 --- /dev/null +++ b/src/WebGPU/LLGI.GraphicsWebGPU.h @@ -0,0 +1,67 @@ +#pragma once + +#include "../LLGI.Base.h" +#include "../LLGI.Graphics.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ +class RenderPassPipelineStateWebGPU; + +class GraphicsWebGPU : public Graphics +{ +private: + //! cached + std::unordered_map, RenderPassPipelineStateKey::Hash> + renderPassPipelineStates_; + + wgpu::Device device_; + wgpu::Instance instance_; + wgpu::Queue queue_; + +public: + + GraphicsWebGPU(wgpu::Device device); + GraphicsWebGPU(wgpu::Device device, wgpu::Instance instance); + + void SetWindowSize(const Vec2I& windowSize) override; + + void Execute(CommandList* commandList) override; + + void WaitFinish() override; + + Buffer* CreateBuffer(BufferUsageType usage, int32_t size) override; + + Shader* CreateShader(DataStructure* data, int32_t count) override; + + PipelineState* CreatePiplineState() override; + + SingleFrameMemoryPool* CreateSingleFrameMemoryPool(int32_t constantBufferPoolSize, int32_t drawingCount) override; + + CommandList* CreateCommandList(SingleFrameMemoryPool* memoryPool) override; + + RenderPass* CreateRenderPass(Texture** textures, int32_t textureCount, Texture* depthTexture) override; + + RenderPass* CreateRenderPass(Texture* texture, Texture* resolvedTexture, Texture* depthTexture, Texture* resolvedDepthTexture) override; + + Texture* CreateTexture(const TextureParameter& parameter) override; + + Texture* CreateTexture(const TextureInitializationParameter& parameter) override; + + Texture* CreateRenderTexture(const RenderTextureInitializationParameter& parameter) override; + + Texture* CreateDepthTexture(const DepthTextureInitializationParameter& parameter) override; + + Texture* CreateTexture(uint64_t id) override; + + std::vector CaptureRenderTarget(Texture* renderTarget) override; + + RenderPassPipelineState* CreateRenderPassPipelineState(RenderPass* renderPass) override; + + RenderPassPipelineState* CreateRenderPassPipelineState(const RenderPassPipelineStateKey& key) override; + + wgpu::Device& GetDevice() { return device_; } + wgpu::Queue& GetQueue() { return queue_; } +}; + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.PipelineStateWebGPU.cpp b/src/WebGPU/LLGI.PipelineStateWebGPU.cpp new file mode 100644 index 00000000..7483f684 --- /dev/null +++ b/src/WebGPU/LLGI.PipelineStateWebGPU.cpp @@ -0,0 +1,159 @@ +#include "LLGI.PipelineStateWebGPU.h" +#include "LLGI.RenderPassPipelineStateWebGPU.h" +#include "LLGI.ShaderWebGPU.h" +#include + +namespace LLGI +{ + +PipelineStateWebGPU::PipelineStateWebGPU(wgpu::Device device) : device_(device) { shaders_.fill(nullptr); } + +PipelineStateWebGPU::~PipelineStateWebGPU() +{ + for (auto& shader : shaders_) + { + SafeRelease(shader); + } +} + +void PipelineStateWebGPU::SetShader(ShaderStageType stage, Shader* shader) +{ + SafeAddRef(shader); + SafeRelease(shaders_[static_cast(stage)]); + shaders_[static_cast(stage)] = shader; +} + +bool PipelineStateWebGPU::Compile() +{ + const char* entryPointName = "main"; + auto computeShader = static_cast(shaders_[static_cast(ShaderStageType::Compute)]); + if (computeShader != nullptr) + { + wgpu::ComputePipelineDescriptor desc{}; + desc.layout = nullptr; + desc.compute.module = computeShader->GetShaderModule(); + desc.compute.entryPoint = entryPointName; + computePipeline_ = device_.CreateComputePipeline(&desc); + return computePipeline_ != nullptr; + } + + wgpu::RenderPipelineDescriptor desc{}; + + desc.primitive.topology = Convert(Topology); + desc.primitive.stripIndexFormat = wgpu::IndexFormat::Undefined; // is it correct? + desc.primitive.frontFace = wgpu::FrontFace::CW; + desc.primitive.cullMode = Convert(Culling); + desc.multisample.count = static_cast(renderPassPipelineState_->Key.SamplingCount); + desc.multisample.mask = std::numeric_limits::max(); + desc.multisample.alphaToCoverageEnabled = false; + desc.layout = nullptr; // is it correct? + + auto vertexShader = static_cast(shaders_[static_cast(ShaderStageType::Vertex)]); + + desc.vertex.module = vertexShader->GetShaderModule(); + desc.vertex.entryPoint = entryPointName; + + desc.vertex.bufferCount = 1; + std::array bufferLayouts; + desc.vertex.buffers = bufferLayouts.data(); + + bufferLayouts[0].attributeCount = VertexLayoutCount; + bufferLayouts[0].arrayStride = 0; + bufferLayouts[0].stepMode = wgpu::VertexStepMode::Vertex; + + std::array attributes; + bufferLayouts[0].attributes = attributes.data(); + + int offset = 0; + for (int i = 0; i < VertexLayoutCount; i++) + { + attributes[i].format = Convert(VertexLayouts[i]); + attributes[i].offset = offset; + attributes[i].shaderLocation = i; + offset += GetSize(VertexLayouts[i]); + } + bufferLayouts[0].arrayStride = offset; + + auto pixelShader = static_cast(shaders_[static_cast(ShaderStageType::Pixel)]); + + // TODO : support blend enabled + wgpu::BlendState blendState; + blendState.color.srcFactor = Convert(BlendSrcFunc); + blendState.color.dstFactor = Convert(BlendDstFunc); + blendState.color.operation = Convert(BlendEquationRGB); + blendState.alpha.srcFactor = Convert(BlendSrcFuncAlpha); + blendState.alpha.dstFactor = Convert(BlendDstFuncAlpha); + blendState.alpha.operation = Convert(BlendEquationAlpha); + + std::array colorTargetStates; + + for (size_t i = 0; i < renderPassPipelineState_->Key.RenderTargetFormats.size(); i++) + { + colorTargetStates[i].blend = IsBlendEnabled ? &blendState : nullptr; + colorTargetStates[i].format = ConvertFormat(renderPassPipelineState_->Key.RenderTargetFormats.at(i)); + colorTargetStates[i].writeMask = wgpu::ColorWriteMask::All; + } + + wgpu::FragmentState fragmentState = {}; + fragmentState.targetCount = static_cast(renderPassPipelineState_->Key.RenderTargetFormats.size()); + fragmentState.targets = colorTargetStates.data(); + fragmentState.entryPoint = entryPointName; + fragmentState.module = pixelShader->GetShaderModule(); + + desc.fragment = &fragmentState; + + wgpu::DepthStencilState depthStencilState = {}; + depthStencilState.depthWriteEnabled = IsDepthWriteEnabled; + + if (IsDepthTestEnabled) + { + depthStencilState.depthCompare = Convert(DepthFunc); + } + else + { + depthStencilState.depthCompare = wgpu::CompareFunction::Always; + } + + if (IsStencilTestEnabled) + { + wgpu::StencilFaceState fs; + + fs.compare = Convert(StencilCompareFunc); + fs.depthFailOp = Convert(StencilDepthFailOp); + fs.failOp = Convert(StencilFailOp); + fs.passOp = Convert(StencilPassOp); + + depthStencilState.stencilFront = fs; + depthStencilState.stencilBack = fs; + + depthStencilState.stencilWriteMask = StencilWriteMask; + depthStencilState.stencilReadMask = StencilReadMask; + } + else + { + wgpu::StencilFaceState fs; + + fs.depthFailOp = wgpu::StencilOperation::Keep; + fs.failOp = wgpu::StencilOperation::Keep; + fs.compare = wgpu::CompareFunction::Always; + fs.passOp = wgpu::StencilOperation::Keep; + + depthStencilState.stencilFront = fs; + depthStencilState.stencilBack = fs; + + depthStencilState.stencilWriteMask = 0xff; + depthStencilState.stencilReadMask = 0xff; + } + + if (renderPassPipelineState_->Key.DepthFormat != TextureFormatType::Unknown) + { + depthStencilState.format = ConvertFormat(renderPassPipelineState_->Key.DepthFormat); + desc.depthStencil = &depthStencilState; + } + + renderPipeline_ = device_.CreateRenderPipeline(&desc); + + return renderPipeline_ != nullptr; +} + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.PipelineStateWebGPU.h b/src/WebGPU/LLGI.PipelineStateWebGPU.h new file mode 100644 index 00000000..e0411774 --- /dev/null +++ b/src/WebGPU/LLGI.PipelineStateWebGPU.h @@ -0,0 +1,30 @@ +#pragma once + +#include "../LLGI.PipelineState.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ + +class PipelineStateWebGPU : public PipelineState +{ + std::array(ShaderStageType::Max)> shaders_; + + wgpu::Device device_; + + wgpu::RenderPipeline renderPipeline_; + wgpu::ComputePipeline computePipeline_; + +public: + PipelineStateWebGPU(wgpu::Device device); + ~PipelineStateWebGPU() override; + + void SetShader(ShaderStageType stage, Shader* shader) override; + + bool Compile() override; + + wgpu::RenderPipeline GetRenderPipeline() { return renderPipeline_; } + wgpu::ComputePipeline GetComputePipeline() { return computePipeline_; } +}; + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.PlatformWebGPU.cpp b/src/WebGPU/LLGI.PlatformWebGPU.cpp new file mode 100644 index 00000000..1aad7ac2 --- /dev/null +++ b/src/WebGPU/LLGI.PlatformWebGPU.cpp @@ -0,0 +1,335 @@ +#include "LLGI.PlatformWebGPU.h" +#include "LLGI.GraphicsWebGPU.h" +#include "LLGI.RenderPassWebGPU.h" +#include "LLGI.TextureWebGPU.h" + +#include +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace LLGI +{ + +namespace +{ +bool IsSurfaceTextureAcquired(wgpu::SurfaceGetCurrentTextureStatus status) +{ + return status == wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal || + status == wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal; +} + +wgpu::PresentMode SelectPresentMode(const wgpu::SurfaceCapabilities& capabilities, bool waitVSync) +{ + if (waitVSync) + { + return wgpu::PresentMode::Fifo; + } + + for (size_t i = 0; i < capabilities.presentModeCount; i++) + { + if (capabilities.presentModes[i] == wgpu::PresentMode::Immediate) + { + return wgpu::PresentMode::Immediate; + } + } + + for (size_t i = 0; i < capabilities.presentModeCount; i++) + { + if (capabilities.presentModes[i] == wgpu::PresentMode::Mailbox) + { + return wgpu::PresentMode::Mailbox; + } + } + + return wgpu::PresentMode::Fifo; +} +} // namespace + +PlatformWebGPU::PlatformWebGPU(wgpu::Device device) : device_(device) {} + +PlatformWebGPU::~PlatformWebGPU() { ResetCurrentScreen(); } + +void PlatformWebGPU::ResetCurrentScreen() +{ + if (surface_ != nullptr && surfaceTexture_.texture != nullptr && isPresentRequested_ && !hasPresentedCurrentSurface_) + { + surface_.Present(); + hasPresentedCurrentSurface_ = true; + } + + SafeRelease(currentScreenRenderPass_); + SafeRelease(currentScreenTexture_); + surfaceTexture_ = {}; + hasPresentedCurrentSurface_ = false; + isPresentRequested_ = false; +} + +bool PlatformWebGPU::ConfigureSurface(const Vec2I& windowSize) +{ + if (surface_ == nullptr || adapter_ == nullptr || device_ == nullptr || windowSize.X <= 0 || windowSize.Y <= 0) + { + return false; + } + + wgpu::SurfaceCapabilities capabilities{}; + surface_.GetCapabilities(adapter_, &capabilities); + if (capabilities.formatCount == 0) + { + Log(LogType::Error, "WebGPU surface has no supported formats."); + return false; + } + + surfaceFormat_ = capabilities.formats[0]; + presentMode_ = SelectPresentMode(capabilities, waitVSync_); + + wgpu::SurfaceConfiguration config{}; + config.device = device_; + config.format = surfaceFormat_; + config.width = static_cast(windowSize.X); + config.height = static_cast(windowSize.Y); + config.presentMode = presentMode_; + config.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst; + surface_.Configure(&config); + + windowSize_ = windowSize; + return true; +} + +bool PlatformWebGPU::Initialize(Window* window, bool waitVSync) +{ + waitVSync_ = waitVSync; + window_ = window; + if (window_ == nullptr) + { + return false; + } + +#if defined(_WIN32) && !defined(__EMSCRIPTEN__) + wgpu::InstanceDescriptor instanceDescriptor{}; + static constexpr auto timedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny; + instanceDescriptor.requiredFeatureCount = 1; + instanceDescriptor.requiredFeatures = &timedWaitAny; + instance_ = wgpu::CreateInstance(&instanceDescriptor); + if (instance_ == nullptr) + { + Log(LogType::Error, "Failed to create WebGPU instance."); + return false; + } + + wgpu::SurfaceSourceWindowsHWND hwndSource{}; + hwndSource.hinstance = GetModuleHandleW(nullptr); + hwndSource.hwnd = window_->GetNativePtr(0); + + wgpu::SurfaceDescriptor surfaceDescriptor{}; + surfaceDescriptor.nextInChain = &hwndSource; + surface_ = instance_.CreateSurface(&surfaceDescriptor); + if (surface_ == nullptr) + { + Log(LogType::Error, "Failed to create WebGPU surface."); + return false; + } + + wgpu::RequestAdapterOptions adapterOptions{}; + adapterOptions.compatibleSurface = surface_; + adapterOptions.powerPreference = wgpu::PowerPreference::HighPerformance; + instance_.WaitAny( + instance_.RequestAdapter(&adapterOptions, + wgpu::CallbackMode::WaitAnyOnly, + [this](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter, wgpu::StringView message) { + if (status != wgpu::RequestAdapterStatus::Success) + { + Log(LogType::Error, + std::string("Failed to request WebGPU adapter: ") + std::string(message.data, message.length)); + return; + } + adapter_ = adapter; + }), + std::numeric_limits::max()); + if (adapter_ == nullptr) + { + return false; + } + + wgpu::DeviceDescriptor deviceDescriptor{}; + std::vector requiredFeatures; + for (auto feature : {wgpu::FeatureName::Float32Filterable, wgpu::FeatureName::TextureFormatsTier2}) + { + if (adapter_.HasFeature(feature)) + { + requiredFeatures.push_back(feature); + } + } + deviceDescriptor.requiredFeatureCount = requiredFeatures.size(); + deviceDescriptor.requiredFeatures = requiredFeatures.data(); + deviceDescriptor.SetUncapturedErrorCallback([](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView message) { + Log(LogType::Error, std::string("WebGPU validation error: ") + std::string(message.data, message.length)); + }); + + instance_.WaitAny( + adapter_.RequestDevice(&deviceDescriptor, + wgpu::CallbackMode::WaitAnyOnly, + [this](wgpu::RequestDeviceStatus status, wgpu::Device device, wgpu::StringView message) { + if (status != wgpu::RequestDeviceStatus::Success) + { + Log(LogType::Error, + std::string("Failed to request WebGPU device: ") + std::string(message.data, message.length)); + return; + } + device_ = device; + }), + std::numeric_limits::max()); + if (device_ == nullptr) + { + return false; + } + + return ConfigureSurface(window_->GetWindowSize()); +#else + Log(LogType::Error, "WebGPU window platform initialization is implemented for Windows only."); + return false; +#endif +} + +bool PlatformWebGPU::Initialize(wgpu::Device device, bool waitVSync) +{ + waitVSync_ = waitVSync; + device_ = device; + return device_ != nullptr; +} + +int PlatformWebGPU::GetCurrentFrameIndex() const { return 0; } + +int PlatformWebGPU::GetMaxFrameCount() const { return 1; } + +bool PlatformWebGPU::NewFrame() +{ + if (device_ == nullptr) + { + return false; + } + + if (window_ != nullptr) + { + if (!window_->OnNewFrame()) + { + return false; + } + + const auto windowSize = window_->GetWindowSize(); + if (windowSize != windowSize_) + { + SetWindowSize(windowSize); + } + } + + if (surface_ == nullptr) + { + return true; + } + + ResetCurrentScreen(); + return true; +} + +bool PlatformWebGPU::AcquireCurrentScreen() +{ + if (surface_ == nullptr) + { + return false; + } + + if (currentScreenRenderPass_ != nullptr) + { + return true; + } + + surface_.GetCurrentTexture(&surfaceTexture_); + if (!IsSurfaceTextureAcquired(surfaceTexture_.status) || surfaceTexture_.texture == nullptr) + { + if (surfaceTexture_.status == wgpu::SurfaceGetCurrentTextureStatus::Outdated || + surfaceTexture_.status == wgpu::SurfaceGetCurrentTextureStatus::Lost) + { + ConfigureSurface(windowSize_); + } + return false; + } + + TextureParameter textureParameter{}; + textureParameter.Usage = TextureUsageType::RenderTarget; + textureParameter.Format = ConvertFormat(surfaceFormat_); + textureParameter.Dimension = 2; + textureParameter.Size = Vec3I(windowSize_.X, windowSize_.Y, 1); + textureParameter.MipLevelCount = 1; + textureParameter.SampleCount = 1; + + currentScreenTexture_ = new TextureWebGPU(); + if (!currentScreenTexture_->InitializeFromSurfaceTexture(device_, surfaceTexture_.texture, textureParameter)) + { + ResetCurrentScreen(); + return false; + } + + Texture* textures[] = {currentScreenTexture_}; + currentScreenRenderPass_ = new RenderPassWebGPU(); + if (!currentScreenRenderPass_->Initialize(textures, 1, nullptr, nullptr, nullptr)) + { + ResetCurrentScreen(); + return false; + } + + return true; +} + +void PlatformWebGPU::Present() +{ + if (surface_ != nullptr && surfaceTexture_.texture != nullptr) + { + isPresentRequested_ = true; + } +} + +Graphics* PlatformWebGPU::CreateGraphics() +{ + if (device_ == nullptr) + { + return nullptr; + } + auto ret = new GraphicsWebGPU(device_, instance_); + ret->SetWindowSize(windowSize_); + return ret; +} + +void PlatformWebGPU::SetWindowSize(const Vec2I& windowSize) +{ + if (windowSize == windowSize_) + { + return; + } + ConfigureSurface(windowSize); +} + +RenderPass* PlatformWebGPU::GetCurrentScreen(const Color8& clearColor, bool isColorCleared, bool isDepthCleared) +{ + if (currentScreenRenderPass_ == nullptr) + { + AcquireCurrentScreen(); + } + + if (currentScreenRenderPass_ == nullptr) + { + return nullptr; + } + + currentScreenRenderPass_->SetClearColor(clearColor); + currentScreenRenderPass_->SetIsColorCleared(isColorCleared); + currentScreenRenderPass_->SetIsDepthCleared(isDepthCleared); + return currentScreenRenderPass_; +} + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.PlatformWebGPU.h b/src/WebGPU/LLGI.PlatformWebGPU.h new file mode 100644 index 00000000..fbad8d1b --- /dev/null +++ b/src/WebGPU/LLGI.PlatformWebGPU.h @@ -0,0 +1,52 @@ +#pragma once + +#include "../LLGI.Platform.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ + +class RenderPassWebGPU; +class TextureWebGPU; + +class PlatformWebGPU : public Platform +{ +private: + Window* window_ = nullptr; + Vec2I windowSize_; + wgpu::Instance instance_; + wgpu::Adapter adapter_; + wgpu::Device device_; + wgpu::Surface surface_; + wgpu::TextureFormat surfaceFormat_ = wgpu::TextureFormat::Undefined; + wgpu::PresentMode presentMode_ = wgpu::PresentMode::Fifo; + wgpu::SurfaceTexture surfaceTexture_; + TextureWebGPU* currentScreenTexture_ = nullptr; + RenderPassWebGPU* currentScreenRenderPass_ = nullptr; + bool hasPresentedCurrentSurface_ = false; + bool isPresentRequested_ = false; + + bool ConfigureSurface(const Vec2I& windowSize); + bool AcquireCurrentScreen(); + void ResetCurrentScreen(); + +public: + PlatformWebGPU() = default; + explicit PlatformWebGPU(wgpu::Device device); + ~PlatformWebGPU() override; + + bool Initialize(Window* window, bool waitVSync); + bool Initialize(wgpu::Device device, bool waitVSync); + + int GetCurrentFrameIndex() const override; + int GetMaxFrameCount() const override; + + bool NewFrame() override; + void Present() override; + Graphics* CreateGraphics() override; + DeviceType GetDeviceType() const override { return DeviceType::WebGPU; } + void SetWindowSize(const Vec2I& windowSize) override; + RenderPass* GetCurrentScreen(const Color8& clearColor, bool isColorCleared, bool isDepthCleared) override; +}; + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.cpp b/src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.cpp new file mode 100644 index 00000000..fc02a044 --- /dev/null +++ b/src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.cpp @@ -0,0 +1,19 @@ +#include "LLGI.RenderPassPipelineStateWebGPU.h" + +namespace LLGI +{ + +void RenderPassPipelineStateWebGPU::SetKey(const RenderPassPipelineStateKey& key) +{ + Key = key; + pixelFormats_.resize(key.RenderTargetFormats.size()); + + for (size_t i = 0; i < pixelFormats_.size(); i++) + { + pixelFormats_.at(i) = ConvertFormat(key.RenderTargetFormats.at(i)); + } + + depthStencilFormat_ = ConvertFormat(key.DepthFormat); +} + +} // namespace LLGI \ No newline at end of file diff --git a/src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.h b/src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.h new file mode 100644 index 00000000..ed962efe --- /dev/null +++ b/src/WebGPU/LLGI.RenderPassPipelineStateWebGPU.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../LLGI.Graphics.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ +class RenderPassPipelineStateWebGPU : public RenderPassPipelineState +{ +private: + FixedSizeVector pixelFormats_; + wgpu::TextureFormat depthStencilFormat_ = wgpu::TextureFormat::Undefined; + +public: + void SetKey(const RenderPassPipelineStateKey& key); + + const FixedSizeVector& GetPixelFormats() const { return pixelFormats_; } + const wgpu::TextureFormat& GetDepthStencilFormat() const { return depthStencilFormat_; } +}; + +} // namespace LLGI \ No newline at end of file diff --git a/src/WebGPU/LLGI.RenderPassWebGPU.cpp b/src/WebGPU/LLGI.RenderPassWebGPU.cpp new file mode 100644 index 00000000..975e4d1a --- /dev/null +++ b/src/WebGPU/LLGI.RenderPassWebGPU.cpp @@ -0,0 +1,128 @@ +#include "LLGI.RenderPassWebGPU.h" +#include "LLGI.TextureWebGPU.h" + +namespace LLGI +{ + +bool RenderPassWebGPU::Initialize( + Texture** textures, int textureCount, Texture* depthTexture, Texture* resolvedRenderTexture, Texture* resolvedDepthTexture) +{ + if (!assignRenderTextures(textures, textureCount)) + { + return false; + } + + if (!assignDepthTexture(depthTexture)) + { + return false; + } + + if (!assignResolvedRenderTexture(resolvedRenderTexture)) + { + return false; + } + + if (!assignResolvedDepthTexture(resolvedDepthTexture)) + { + return false; + } + + if (!getSize(screenSize_, (const Texture**)textures, textureCount, depthTexture)) + { + return false; + } + + std::array texturesImpl; + texturesImpl.fill(nullptr); + TextureWebGPU* depthTextureImpl = nullptr; + + for (int32_t i = 0; i < textureCount; i++) + { + if (textures[i] == nullptr) + continue; + + texturesImpl.at(i) = reinterpret_cast(textures[i]); + } + + if (depthTexture != nullptr) + { + depthTextureImpl = reinterpret_cast(depthTexture); + } + + TextureWebGPU* resolvedTextureImpl = nullptr; + TextureWebGPU* resolvedDepthTextureImpl = nullptr; + + if (resolvedRenderTexture != nullptr) + { + resolvedTextureImpl = reinterpret_cast(resolvedRenderTexture); + } + + if (resolvedDepthTexture != nullptr) + { + resolvedDepthTextureImpl = reinterpret_cast(resolvedDepthTexture); + } + + descriptor_.colorAttachmentCount = textureCount; + descriptor_.colorAttachments = colorAttachments_.data(); + + for (int i = 0; i < textureCount; i++) + { + colorAttachments_[i].view = texturesImpl[i]->GetTextureView(); + + if (GetIsColorCleared()) + { + colorAttachments_[i].loadOp = wgpu::LoadOp::Clear; + colorAttachments_[i].storeOp = wgpu::StoreOp::Store; + colorAttachments_[i].clearValue = { + GetClearColor().R / 255.0, GetClearColor().G / 255.0, GetClearColor().B / 255.0, GetClearColor().A / 255.0}; + } + else + { + colorAttachments_[i].loadOp = wgpu::LoadOp::Load; + colorAttachments_[i].storeOp = wgpu::StoreOp::Store; + colorAttachments_[i].clearValue = {0, 0, 0, 1}; + } + + if (resolvedTextureImpl != nullptr) + { + colorAttachments_[i].resolveTarget = resolvedTextureImpl->GetTextureView(); + colorAttachments_[i].storeOp = wgpu::StoreOp::Store; + } + } + + if (depthTexture != nullptr) + { + depthStencilAttachiment_.view = depthTextureImpl->GetTextureView(); + + if (GetIsDepthCleared()) + { + depthStencilAttachiment_.depthLoadOp = wgpu::LoadOp::Clear; + depthStencilAttachiment_.depthStoreOp = wgpu::StoreOp::Store; + depthStencilAttachiment_.depthClearValue = 1.0f; + } + else + { + depthStencilAttachiment_.depthLoadOp = wgpu::LoadOp::Load; + depthStencilAttachiment_.depthStoreOp = wgpu::StoreOp::Store; + depthStencilAttachiment_.depthClearValue = 1.0f; + } + + if (depthTextureImpl->GetFormat() == TextureFormatType::D24S8 || depthTextureImpl->GetFormat() == TextureFormatType::D32S8) + { + depthStencilAttachiment_.stencilLoadOp = GetIsDepthCleared() ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load; + depthStencilAttachiment_.stencilStoreOp = wgpu::StoreOp::Store; + depthStencilAttachiment_.stencilClearValue = 0; + } + + if (resolvedDepthTextureImpl != nullptr) + { + // ? + } + + descriptor_.depthStencilAttachment = &depthStencilAttachiment_; + } + + return true; +} + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.RenderPassWebGPU.h b/src/WebGPU/LLGI.RenderPassWebGPU.h new file mode 100644 index 00000000..b1b4dcc2 --- /dev/null +++ b/src/WebGPU/LLGI.RenderPassWebGPU.h @@ -0,0 +1,23 @@ +#pragma once + +#include "../LLGI.Graphics.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ +class TextureWebGPU; + +class RenderPassWebGPU : public RenderPass +{ + wgpu::RenderPassDescriptor descriptor_; + std::array colorAttachments_; + wgpu::RenderPassDepthStencilAttachment depthStencilAttachiment_; + +public: + bool + Initialize(Texture** textures, int textureCount, Texture* depthTexture, Texture* resolvedRenderTexture, Texture* resolvedDepthTexture); + + const wgpu::RenderPassDescriptor& GetDescriptor() const { return descriptor_; } +}; + +} // namespace LLGI \ No newline at end of file diff --git a/src/WebGPU/LLGI.ShaderWebGPU.cpp b/src/WebGPU/LLGI.ShaderWebGPU.cpp new file mode 100644 index 00000000..dc01a7d7 --- /dev/null +++ b/src/WebGPU/LLGI.ShaderWebGPU.cpp @@ -0,0 +1,66 @@ +#include "LLGI.ShaderWebGPU.h" +#include +#include + +namespace LLGI +{ +ShaderWebGPU::ShaderWebGPU() {} + +ShaderWebGPU::~ShaderWebGPU() {} + +bool ShaderWebGPU::Initialize(wgpu::Device& device, DataStructure* data, int32_t count) +{ + static const char wgslHeader[] = {'w', 'g', 's', 'l', 'c', 'o', 'd', 'e'}; + + if (data == nullptr || count == 0) + { + return false; + } + + wgpu::ShaderModuleDescriptor desc = {}; + + if (data[0].Data == nullptr || data[0].Size <= 0) + { + return false; + } + + const auto* bytes = static_cast(data[0].Data); + const bool hasWGSLHeader = data[0].Size >= static_cast(sizeof(wgslHeader)) && + memcmp(bytes, wgslHeader, sizeof(wgslHeader)) == 0; + const bool hasSPIRVMagic = data[0].Size >= 4 && bytes[0] == 0x03 && bytes[1] == 0x02 && bytes[2] == 0x23 && bytes[3] == 0x07; + + wgpu::ShaderSourceSPIRV sprivDesc = {}; + wgpu::ShaderSourceWGSL wgslDesc = {}; + std::string wgslCode; + + if (!hasSPIRVMagic) + { + const auto codeOffset = hasWGSLHeader ? sizeof(wgslHeader) : 0; + if (data[0].Size <= static_cast(codeOffset)) + { + return false; + } + + wgslCode.assign(reinterpret_cast(bytes + codeOffset), static_cast(data[0].Size) - codeOffset); + while (!wgslCode.empty() && wgslCode.back() == '\0') + { + wgslCode.pop_back(); + } + wgslDesc.code = wgpu::StringView(wgslCode.data(), wgslCode.size()); + desc.nextInChain = reinterpret_cast(&wgslDesc); + } + else + { + sprivDesc.codeSize = data[0].Size / sizeof(uint32_t); + sprivDesc.code = reinterpret_cast(data[0].Data); + desc.nextInChain = reinterpret_cast(&sprivDesc); + } + + shaderModule_ = device.CreateShaderModule(&desc); + + return shaderModule_ != nullptr; +} + +wgpu::ShaderModule& ShaderWebGPU::GetShaderModule() { return shaderModule_; } + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.ShaderWebGPU.h b/src/WebGPU/LLGI.ShaderWebGPU.h new file mode 100644 index 00000000..0cd74907 --- /dev/null +++ b/src/WebGPU/LLGI.ShaderWebGPU.h @@ -0,0 +1,23 @@ +#pragma once + +#include "LLGI.BaseWebGPU.h" +#include "../LLGI.Shader.h" + +namespace LLGI +{ + +class ShaderWebGPU : public Shader +{ +private: + wgpu::ShaderModule shaderModule_; + +public: + ShaderWebGPU(); + ~ShaderWebGPU() override; + + bool Initialize(wgpu::Device& device, DataStructure* data, int32_t count); + + wgpu::ShaderModule& GetShaderModule(); +}; + +} \ No newline at end of file diff --git a/src/WebGPU/LLGI.TextureWebGPU.cpp b/src/WebGPU/LLGI.TextureWebGPU.cpp new file mode 100644 index 00000000..06fc90b1 --- /dev/null +++ b/src/WebGPU/LLGI.TextureWebGPU.cpp @@ -0,0 +1,264 @@ +#include "LLGI.TextureWebGPU.h" + +#include +#include +#include + +namespace LLGI +{ + +namespace +{ +uint32_t AlignTo(uint32_t value, uint32_t alignment) +{ + return (value + alignment - 1) / alignment * alignment; +} +} // namespace + +bool TextureWebGPU::Initialize(wgpu::Device& device, const TextureParameter& parameter, wgpu::Instance instance) +{ + device_ = device; + instance_ = instance; + parameter_ = parameter; + + const auto getDimension = [](int dimension) + { + if (dimension == 1) + return wgpu::TextureDimension::e1D; + + if (dimension == 2) + return wgpu::TextureDimension::e2D; + + if (dimension == 3) + return wgpu::TextureDimension::e3D; + + throw "Not implemented"; + }; + + const auto getViewDimension = [](int dimension) + { + if (dimension == 1) + return wgpu::TextureViewDimension::e1D; + + if (dimension == 2) + return wgpu::TextureViewDimension::e2D; + + if (dimension == 3) + return wgpu::TextureViewDimension::e3D; + + throw "Not implemented"; + }; + + { + wgpu::TextureDescriptor texDesc{}; + + texDesc.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::CopySrc; + if ((parameter.Usage & TextureUsageType::RenderTarget) != TextureUsageType::NoneFlag) + { + texDesc.usage |= wgpu::TextureUsage::RenderAttachment; + } + + if (BitwiseContains(parameter.Usage, TextureUsageType::Storage)) + { + texDesc.usage |= wgpu::TextureUsage::StorageBinding; + } + + if ((parameter.Usage & TextureUsageType::External) != TextureUsageType::NoneFlag) + { + throw "Not implemented"; + // texDesc.usage |= dawn::platform::kPresentTextureUsage; + } + + bool isArray = false; + if ((parameter.Usage & TextureUsageType::Array) != TextureUsageType::NoneFlag) + { + isArray = true; + } + + texDesc.dimension = getDimension(parameter.Dimension); + texDesc.format = ConvertFormat(parameter.Format); + texDesc.mipLevelCount = parameter.MipLevelCount; + texDesc.sampleCount = parameter.SampleCount; + texDesc.size.width = parameter.Size.X; + texDesc.size.height = parameter.Size.Y; + texDesc.size.depthOrArrayLayers = parameter.Size.Z; + + texture_ = device.CreateTexture(&texDesc); + if (texture_ == nullptr) + { + return false; + } + + wgpu::TextureViewDescriptor texViewDesc{}; + texViewDesc.format = texDesc.format; + texViewDesc.dimension = isArray && parameter.Dimension == 2 ? wgpu::TextureViewDimension::e2DArray : getViewDimension(parameter.Dimension); + texViewDesc.baseMipLevel = 0; + texViewDesc.mipLevelCount = texDesc.mipLevelCount; + texViewDesc.baseArrayLayer = 0; + texViewDesc.arrayLayerCount = isArray ? parameter.Size.Z : 1; + texViewDesc.aspect = wgpu::TextureAspect::All; + + textureView_ = texture_.CreateView(&texViewDesc); + } + + format_ = parameter.Format; + usage_ = parameter.Usage; + samplingCount_ = parameter.SampleCount; + mipmapCount_ = parameter.MipLevelCount; + type_ = TextureType::Color; + if (IsDepthFormat(parameter.Format)) + { + type_ = TextureType::Depth; + } + else if (BitwiseContains(parameter.Usage, TextureUsageType::RenderTarget)) + { + type_ = TextureType::Render; + } + + return texture_ != nullptr && textureView_ != nullptr; +} + +bool TextureWebGPU::InitializeFromSurfaceTexture(wgpu::Device& device, wgpu::Texture texture, const TextureParameter& parameter) +{ + device_ = device; + parameter_ = parameter; + texture_ = texture; + if (texture_ == nullptr) + { + return false; + } + + wgpu::TextureViewDescriptor texViewDesc{}; + texViewDesc.format = ConvertFormat(parameter.Format); + texViewDesc.dimension = wgpu::TextureViewDimension::e2D; + texViewDesc.baseMipLevel = 0; + texViewDesc.mipLevelCount = 1; + texViewDesc.baseArrayLayer = 0; + texViewDesc.arrayLayerCount = 1; + texViewDesc.aspect = wgpu::TextureAspect::All; + textureView_ = texture_.CreateView(&texViewDesc); + + format_ = parameter.Format; + usage_ = parameter.Usage; + samplingCount_ = parameter.SampleCount; + mipmapCount_ = parameter.MipLevelCount; + type_ = TextureType::Screen; + + return textureView_ != nullptr; +} + +void* TextureWebGPU::Lock() +{ + auto cpuMemorySize = GetTextureMemorySize(format_, parameter_.Size); + temp_buffer_.resize(cpuMemorySize); + return temp_buffer_.data(); +} + +void TextureWebGPU::Unlock() +{ + wgpu::TexelCopyTextureInfo imageCopyTexture{}; + imageCopyTexture.texture = texture_; + imageCopyTexture.aspect = wgpu::TextureAspect::All; + + wgpu::TexelCopyBufferLayout textureDataLayout; + textureDataLayout.bytesPerRow = GetTextureRowPitch(format_, parameter_.Size); + wgpu::Extent3D extent; + extent.width = parameter_.Size.X; + extent.height = parameter_.Size.Y; + extent.depthOrArrayLayers = parameter_.Size.Z; + device_.GetQueue().WriteTexture(&imageCopyTexture, temp_buffer_.data(), temp_buffer_.size(), &textureDataLayout, &extent); +} + +bool TextureWebGPU::GetData(std::vector& data) +{ + const auto bytesPerRowUnaligned = static_cast(GetTextureRowPitch(format_, parameter_.Size)); + const auto bytesPerRow = AlignTo(bytesPerRowUnaligned, 256); + const auto height = static_cast(parameter_.Size.Y); + const auto depth = static_cast(parameter_.Size.Z); + const auto bufferSize = static_cast(bytesPerRow) * height * depth; + + wgpu::BufferDescriptor bufferDesc{}; + bufferDesc.size = bufferSize; + bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead; + auto readbackBuffer = device_.CreateBuffer(&bufferDesc); + + wgpu::CommandEncoderDescriptor encoderDesc{}; + auto encoder = device_.CreateCommandEncoder(&encoderDesc); + + wgpu::TexelCopyTextureInfo src{}; + src.texture = texture_; + src.aspect = wgpu::TextureAspect::All; + + wgpu::TexelCopyBufferInfo dst{}; + dst.buffer = readbackBuffer; + dst.layout.bytesPerRow = bytesPerRow; + dst.layout.rowsPerImage = height; + + wgpu::Extent3D extent{}; + extent.width = static_cast(parameter_.Size.X); + extent.height = height; + extent.depthOrArrayLayers = depth; + encoder.CopyTextureToBuffer(&src, &dst, &extent); + + auto commandBuffer = encoder.Finish(); + device_.GetQueue().Submit(1, &commandBuffer); + + bool completed = false; + bool succeeded = false; + auto future = readbackBuffer.MapAsync(wgpu::MapMode::Read, + 0, + bufferSize, + instance_ != nullptr ? wgpu::CallbackMode::WaitAnyOnly : wgpu::CallbackMode::AllowProcessEvents, + [&completed, &succeeded](wgpu::MapAsyncStatus status, wgpu::StringView) { + succeeded = status == wgpu::MapAsyncStatus::Success; + completed = true; + }); + + if (instance_ != nullptr) + { + instance_.WaitAny(future, 5ULL * 1000ULL * 1000ULL * 1000ULL); + } + else + { + const auto waitStart = std::chrono::steady_clock::now(); + while (!completed) + { + device_.Tick(); + if (std::chrono::steady_clock::now() - waitStart > std::chrono::seconds(5)) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + if (!succeeded) + { + return false; + } + + data.resize(static_cast(bytesPerRowUnaligned) * height * depth); + const auto mapped = static_cast(readbackBuffer.GetConstMappedRange(0, bufferSize)); + for (uint32_t z = 0; z < depth; z++) + { + for (uint32_t y = 0; y < height; y++) + { + memcpy(data.data() + (static_cast(z) * height + y) * bytesPerRowUnaligned, + mapped + (static_cast(z) * height + y) * bytesPerRow, + bytesPerRowUnaligned); + } + } + readbackBuffer.Unmap(); + return true; +} + +Vec2I TextureWebGPU::GetSizeAs2D() const { return Vec2I(parameter_.Size.X, parameter_.Size.Y); } + +bool TextureWebGPU::IsRenderTexture() const +{ + return type_ == TextureType::Render || type_ == TextureType::Screen; +} + +bool TextureWebGPU::IsDepthTexture() const { return type_ == TextureType::Depth; } + +} // namespace LLGI diff --git a/src/WebGPU/LLGI.TextureWebGPU.h b/src/WebGPU/LLGI.TextureWebGPU.h new file mode 100644 index 00000000..76354bb9 --- /dev/null +++ b/src/WebGPU/LLGI.TextureWebGPU.h @@ -0,0 +1,37 @@ +#pragma once + +#include "../LLGI.Graphics.h" +#include "../LLGI.Texture.h" +#include "LLGI.BaseWebGPU.h" + +namespace LLGI +{ + +class TextureWebGPU : public Texture +{ + wgpu::Device device_; + wgpu::Instance instance_; + + wgpu::Texture texture_; + wgpu::TextureView textureView_; + TextureParameter parameter_; + std::vector temp_buffer_; + +public: + bool Initialize(wgpu::Device& device, const TextureParameter& parameter, wgpu::Instance instance = nullptr); + bool InitializeFromSurfaceTexture(wgpu::Device& device, wgpu::Texture texture, const TextureParameter& parameter); + void* Lock() override; + void Unlock() override; + bool GetData(std::vector& data) override; + Vec2I GetSizeAs2D() const override; + bool IsRenderTexture() const override; + bool IsDepthTexture() const override; + + const TextureParameter& GetParameter() const { return parameter_; } + + wgpu::Texture GetTexture() const { return texture_; } + + wgpu::TextureView GetTextureView() const { return textureView_; } +}; + +} // namespace LLGI diff --git a/src_test/Shaders/WebGPU/basic.comp b/src_test/Shaders/WebGPU/basic.comp new file mode 100644 index 00000000..413bfbcd --- /dev/null +++ b/src_test/Shaders/WebGPU/basic.comp @@ -0,0 +1,42 @@ +diagnostic(off, derivative_uniformity); + +struct CS_OUTPUT { + value : f32, +} + +struct S { + m : array, +} + +@group(2) @binding(1) var write_1 : S; + +struct CS_INPUT { + value1 : f32, + value2 : f32, +} + +struct read_2 { + m_1 : array, +} + +@group(2) @binding(0) var read_1 : read_2; + +struct CB { + offset : f32, +} + +@group(0) @binding(0) var v : CB; + +@compute @workgroup_size(1u, 1u, 1u) +fn main(@builtin(global_invocation_id) dtid : vec3) { + var dtid_1 : vec3; + var param : vec3; + dtid_1 = dtid; + param = dtid_1; + v_1(&(param)); +} + +fn v_1(dtid : ptr>) { + let v_2 = (*(dtid)).x; + write_1.m[v_2].value = ((read_1.m_1[(*(dtid)).x].value1 * read_1.m_1[(*(dtid)).x].value2) + v.offset); +} diff --git a/src_test/Shaders/WebGPU/instancing.vert b/src_test/Shaders/WebGPU/instancing.vert new file mode 100644 index 00000000..0b88b11b --- /dev/null +++ b/src_test/Shaders/WebGPU/instancing.vert @@ -0,0 +1,62 @@ +diagnostic(off, derivative_uniformity); + +struct CB { + offsets : array, 10u>, +} + +@group(0) @binding(0) var v : CB; + +var v_1 : vec4; + +var v_2 : vec4; + +struct VS_INPUT { + g_position : vec3, + g_uv : vec2, + g_color : vec4, + InstanceId : u32, +} + +struct VS_OUTPUT { + g_position : vec4, + g_color : vec4, +} + +fn main_inner(v_3 : vec3, v_4 : vec2, v_5 : vec4, v_6 : u32) { + var input : VS_INPUT; + var flattenTemp : VS_OUTPUT; + var param : VS_INPUT; + input.g_position = v_3; + input.g_uv = v_4; + input.g_color = v_5; + input.InstanceId = v_6; + param = input; + flattenTemp = v_7(&(param)); + v_1 = flattenTemp.g_position; + v_2 = flattenTemp.g_color; +} + +fn v_7(input : ptr) -> VS_OUTPUT { + var output : VS_OUTPUT; + let v_8 = (*(input)).g_position; + output.g_position = vec4(v_8.x, v_8.y, v_8.z, 1.0f); + let v_9 = v.offsets[(*(input)).InstanceId].x; + output.g_position.x = (output.g_position.x + v_9); + let v_10 = v.offsets[(*(input)).InstanceId].y; + output.g_position.y = (output.g_position.y + v_10); + output.g_color = (*(input)).g_color; + return output; +} + +struct tint_symbol { + @builtin(position) + m : vec4, + @location(0u) + m_1 : vec4, +} + +@vertex +fn main(@location(0u) v_11 : vec3, @location(1u) v_12 : vec2, @location(2u) v_13 : vec4, @builtin(instance_index) v_14 : u32) -> tint_symbol { + main_inner(v_11, v_12, v_13, v_14); + return tint_symbol(v_1, v_2); +} diff --git a/src_test/Shaders/WebGPU/readwrite.comp b/src_test/Shaders/WebGPU/readwrite.comp new file mode 100644 index 00000000..c1c3839b --- /dev/null +++ b/src_test/Shaders/WebGPU/readwrite.comp @@ -0,0 +1,42 @@ +diagnostic(off, derivative_uniformity); + +struct CS_OUTPUT { + value : f32, +} + +struct S { + m : array, +} + +@group(2) @binding(1) var write_1 : S; + +struct CS_INPUT { + value1 : f32, + value2 : f32, +} + +struct read_2 { + m_1 : array, +} + +@group(2) @binding(0) var read_1 : read_2; + +struct CB { + offset : f32, +} + +@group(0) @binding(0) var v : CB; + +@compute @workgroup_size(1u, 1u, 1u) +fn main(@builtin(global_invocation_id) dtid : vec3) { + var dtid_1 : vec3; + var param : vec3; + dtid_1 = dtid; + param = dtid_1; + v_1(&(param)); +} + +fn v_1(dtid : ptr>) { + let v_2 = (*(dtid)).x; + write_1.m[v_2].value = ((read_1.m_1[(*(dtid)).x].value1 * read_1.m_1[(*(dtid)).x].value2) + v.offset); +} diff --git a/src_test/Shaders/WebGPU/readwrite_texture.comp b/src_test/Shaders/WebGPU/readwrite_texture.comp new file mode 100644 index 00000000..9be01f55 --- /dev/null +++ b/src_test/Shaders/WebGPU/readwrite_texture.comp @@ -0,0 +1,26 @@ +diagnostic(off, derivative_uniformity); + +@group(1) @binding(1) var read1 : texture_storage_2d; + +@group(1) @binding(2) var read2 : texture_2d; + +@group(2) @binding(2) var read2_sampler : sampler; + +@group(1) @binding(0) var v : texture_storage_2d; + +@compute @workgroup_size(1u, 1u, 1u) +fn main(@builtin(global_invocation_id) dtid : vec3) { + var dtid_1 : vec3; + var param : vec3; + dtid_1 = dtid; + param = dtid_1; + v_1(&(param)); +} + +fn v_1(dtid : ptr>) { + var index : vec2; + var storeTemp : vec4; + index = (*(dtid)).xy; + storeTemp = (textureLoad(read1, index) + textureSampleLevel(read2, read2_sampler, vec2(), 0.0f)); + textureStore(v, index, storeTemp); +} diff --git a/src_test/Shaders/WebGPU/simple_compute_rectangle.frag b/src_test/Shaders/WebGPU/simple_compute_rectangle.frag new file mode 100644 index 00000000..e10eb03f --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_compute_rectangle.frag @@ -0,0 +1,46 @@ +diagnostic(off, derivative_uniformity); + +struct compute { + m : array, +} + +@group(1) @binding(0) var compute_1 : compute; + +var v : vec4; + +struct CB { + offset : vec4, +} + +@group(0) @binding(1) var v_1 : CB; + +struct PS_INPUT { + Position : vec4, + UV : vec2, + Color : vec4, +} + +fn main_inner(v_2 : vec4, v_3 : vec2, v_4 : vec4) { + var input : PS_INPUT; + var param : PS_INPUT; + input.Position = v_2; + input.UV = v_3; + input.Color = v_4; + param = input; + v = v_5(&(param)); +} + +fn v_5(input : ptr) -> vec4 { + var c : vec4; + let v_6 = (*(input)).Color; + let v_7 = compute_1.m[0i]; + c = (v_6 + vec4(v_7, v_7, v_7, v_7)); + c.w = 1.0f; + return c; +} + +@fragment +fn main(@builtin(position) v_8 : vec4, @location(0u) v_9 : vec2, @location(1u) v_10 : vec4) -> @location(0u) vec4 { + main_inner(v_8, v_9, v_10); + return v; +} diff --git a/src_test/Shaders/WebGPU/simple_compute_rectangle.vert b/src_test/Shaders/WebGPU/simple_compute_rectangle.vert new file mode 100644 index 00000000..7229e532 --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_compute_rectangle.vert @@ -0,0 +1,64 @@ +diagnostic(off, derivative_uniformity); + +struct compute { + m : array, +} + +@group(1) @binding(0) var compute_1 : compute; + +var v : vec4; + +var v_1 : vec2; + +var v_2 : vec4; + +struct VS_INPUT { + Position : vec3, + UV : vec2, + Color : vec4, +} + +struct VS_OUTPUT { + Position : vec4, + UV : vec2, + Color : vec4, +} + +fn main_inner(v_3 : vec3, v_4 : vec2, v_5 : vec4) { + var input : VS_INPUT; + var flattenTemp : VS_OUTPUT; + var param : VS_INPUT; + input.Position = v_3; + input.UV = v_4; + input.Color = v_5; + param = input; + flattenTemp = v_6(&(param)); + v = flattenTemp.Position; + v_1 = flattenTemp.UV; + v_2 = flattenTemp.Color; +} + +fn v_6(input : ptr) -> VS_OUTPUT { + var output : VS_OUTPUT; + let v_7 = (*(input)).Position; + let v_8 = compute_1.m[0i]; + output.Position = (vec4(v_7.x, v_7.y, v_7.z, 1.0f) + vec4(v_8, v_8, v_8, v_8)); + output.UV = (*(input)).UV; + output.Color = (*(input)).Color; + return output; +} + +struct tint_symbol { + @builtin(position) + m_1 : vec4, + @location(0u) + m_2 : vec2, + @location(1u) + m_3 : vec4, +} + +@vertex +fn main(@location(0u) v_9 : vec3, @location(1u) v_10 : vec2, @location(2u) v_11 : vec4) -> tint_symbol { + main_inner(v_9, v_10, v_11); + return tint_symbol(v, v_1, v_2); +} diff --git a/src_test/Shaders/WebGPU/simple_constant_rectangle.frag b/src_test/Shaders/WebGPU/simple_constant_rectangle.frag new file mode 100644 index 00000000..17bbaf8a --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_constant_rectangle.frag @@ -0,0 +1,38 @@ +diagnostic(off, derivative_uniformity); + +struct CB { + offset : vec4, +} + +@group(0) @binding(1) var v : CB; + +var v_1 : vec4; + +struct PS_INPUT { + Position : vec4, + UV : vec2, + Color : vec4, +} + +fn main_inner(v_2 : vec4, v_3 : vec2, v_4 : vec4) { + var input : PS_INPUT; + var param : PS_INPUT; + input.Position = v_2; + input.UV = v_3; + input.Color = v_4; + param = input; + v_1 = v_5(&(param)); +} + +fn v_5(input : ptr) -> vec4 { + var c : vec4; + c = ((*(input)).Color + v.offset); + c.w = 1.0f; + return c; +} + +@fragment +fn main(@builtin(position) v_6 : vec4, @location(0u) v_7 : vec2, @location(1u) v_8 : vec4) -> @location(0u) vec4 { + main_inner(v_6, v_7, v_8); + return v_1; +} diff --git a/src_test/Shaders/WebGPU/simple_constant_rectangle.vert b/src_test/Shaders/WebGPU/simple_constant_rectangle.vert new file mode 100644 index 00000000..98b572b0 --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_constant_rectangle.vert @@ -0,0 +1,63 @@ +diagnostic(off, derivative_uniformity); + +struct CB { + offset : vec4, +} + +@group(0) @binding(0) var v : CB; + +var v_1 : vec4; + +var v_2 : vec2; + +var v_3 : vec4; + +struct VS_INPUT { + Position : vec3, + UV : vec2, + Color : vec4, +} + +struct VS_OUTPUT { + Position : vec4, + UV : vec2, + Color : vec4, +} + +fn main_inner(v_4 : vec3, v_5 : vec2, v_6 : vec4) { + var input : VS_INPUT; + var flattenTemp : VS_OUTPUT; + var param : VS_INPUT; + input.Position = v_4; + input.UV = v_5; + input.Color = v_6; + param = input; + flattenTemp = v_7(&(param)); + v_1 = flattenTemp.Position; + v_2 = flattenTemp.UV; + v_3 = flattenTemp.Color; +} + +fn v_7(input : ptr) -> VS_OUTPUT { + var output : VS_OUTPUT; + let v_8 = (*(input)).Position; + output.Position = (vec4(v_8.x, v_8.y, v_8.z, 1.0f) + v.offset); + output.UV = (*(input)).UV; + output.Color = (*(input)).Color; + return output; +} + +struct tint_symbol { + @builtin(position) + m : vec4, + @location(0u) + m_1 : vec2, + @location(1u) + m_2 : vec4, +} + +@vertex +fn main(@location(0u) v_9 : vec3, @location(1u) v_10 : vec2, @location(2u) v_11 : vec4) -> tint_symbol { + main_inner(v_9, v_10, v_11); + return tint_symbol(v_1, v_2, v_3); +} diff --git a/src_test/Shaders/WebGPU/simple_mrt_texture_rectangle.frag b/src_test/Shaders/WebGPU/simple_mrt_texture_rectangle.frag new file mode 100644 index 00000000..6cf29608 --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_mrt_texture_rectangle.frag @@ -0,0 +1,59 @@ +diagnostic(off, derivative_uniformity); + +@group(1) @binding(0) var txt : texture_2d; + +@group(2) @binding(0) var smp : sampler; + +var v : vec4; + +var v_1 : vec4; + +struct PS_INPUT { + Position : vec4, + UV : vec2, + Color : vec4, +} + +struct PS_OUTPUT { + Color0 : vec4, + Color1 : vec4, +} + +fn main_inner(v_2 : vec4, v_3 : vec2, v_4 : vec4) { + var input : PS_INPUT; + var flattenTemp : PS_OUTPUT; + var param : PS_INPUT; + input.Position = v_2; + input.UV = v_3; + input.Color = v_4; + param = input; + flattenTemp = v_5(&(param)); + v = flattenTemp.Color0; + v_1 = flattenTemp.Color1; +} + +fn v_5(input : ptr) -> PS_OUTPUT { + var c : vec4; + var output : PS_OUTPUT; + c = textureSample(txt, smp, (*(input)).UV); + c.w = 255.0f; + output.Color0 = c; + c.x = (1.0f - c.x); + c.y = (1.0f - c.y); + c.z = (1.0f - c.z); + output.Color1 = c; + return output; +} + +struct tint_symbol { + @location(0u) + m : vec4, + @location(1u) + m_1 : vec4, +} + +@fragment +fn main(@builtin(position) v_6 : vec4, @location(0u) v_7 : vec2, @location(1u) v_8 : vec4) -> tint_symbol { + main_inner(v_6, v_7, v_8); + return tint_symbol(v, v_1); +} diff --git a/src_test/Shaders/WebGPU/simple_rectangle.frag b/src_test/Shaders/WebGPU/simple_rectangle.frag new file mode 100644 index 00000000..192cdc4b --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_rectangle.frag @@ -0,0 +1,27 @@ +diagnostic(off, derivative_uniformity); + +var v : vec4; + +struct PS_INPUT { + g_position : vec4, + g_color : vec4, +} + +fn main_inner(v_1 : vec4, v_2 : vec4) { + var input : PS_INPUT; + var param : PS_INPUT; + input.g_position = v_1; + input.g_color = v_2; + param = input; + v = v_3(&(param)); +} + +fn v_3(input : ptr) -> vec4 { + return (*(input)).g_color; +} + +@fragment +fn main(@builtin(position) v_4 : vec4, @location(0u) v_5 : vec4) -> @location(0u) vec4 { + main_inner(v_4, v_5); + return v; +} diff --git a/src_test/Shaders/WebGPU/simple_rectangle.vert b/src_test/Shaders/WebGPU/simple_rectangle.vert new file mode 100644 index 00000000..381b6d9a --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_rectangle.vert @@ -0,0 +1,50 @@ +diagnostic(off, derivative_uniformity); + +var v : vec4; + +var v_1 : vec4; + +struct VS_INPUT { + g_position : vec3, + g_uv : vec2, + g_color : vec4, +} + +struct VS_OUTPUT { + g_position : vec4, + g_color : vec4, +} + +fn main_inner(v_2 : vec3, v_3 : vec2, v_4 : vec4) { + var input : VS_INPUT; + var flattenTemp : VS_OUTPUT; + var param : VS_INPUT; + input.g_position = v_2; + input.g_uv = v_3; + input.g_color = v_4; + param = input; + flattenTemp = v_5(&(param)); + v = flattenTemp.g_position; + v_1 = flattenTemp.g_color; +} + +fn v_5(input : ptr) -> VS_OUTPUT { + var output : VS_OUTPUT; + let v_6 = (*(input)).g_position; + output.g_position = vec4(v_6.x, v_6.y, v_6.z, 1.0f); + output.g_color = (*(input)).g_color; + return output; +} + +struct tint_symbol { + @builtin(position) + m : vec4, + @location(0u) + m_1 : vec4, +} + +@vertex +fn main(@location(0u) v_7 : vec3, @location(1u) v_8 : vec2, @location(2u) v_9 : vec4) -> tint_symbol { + main_inner(v_7, v_8, v_9); + return tint_symbol(v, v_1); +} diff --git a/src_test/Shaders/WebGPU/simple_texture_rectangle.frag b/src_test/Shaders/WebGPU/simple_texture_rectangle.frag new file mode 100644 index 00000000..963fb6e5 --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_texture_rectangle.frag @@ -0,0 +1,36 @@ +diagnostic(off, derivative_uniformity); + +@group(1) @binding(0) var txt : texture_2d; + +@group(2) @binding(0) var smp : sampler; + +var v : vec4; + +struct PS_INPUT { + Position : vec4, + UV : vec2, + Color : vec4, +} + +fn main_inner(v_1 : vec4, v_2 : vec2, v_3 : vec4) { + var input : PS_INPUT; + var param : PS_INPUT; + input.Position = v_1; + input.UV = v_2; + input.Color = v_3; + param = input; + v = v_4(&(param)); +} + +fn v_4(input : ptr) -> vec4 { + var c : vec4; + c = textureSample(txt, smp, (*(input)).UV); + c.w = 255.0f; + return c; +} + +@fragment +fn main(@builtin(position) v_5 : vec4, @location(0u) v_6 : vec2, @location(1u) v_7 : vec4) -> @location(0u) vec4 { + main_inner(v_5, v_6, v_7); + return v; +} diff --git a/src_test/Shaders/WebGPU/simple_texture_rectangle.vert b/src_test/Shaders/WebGPU/simple_texture_rectangle.vert new file mode 100644 index 00000000..1880349c --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_texture_rectangle.vert @@ -0,0 +1,57 @@ +diagnostic(off, derivative_uniformity); + +var v : vec4; + +var v_1 : vec2; + +var v_2 : vec4; + +struct VS_INPUT { + Position : vec3, + UV : vec2, + Color : vec4, +} + +struct VS_OUTPUT { + Position : vec4, + UV : vec2, + Color : vec4, +} + +fn main_inner(v_3 : vec3, v_4 : vec2, v_5 : vec4) { + var input : VS_INPUT; + var flattenTemp : VS_OUTPUT; + var param : VS_INPUT; + input.Position = v_3; + input.UV = v_4; + input.Color = v_5; + param = input; + flattenTemp = v_6(&(param)); + v = flattenTemp.Position; + v_1 = flattenTemp.UV; + v_2 = flattenTemp.Color; +} + +fn v_6(input : ptr) -> VS_OUTPUT { + var output : VS_OUTPUT; + let v_7 = (*(input)).Position; + output.Position = vec4(v_7.x, v_7.y, v_7.z, 1.0f); + output.UV = (*(input)).UV; + output.Color = (*(input)).Color; + return output; +} + +struct tint_symbol { + @builtin(position) + m : vec4, + @location(0u) + m_1 : vec2, + @location(1u) + m_2 : vec4, +} + +@vertex +fn main(@location(0u) v_8 : vec3, @location(1u) v_9 : vec2, @location(2u) v_10 : vec4) -> tint_symbol { + main_inner(v_8, v_9, v_10); + return tint_symbol(v, v_1, v_2); +} diff --git a/src_test/Shaders/WebGPU/textures.frag b/src_test/Shaders/WebGPU/textures.frag new file mode 100644 index 00000000..e1a0e10f --- /dev/null +++ b/src_test/Shaders/WebGPU/textures.frag @@ -0,0 +1,47 @@ +diagnostic(off, derivative_uniformity); + +@group(1) @binding(0) var g_texture1 : texture_2d; + +@group(2) @binding(0) var g_sampler1 : sampler; + +@group(1) @binding(1) var g_texture2 : texture_2d_array; + +@group(2) @binding(1) var g_sampler2 : sampler; + +@group(1) @binding(2) var g_texture3 : texture_3d; + +@group(2) @binding(2) var g_sampler3 : sampler; + +var v : vec4; + +struct PS_Input { + Pos : vec4, + UV : vec2, + Color : vec4, +} + +fn main_inner(v_1 : vec4, v_2 : vec2, v_3 : vec4) { + var Input : PS_Input; + Input.Pos = v_1; + Input.UV = v_2; + Input.Color = v_3; + v = v_4(Input); +} + +fn v_4(Input : PS_Input) -> vec4 { + if ((Input.UV.x < 0.30000001192092895508f)) { + return textureSample(g_texture1, g_sampler1, Input.UV); + } else if ((Input.UV.x < 0.60000002384185791016f)) { + let v_5 = Input.UV; + let v_6 = vec3(v_5.x, v_5.y, 1.0f); + return textureSample(g_texture2, g_sampler2, v_6.xy, i32(v_6.z)); + } + let v_7 = Input.UV; + return textureSample(g_texture3, g_sampler3, vec3(v_7.x, v_7.y, 0.5f)); +} + +@fragment +fn main(@builtin(position) v_8 : vec4, @location(0u) v_9 : vec2, @location(1u) v_10 : vec4) -> @location(0u) vec4 { + main_inner(v_8, v_9, v_10); + return v; +} diff --git a/src_test/Shaders/WebGPU/vertex_structured.vert b/src_test/Shaders/WebGPU/vertex_structured.vert new file mode 100644 index 00000000..45fecc70 --- /dev/null +++ b/src_test/Shaders/WebGPU/vertex_structured.vert @@ -0,0 +1,67 @@ +diagnostic(off, derivative_uniformity); + +struct CS_INPUT { + value1 : f32, + value2 : f32, +} + +struct read_2 { + m : array, +} + +@group(1) @binding(0) var read_1 : read_2; + +var v : vec4; + +var v_1 : vec4; + +struct VS_INPUT { + g_position : vec3, + g_uv : vec2, + g_color : vec4, + InstanceId : u32, +} + +struct VS_OUTPUT { + g_position : vec4, + g_color : vec4, +} + +fn main_inner(v_2 : vec3, v_3 : vec2, v_4 : vec4, v_5 : u32) { + var input : VS_INPUT; + var flattenTemp : VS_OUTPUT; + var param : VS_INPUT; + input.g_position = v_2; + input.g_uv = v_3; + input.g_color = v_4; + input.InstanceId = v_5; + param = input; + flattenTemp = v_6(&(param)); + v = flattenTemp.g_position; + v_1 = flattenTemp.g_color; +} + +fn v_6(input : ptr) -> VS_OUTPUT { + var output : VS_OUTPUT; + let v_7 = (*(input)).g_position; + output.g_position = vec4(v_7.x, v_7.y, v_7.z, 1.0f); + let v_8 = read_1.m[(*(input)).InstanceId].value1; + output.g_position.x = (output.g_position.x + v_8); + let v_9 = read_1.m[(*(input)).InstanceId].value2; + output.g_position.y = (output.g_position.y + v_9); + output.g_color = (*(input)).g_color; + return output; +} + +struct tint_symbol { + @builtin(position) + m_1 : vec4, + @location(0u) + m_2 : vec4, +} + +@vertex +fn main(@location(0u) v_10 : vec3, @location(1u) v_11 : vec2, @location(2u) v_12 : vec4, @builtin(instance_index) v_13 : u32) -> tint_symbol { + main_inner(v_10, v_11, v_12, v_13); + return tint_symbol(v, v_1); +} diff --git a/src_test/Shaders/WebGPU/vtf.vert b/src_test/Shaders/WebGPU/vtf.vert new file mode 100644 index 00000000..3ba83595 --- /dev/null +++ b/src_test/Shaders/WebGPU/vtf.vert @@ -0,0 +1,60 @@ +diagnostic(off, derivative_uniformity); + +@group(1) @binding(0) var txt : texture_2d; + +@group(2) @binding(0) var smp : sampler; + +var v : vec4; + +var v_1 : vec4; + +struct VS_INPUT { + g_position : vec3, + g_uv : vec2, + g_color : vec4, +} + +struct VS_OUTPUT { + g_position : vec4, + g_color : vec4, +} + +fn main_inner(v_2 : vec3, v_3 : vec2, v_4 : vec4) { + var input : VS_INPUT; + var flattenTemp : VS_OUTPUT; + var param : VS_INPUT; + input.g_position = v_2; + input.g_uv = v_3; + input.g_color = v_4; + param = input; + flattenTemp = v_5(&(param)); + v = flattenTemp.g_position; + v_1 = flattenTemp.g_color; +} + +fn v_5(input : ptr) -> VS_OUTPUT { + var c : vec4; + var output : VS_OUTPUT; + c = textureSampleLevel(txt, smp, (*(input)).g_uv, 0.0f); + let v_6 = (*(input)).g_position; + output.g_position = vec4(v_6.x, v_6.y, v_6.z, 1.0f); + let v_7 = c.xy; + let v_8 = (output.g_position.xy + v_7); + output.g_position.x = v_8.x; + output.g_position.y = v_8.y; + output.g_color = (*(input)).g_color; + return output; +} + +struct tint_symbol { + @builtin(position) + m : vec4, + @location(0u) + m_1 : vec4, +} + +@vertex +fn main(@location(0u) v_9 : vec3, @location(1u) v_10 : vec2, @location(2u) v_11 : vec4) -> tint_symbol { + main_inner(v_9, v_10, v_11); + return tint_symbol(v, v_1); +} diff --git a/src_test/Shaders/WebGPU_Compiled/basic.comp b/src_test/Shaders/WebGPU_Compiled/basic.comp new file mode 100644 index 0000000000000000000000000000000000000000..3c25587fe499f19ed371774ad6195082b68beea1 GIT binary patch literal 799 zcmaKq!EW3j5Qcm9Q_QJ?#Ij=Vt=X=ulgo};5&#YzrRDPy0POicm?&zx?fZ_G3h^wl_7`p@vJj2N^Mwu z2zv1N(+~C_2cb>04Ytx6p<52E*Dd-+)oiw5Q|&n6Vbv)@EztTi@ZAz^*8R^Wyhx ryG%kcK4bW2k){t-EH$ARMfrS|vM5TpO3f+X5#S(RLo8CK2=~)}O?URt literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/instancing.vert b/src_test/Shaders/WebGPU_Compiled/instancing.vert new file mode 100644 index 0000000000000000000000000000000000000000..d8250d1c1a40f1c42797c9fda47670a25860fb87 GIT binary patch literal 1525 zcma)6O^?$s5apa-;S)-rL`~XVcIk(z-BT}sM3)OfQJOlDrPPibJ2asE@66av>wX|{ z>Bk#;-kax{`Bk-bA$D}f@`{UAu_6>@IU+kMS(hu;(M)qz3b|*hk7hy8Dya)ao}b8{ zfDky_Qq_`AB$qPp=N&Dk^Kz0dMkAjZo+XyR$In*j92)(%jMiBpL>t3Q0nHM?k14Oi=|$LS`~EFkfByQiF$$}!5iL_paC>-S<$?Ap)5r_?Qx7Xd zEu^1!1V`7rRXH!{dIu3bNljfRwl6Q6f6+NN4Bd^?U{A@(K4&~*oKqQg+2l+om@qvP z3BcW%JOJ*ks>l=!781rAtpFUHm;pcHWt}TU`G)Qr8@0-ZaFfe?e{s#Eiai$>Ksdo0 zLj+tzZ0cjSRD|zNu;(I1?lEF17d9CJzC=9rF?<`ELlJZzQ~^D)@DF*?Y&^Y+8!gejpx0))*`2eKhJDWI)E;QMT9*BMo zg90!95FbXEBkt|jZy26w>EF?VNrmGME5b`9jG{AK{_X&_ghmA%r!`|sqOqOKvL{QeHDn$}LI;1x7y>wZ#TV%EPED?<+J!?WJNB(-79 zG3e3b$1vKX9ECQ~wm3v&y!f@) rraMqfPgwp@toII8jIu9_Nt90)ElE+rO`6gh0=$ABAQtPS2>0DTHP!S+ literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/readwrite_texture.comp b/src_test/Shaders/WebGPU_Compiled/readwrite_texture.comp new file mode 100644 index 0000000000000000000000000000000000000000..619397e097cf97c47c5b67f090cfa013dbc835df GIT binary patch literal 776 zcma)4%TB{E5agV%*b}lLsv4IQ6x0vs4epj3Zz7AtjvU7=ApRXc(l$aII7D(hJ3Hgq zJ+@<48;MehR-4hOnwX|3L84V&IOUN~S~bS@%6-tMqS&?8OarMX><&sxrQ3M+!h%I1 zD|iHlCpTH-qcc{t$eG;Q_8>N_>5On?s^dqi9KQVBk6rt*MZeIe*{}sC)LlF*IeZlT z(4h_7`SiEWI_vm_#y>sRrXMDUusa(2o&w>c`b1JqCFGA5jfP$*O?HP#buJv*&KyL? zmG-6%$r{a-q#&J=!GOnlvz@~8D0~%=@{9bN zkz11PX_)1A5l#AVIhz=q-8Pf1qq*`byYLhyj;E~5lHIeV7#zM~KV9u+`lJOjB) zDwM8HACYt=1Z-ZJy%Cb6H7YZY!u@%jUeiqX=&?&BEH4r}4?Fo~NtVzO)~j_B-v}3e PG)Vm1xV-A8AZ~sDIVbw> literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/simple_compute_rectangle.frag b/src_test/Shaders/WebGPU_Compiled/simple_compute_rectangle.frag new file mode 100644 index 0000000000000000000000000000000000000000..32cf9f57c414a27aa00045af18f58d360c448168 GIT binary patch literal 965 zcmZ8g!ET%|5bc?-m{Vn1iePusB+X`pvZr3EmD==DRmd?0SppjwLljm0_s$qtpcf?1 zdw%cDczCV4T9`r>idWinP6=kpGJ--{HE^c}nfF?i#x}~mg%3f{Iok^d!ZdB~B>V{g z8u$X-TK;w{_q$UR{0)MvvZilY8bbD>bfI*`k`M-NkDW7?S26-Cd2yO`@)R{DO*A25 z>jTIW3NQ^nz9anrq=)%hIFTPP}iHDpBBl>_x$U(%kvK}d@-GJ z%IK{$!1K?svYWaEIGftoEe%IdYG}C9xzbu%Hsrev1Ksx<9V{HyjL?U`z@Riv8lgGe z1CE^PEw`LTzpi_3e2xBysypJ3b&O7x{ZbjDQ}IB>QSx!5dmn${hP`JV3ULF*)`t9s z&B0n{k7cg~O)I*_Ifd{Ehde*rwH>kUl00gBz9IjNPVB>|8HTYBwxG9&PV14Uaq}xt zzX=p{iSB+}jPQILq2TI6v5_3VB0Y_hl1^5--f9rOp{&H6Y@~MctFi2*SG7}`wbL5l z^5l2x=n-VK5%`N3&61wr`w^ZNV+E%kPLuU@Kb`*+HnZ9+&mDDiPoFgL%}ntJivR8( D+z25K literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/simple_compute_rectangle.vert b/src_test/Shaders/WebGPU_Compiled/simple_compute_rectangle.vert new file mode 100644 index 0000000000000000000000000000000000000000..e8a33fdd63501c706a89add7bc2f0a9f00482a52 GIT binary patch literal 1399 zcma)6%Z}496lI;S@CqdmswC~aXs4>giVciJ+YK5;Zt7H)5<7C7Fa^!ObFb|rZUKo+ zs@`*aeD2FXPHk1lBRg_>60+622xVEu z(}YWq*cbzBO?rTORxFJAAD}r2aP|*4yE^Us@4w}rzdY@>CV?l}a?Pc%)VWK8klo%! ztac2kev*|`zM{!?()_yH{$13%M~VLxEd{A57daP#sj$!I7w$-(FD}eM;Knx=narS& zgbU~tKtsw)FoE&1qFOVtWp!gwYkq(mswj5WKW3z6W4RvT(mC>N95TbmVv5*uVekfn zR`Su(G2mN3)@}GUG^7Yd4P1hrM;R|R7&QJ8-R*5WyNVkVOPoq5Dc&&@q|s_scA~)6 z$77!BhUO_Ny+!{rYSv~CKH29lJ7Vx4-5hk6bxZRxAYoer%;--F3 z!KbBXO0ySiChkW#odH=W#Dm=ex5Z?FK@#9JvADa`!!wKHOEfmY%))iT7i;-|#ZMFS VP0iH0!g;5{-oW_Dfbtr=`3)cksvrOW literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/simple_constant_rectangle.frag b/src_test/Shaders/WebGPU_Compiled/simple_constant_rectangle.frag new file mode 100644 index 0000000000000000000000000000000000000000..e1ee212dfdd9b66b8ded975ad7aafa109b5dbbad GIT binary patch literal 799 zcmZ9K&2ED*5QKZ?DfU#^Eky}Q)2R6oN>060k=pc%h+~kY#74#tRaM`;Ya0^i1&P`5 z?6+QDit5DFksn1?NL3lZh|2R2j$Df-Gos;XEk&;MS(tCS2!hJ!ni<$`;3oh;Z^aF~ zLc`f~nNQ+%82ko7QfO6|WJDo32zeB;AY%$mrk8El8X7#b-C&?qOV^B}nvVNnw|={i z^zD7O|8!k!un)k*204e@db=S;{{NXa#6(sZ5M_;2zl^vlt!NF5t_)a9($rG zx}a?wjdJ^^xv8}T=4g^!XT_OI(_K1A4z)NLAxYV85g$f6>-P;IIVpyZPf0SWZTZEQ k+r9S6V^^N{PrJ$f1q|jKtbP{Sc>9^VQCfI>hZg_t4|&n<)Bpeg literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/simple_constant_rectangle.vert b/src_test/Shaders/WebGPU_Compiled/simple_constant_rectangle.vert new file mode 100644 index 0000000000000000000000000000000000000000..dff47a7d48c109530ce7611e4fc644f014a557e0 GIT binary patch literal 1333 zcma)6O^=%}5bc>?;Zp@IMFFzg-R48l=G04fr6#!|LQG@{Y-9}GwA%mP8QTz(?V%SE z-aNmVdE?>NxvdIuq9>N0xo8zDd{LGmIZ?^_T(O>J9cQJGHB*;h;d!l+U7^VKiTv>h z0d7kb`9^wL%$H@FtV8e3^EPKGy2hUcWOHQv#P}Ki_qkkJu{G)8(Dn%g5p8rb3TPI) zV(-I=2`4(&`*3=5+E#n`nSKB9vft^NUqs6k6WmZ!TQwp3gR31vcUw^j=_=~xuA0yL z-M>W#EB;@!ggO>Uz5gTd1qT$i!HYY3)dmcf!n$z|E`0*4=t2CW0~WsPsnG|@28snN-+ zR~6j$K6`+a{wE_G1d}?=u@OSzxEnCnhv+p#f#_m>eupHEW@SJ=lRh%rq|LB0+GHn_ z>DdMczn!@4oe)V?C%J7xn_;i5Vq9hIr9O&E-(RHA^d zf#CkE(}ZD9%_2@Iv8C8u9hL#V3q6&JzT#l;w)+!Vu;tO@wL9r#1xX0UGZVK-op@s6 llaGg<>utM}C40mf#54Vjwe8k6=M>B=MnZy*6i9F0hrhIYl=c7s literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/simple_mrt_texture_rectangle.frag b/src_test/Shaders/WebGPU_Compiled/simple_mrt_texture_rectangle.frag new file mode 100644 index 0000000000000000000000000000000000000000..c82fd5cb1b9e98e7162497790c4546ee229ade5b GIT binary patch literal 1244 zcmZuw!H(K65bZf%;Zx;Q*@y(#t_q8gdg`S`TG(q8jDsvCcH}r&*lPd1Gj>88wiiU+ zd-KMAo_RYrB~vGU68TxG#t24LQAFg#wdisqI-a#s6iQdZT*FlmY|mP?HBCaYJqmde z@=RwT>2giX#gHGw@QZ0R&(hPnn5UbuBK=&^R5esIc~zHOgA!cUwe8C{qywtsY%$nO z$t<~}8>3rh$nGWk^?P^NlfMBWJJkpyl=P($;D=X-q}CgepQ==PD*Fh6(euw?KbTtS zY^v=ICTEHQa400Hav?JzCD*je(n%fZviamB04#h(Xs?0c3R$-p?{L2r92)VW%#Go4 z&ux^zx|0rco$I{1E3;PdEt_X(nxfv}kPRa95pmfdxIoZZK07%A{t-~Mq+h8Og)lF0 zO7Qa}vCGdg@QDB6l2xA^S`GKEjb0b6WcZxXErm@;9ya*py$`0s{19y`099*l8ICD2 zI6^Gab8D`X{AsGCIB zD7qBFguP0?rY8J%w4A-Vu(K8SeVv7*-{UN+W*b6`V5(Gjcc8J+UdGey&qX{JECI))t>3&^r)v2S`p(QtvX(yxC@Px^&@MrB}>+%R{s6}=T I>^1uI7vkYt1ONa4 literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/simple_texture_rectangle.frag b/src_test/Shaders/WebGPU_Compiled/simple_texture_rectangle.frag new file mode 100644 index 0000000000000000000000000000000000000000..2fbf32f8b2906702053288ea9a76027e0cc42a90 GIT binary patch literal 815 zcmZWn%Wi`(5bT++*i&UHiV~7EQu7c}PrX!;+T@CeF~}0w$QUP8)qn5WgapzH60@^A zGrK+&O-W_OGM*MfHkwl+^E`lzDQ;5D4NF?VbEzt>zx|cx#f6e>O{PA?M=mlh3NrD* zqzd#|!z*ZZ)~#Yml&$kww7qpi|2vwh#*QYf>XIpB;*@pmblDaRmJFlweoUiPOEv61 zllPC^{tJFQfSqi(=28r)0pj~ZheWQ~B5$&kYE1j}yj(z)a*=Q$m?9>bju>E)Xfy|b z*^uEo6E-^+b!*f2?p7#i@j6v$b%#4{c(uO4sZ-1EaI|ib=!SIGEjY7aH*(X_osMgm zlSkr8K3=JdW|BGSjn!JM^Hxy1t>9{N>%%i{y5I6O4jrbn8Wc4ym;H3-B6#fp@PGl3 zr%uK9!~FrpksY3pA1xN)B)9*H;gb+Rov}DqX;Cqu`|XqX*z!_yLF&sn@jWGrp@jhA uQc`@5Qjke&%P(E|a^OLE>dMPuw+neUU=(@u1{Q%GZ$C>H=3?Gh^ACUhNcJ26 literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/simple_texture_rectangle.vert b/src_test/Shaders/WebGPU_Compiled/simple_texture_rectangle.vert new file mode 100644 index 0000000000000000000000000000000000000000..e42e060bc16f0f2edd3b43077db034dd2160d181 GIT binary patch literal 1231 zcmb7D!EW0y4BgpZq0=A_UJyI!+Ae7jV5eQS0!?x?Tt_Yh*pea3TC<`5K2lN~DccUa zILLeCBl#%)cp2JS9@&vsFG3EQ*Hku5MvhEzQ)zBkISSrL)p31FS4m>3td>H4DkIS)JEis zY^4env*ayFE}Gx>yT6ON?*B)$5u~fQD7g?!QBy9W?uaRuQF8#?gpH+kGBA>G(T^Ia z^T;ch1M#M>j8=`Fy7%{7b43G;_O2320@iDtUIJ~&RZ4)cp6IhBh&W7|_t(#HQxcY3eXOohT z8~hjS677;ahvYsax;}d=Tf>xQueb|*-ZYX0EACufe}q9*kOcbP(eB!_fG#YZ k&%?IuCaTRV}a z(Rh&$L!qFdnq73S7=b4kk2J1!brQc!#P};>SDel*Q-(MmC&F9_&kLn^p+pO%it7Wt zJAD)}^oM$AcA=Nd^pZ!D-HBs+L!6Yb=k9Fx%U)o>(!k2~&3C6&Uut-Lqrb$t*YI}* z@G86ctpK6J(IK(r8RVyIrCMJ5SgmRSr-BR0g<#4XsM+ydE`WiWB_J^NLBM#u-|(%z zS%P!(bT{^lk`@b%yB|m-6NK&z5>JS8XTZdOgRQSU+W~XsoDOtE(86s2K77X|-4B6vNg%lZB8PxnIv1Lao7~+bXz(cO*;UGZ_NCxOzi97v!vn0JVqK` z10Z2kTkZe(j%U8{JkU*eK|W7n?6?X4_-+D}b)wr(W*%=PnRUnW-0jx6-5R%1nBvg( z1^2ZoniCWHmO5|$-t$&-;hkm2HJ1W^P&AtdEP#D0OZ=yi!i#!S&X<9l%EXpQG^g8i bOjm4&MOGH!&&-l?^F<=Nn{fk!vH$HKE)0q4 literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/vertex_structured.vert b/src_test/Shaders/WebGPU_Compiled/vertex_structured.vert new file mode 100644 index 0000000000000000000000000000000000000000..0643f089f19a083014179f4d68ab3bdab2400639 GIT binary patch literal 1587 zcma)6O^=%}5bZg?!lw#aiUP@YcbjY!si(cPm749Pst^H#EP;)TO_Nsp-#cUD;Cvi< zA<8_@yqPy2zw5p!L`h4Q*Ie|96@jR#h?G>aAy;gmndYn#vSsQRu4c1dNnI%N`Fr;K z<=e}S{GAapO=2GUzwTrZgX7F-bOQE|U3CVWP zc*%Gj%tIiM>s|?&*EAv+Wn;b>a@wGf89wSv)^tM#Kn7YY*O1cY3N(&BJXZC?ud2>E z(KE#ax3Lx`7S>m1dW@fXwn8*Q`n34;nfEH^1${07(J0R(2e5s8**Os}>1^tj?nY<` zQ*qMfjAx8pmlCFCp^(`-LeD^8Q^YHCp@+IIVIf2F?Tke7q|f>J(y6q^l)g* hFkaStb}o`Rj!lx7Z)&~^qx2}D=Nt?M2K06I<{x5f?tcIP literal 0 HcmV?d00001 diff --git a/src_test/Shaders/WebGPU_Compiled/vtf.vert b/src_test/Shaders/WebGPU_Compiled/vtf.vert new file mode 100644 index 0000000000000000000000000000000000000000..85dd5fb9a4f9083ad71452a4147692663bc52e78 GIT binary patch literal 1413 zcmZ`(O^=%}5bc>?;ZtQ=ih!j1(PX1sD^+c!&Gw22F_9&(k+EsgYX5s@Y#=6^UV<`j z=FQBT8UH?Zbtw<*z>A}ho#rK#RTYr~Q@k%U?^$jHucT_YK8LFy*dCQMEloqR-E(o^ z;z*N_^o1h&q{$be*-0D4^6ap#7TM+!k-d%Rnie9uqG@ZUz!O;3tOwm5iW;KyMUpT{15EzzzDWUBMb>^ZqsPx$C3i?5NRTo+_ zv13+MKwY^b+!m^6uEH!;?8)>5X)}x%5s(RO3*YAYL-P{Nj`+!ipusN$y*|_T)H;Q* zKu{|%dM;jii^62Dahq_3vj7p1Zv%Zkqf zZ1YUfMa?Z6YGvl!^_(UA`4?@^`(o~}Gi96ny&B|LNZ?5(lOb=}MC avLA73X<~mCreateTexture(texParamWrite)); if (!platform->NewFrame()) @@ -220,7 +224,16 @@ void test_compute_shader_texture(LLGI::DeviceType deviceType) std::vector result; if (texWrite->GetData(result)) { - if (!(result[0] == 128 && result[1] == 64 && result[2] == 64 && result[3] == 128)) + if (deviceType == LLGI::DeviceType::WebGPU) + { + const auto p = reinterpret_cast(result.data()); + if (!(p[0] == 0.5f && p[1] == 0.25f && p[2] == 0.25f && p[3] == 0.5f)) + { + std::cout << "Failed : Mismatch" << p[0] << "," << p[1] << "," << p[2] << "," << p[3] << std::endl; + abort(); + } + } + else if (!(result[0] == 128 && result[1] == 64 && result[2] == 64 && result[3] == 128)) { std::cout << "Failed : Mismatch" << static_cast(result[0]) << "," << static_cast(result[1]) << "," diff --git a/src_test/test_mipmap.cpp b/src_test/test_mipmap.cpp index f787ab0e..c7d855e9 100644 --- a/src_test/test_mipmap.cpp +++ b/src_test/test_mipmap.cpp @@ -73,7 +73,7 @@ void test_mipmap(LLGI::DeviceType deviceType) LLGI::CompilerResult result_ps; if (platform->GetDeviceType() == LLGI::DeviceType::Metal || platform->GetDeviceType() == LLGI::DeviceType::DirectX12 || - platform->GetDeviceType() == LLGI::DeviceType::Vulkan) + platform->GetDeviceType() == LLGI::DeviceType::Vulkan || platform->GetDeviceType() == LLGI::DeviceType::WebGPU) { auto code_vs = TestHelper::LoadData("simple_texture_rectangle.vert"); auto code_ps = TestHelper::LoadData("simple_texture_rectangle.frag"); diff --git a/src_test/test_simple_render.cpp b/src_test/test_simple_render.cpp index 0fda2e9f..44aa587b 100644 --- a/src_test/test_simple_render.cpp +++ b/src_test/test_simple_render.cpp @@ -378,7 +378,7 @@ void main() LLGI::CompilerResult result_ps; if (platform->GetDeviceType() == LLGI::DeviceType::Metal || platform->GetDeviceType() == LLGI::DeviceType::DirectX12 || - platform->GetDeviceType() == LLGI::DeviceType::Vulkan) + platform->GetDeviceType() == LLGI::DeviceType::Vulkan || platform->GetDeviceType() == LLGI::DeviceType::WebGPU) { auto code_vs = TestHelper::LoadData("simple_constant_rectangle.vert"); auto code_ps = TestHelper::LoadData("simple_constant_rectangle.frag"); @@ -681,7 +681,7 @@ void main() LLGI::CompilerResult result_ps; if (platform->GetDeviceType() == LLGI::DeviceType::Metal || platform->GetDeviceType() == LLGI::DeviceType::DirectX12 || - platform->GetDeviceType() == LLGI::DeviceType::Vulkan) + platform->GetDeviceType() == LLGI::DeviceType::Vulkan || platform->GetDeviceType() == LLGI::DeviceType::WebGPU) { auto code_vs = TestHelper::LoadData("simple_texture_rectangle.vert"); auto code_ps = TestHelper::LoadData("simple_texture_rectangle.frag"); diff --git a/src_test/test_textures.cpp b/src_test/test_textures.cpp index 843d42fe..428eefae 100644 --- a/src_test/test_textures.cpp +++ b/src_test/test_textures.cpp @@ -115,7 +115,7 @@ void test_textures(LLGI::DeviceType deviceType) LLGI::CompilerResult result_ps; if (platform->GetDeviceType() == LLGI::DeviceType::Metal || platform->GetDeviceType() == LLGI::DeviceType::DirectX12 || - platform->GetDeviceType() == LLGI::DeviceType::Vulkan) + platform->GetDeviceType() == LLGI::DeviceType::Vulkan || platform->GetDeviceType() == LLGI::DeviceType::WebGPU) { auto code_vs = TestHelper::LoadData("simple_texture_rectangle.vert"); auto code_ps = TestHelper::LoadData("textures.frag"); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 5ba7ebb1..edfc0525 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,3 +1,7 @@ +if(BUILD_WEBGPU) + add_definitions(-DENABLE_WEBGPU) +endif() + add_subdirectory(ShaderTranspilerCore) add_subdirectory(ShaderTranspiler) install(TARGETS ShaderTranspiler DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/tools/ShaderTranspiler/CMakeLists.txt b/tools/ShaderTranspiler/CMakeLists.txt index 8d5627d8..e6f0f91e 100644 --- a/tools/ShaderTranspiler/CMakeLists.txt +++ b/tools/ShaderTranspiler/CMakeLists.txt @@ -3,7 +3,11 @@ project(ShaderTranspiler) add_executable(ShaderTranspiler main.cpp) -target_compile_features(ShaderTranspiler PUBLIC cxx_std_17) +if(BUILD_WEBGPU) + target_compile_features(ShaderTranspiler PUBLIC cxx_std_20) +else() + target_compile_features(ShaderTranspiler PUBLIC cxx_std_17) +endif() target_include_directories(ShaderTranspiler PUBLIC ../ShaderTranspilerCore) diff --git a/tools/ShaderTranspiler/main.cpp b/tools/ShaderTranspiler/main.cpp index b2025c3c..492e7d1d 100644 --- a/tools/ShaderTranspiler/main.cpp +++ b/tools/ShaderTranspiler/main.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -11,13 +12,13 @@ enum class OutputType VULKAN_GLSL, MSL, HLSL, + WGSL, SPV, Max, }; int main(int argc, char* argv[]) { - std::vector args; for (int i = 1; i < argc; i++) @@ -30,6 +31,7 @@ int main(int argc, char* argv[]) std::string code; std::string inputPath; std::string outputPath; + std::string compiledOutputPath; bool isES = false; bool isDX12 = false; bool plain = false; @@ -74,6 +76,11 @@ int main(int argc, char* argv[]) outputType = OutputType::VULKAN_GLSL; i += 1; } + else if (args[i] == "-W") + { + outputType = OutputType::WGSL; + i += 1; + } else if (args[i] == "-S") { outputType = OutputType::SPV; @@ -139,6 +146,18 @@ int main(int argc, char* argv[]) i += 2; } + else if (args[i] == "--compiled-output") + { + if (i == args.size() - 1) + { + std::cout << "Invald compiled output : arg is none" << std::endl; + return 0; + } + + compiledOutputPath = args[i + 1]; + + i += 2; + } else { i++; @@ -180,8 +199,7 @@ int main(int argc, char* argv[]) auto generator = std::make_shared(loadFunc); - auto spirv = - generator->Generate(inputPath.c_str(), code.c_str(), includeDir, macros, shaderStage, outputType == OutputType::VULKAN_GLSL); + auto spirv = generator->Generate(inputPath.c_str(), code.c_str(), includeDir, macros, shaderStage, outputType == OutputType::VULKAN_GLSL, outputType == OutputType::WGSL); if (spirv->GetData().size() == 0) { @@ -207,6 +225,10 @@ int main(int argc, char* argv[]) { transpiler = std::make_shared(shaderModel != 0 ? shaderModel : 40, isDX12); } + else if (outputType == OutputType::WGSL) + { + transpiler = std::make_shared(); + } std::cout << inputPath << " -> " << outputPath << " ShaderModel=" << shaderModel << std::endl; @@ -237,7 +259,7 @@ int main(int argc, char* argv[]) } catch (const std::runtime_error& e) { - std::cout << e.what() << std::endl; + std::cout << "Error : " << e.what() << std::endl; return 0; } @@ -248,7 +270,31 @@ int main(int argc, char* argv[]) return 0; } + if (transpiler->GetCode() == "") + { + std::cout << "No code is generated." << std::endl; + return 1; + } + outputfile << transpiler->GetCode(); + if (outputType == OutputType::WGSL && compiledOutputPath != "") + { + static const char header[] = {'w', 'g', 's', 'l', 'c', 'o', 'd', 'e'}; + const auto transpiledCode = transpiler->GetCode(); + + std::ofstream compiledOutput(compiledOutputPath, std::ios::binary); + if (compiledOutput.bad()) + { + std::cout << "Invald compiled output" << std::endl; + return 0; + } + + compiledOutput.write(header, sizeof(header)); + compiledOutput.write(transpiledCode.data(), static_cast(transpiledCode.size())); + const char terminator = 0; + compiledOutput.write(&terminator, 1); + } + return 0; } diff --git a/tools/ShaderTranspilerCore/CMakeLists.txt b/tools/ShaderTranspilerCore/CMakeLists.txt index 04180b9d..a09a6189 100644 --- a/tools/ShaderTranspilerCore/CMakeLists.txt +++ b/tools/ShaderTranspilerCore/CMakeLists.txt @@ -4,7 +4,11 @@ project(ShaderTranspilerCore) add_library( ShaderTranspilerCore STATIC ShaderTranspilerCore.cpp ShaderTranspilerCore.h) -target_compile_features(ShaderTranspilerCore PUBLIC cxx_std_17) +if(BUILD_WEBGPU) + target_compile_features(ShaderTranspilerCore PUBLIC cxx_std_20) +else() + target_compile_features(ShaderTranspilerCore PUBLIC cxx_std_17) +endif() target_include_directories(ShaderTranspilerCore PUBLIC ${LLGI_THIRDPARTY_INCLUDES}) @@ -29,7 +33,7 @@ if(USE_THIRDPARTY_DIRECTORY) endif() if(MSVC) - target_compile_options(ShaderTranspilerCore PRIVATE /W4 /WX /wd4100) + target_compile_options(ShaderTranspilerCore PRIVATE /W4 /WX /wd4100 /wd4324) else() target_compile_options(ShaderTranspilerCore PRIVATE -Wall -Werror) endif() @@ -41,3 +45,8 @@ endif() if(SPIRVCROSS_WITHOUT_INSTALL) target_compile_definitions(ShaderTranspilerCore PRIVATE ENABLE_SPIRVCROSS_WITHOUT_INSTALL) endif() + +if(BUILD_WEBGPU) + target_compile_definitions(ShaderTranspilerCore PRIVATE ENABLE_WEBGPU) + target_link_libraries(ShaderTranspilerCore PRIVATE tint_api) +endif() diff --git a/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp b/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp index db83d2dc..308539e9 100644 --- a/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp +++ b/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp @@ -25,6 +25,13 @@ #include #endif +#if (ENABLE_WEBGPU) +#include +#endif + +#include +#include + namespace LLGI { @@ -43,6 +50,48 @@ std::string Replace(std::string target, std::string from_, std::string to_) return target; } +std::string NormalizeWGSLForLLGI(std::string code, ShaderStageType shaderStageType) +{ + for (uint32_t i = 0; i < TextureSlotMax; i++) + { + code = Replace(code, "@group(0u) @binding(" + std::to_string(300 + i) + "u)", "@group(0) @binding(" + std::to_string(i) + ")"); + code = Replace(code, "@group(0) @binding(" + std::to_string(300 + i) + ")", "@group(0) @binding(" + std::to_string(i) + ")"); + + const auto storageGroup = shaderStageType == ShaderStageType::Compute ? 2 : 1; + code = Replace(code, + "@group(0u) @binding(" + std::to_string(400 + i) + "u)", + "@group(" + std::to_string(storageGroup) + ") @binding(" + std::to_string(i) + ")"); + code = Replace(code, + "@group(0) @binding(" + std::to_string(400 + i) + ")", + "@group(" + std::to_string(storageGroup) + ") @binding(" + std::to_string(i) + ")"); + + code = Replace(code, "@group(0u) @binding(" + std::to_string(100 + i) + "u)", "@group(1) @binding(" + std::to_string(i) + ")"); + code = Replace(code, "@group(0) @binding(" + std::to_string(100 + i) + ")", "@group(1) @binding(" + std::to_string(i) + ")"); + + code = Replace(code, "@group(0u) @binding(" + std::to_string(200 + i) + "u)", "@group(1) @binding(" + std::to_string(i) + ")"); + code = Replace(code, "@group(0) @binding(" + std::to_string(200 + i) + ")", "@group(1) @binding(" + std::to_string(i) + ")"); + } + + std::stringstream input(code); + std::stringstream output; + std::string line; + while (std::getline(input, line)) + { + if (line.find(": sampler") != std::string::npos) + { + for (uint32_t i = 0; i < TextureSlotMax; i++) + { + line = Replace(line, "@group(0u) @binding(" + std::to_string(i) + "u)", "@group(2) @binding(" + std::to_string(i) + ")"); + line = Replace(line, "@group(0) @binding(" + std::to_string(i) + ")", "@group(2) @binding(" + std::to_string(i) + ")"); + } + } + output << line << "\n"; + } + code = output.str(); + + return code; +} + // https://stackoverflow.com/questions/8518743/get-directory-from-file-path-c/14631366 std::string dirnameOf(const std::string& fname) { @@ -391,6 +440,41 @@ bool SPIRVToGLSLTranspiler::Transpile(const std::shared_ptr& spirv, LLGI: return true; } +SPIRVToWGSLTranspiler::SPIRVToWGSLTranspiler() +{ +#if (ENABLE_WEBGPU) + tint::Initialize(); +#endif +} + +SPIRVToWGSLTranspiler::~SPIRVToWGSLTranspiler() +{ +#if (ENABLE_WEBGPU) + tint::Shutdown(); +#endif +} + +bool SPIRVToWGSLTranspiler::Transpile(const std::shared_ptr& spirv, LLGI::ShaderStageType shaderStageType) +{ +#if (ENABLE_WEBGPU) + tint::wgsl::writer::Options gen_options; + gen_options.allow_non_uniform_derivatives = true; + gen_options.allowed_features.features.insert(tint::wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures); + auto result = tint::SpirvToWgsl(spirv->GetData(), gen_options); + if (result != tint::Success) + { + errorCode_ = result.Failure().reason; + return false; + } + + code_ = NormalizeWGSLForLLGI(result.Get(), shaderStageType); + return true; +#else + errorCode_ = "WGSL output requires ShaderTranspilerCore to be built with BUILD_WEBGPU=ON."; + return false; +#endif +} + class ReflectionCompiler : public spirv_cross::Compiler { public: @@ -478,7 +562,8 @@ std::shared_ptr SPIRVGenerator::Generate(const char* path, std::vector includeDirs, std::vector macros, ShaderStageType shaderStageType, - bool isYInverted) + bool isYInverted, + bool addBindingOffset) { std::string codeStr(code); glslang::TProgram program; @@ -506,11 +591,19 @@ std::shared_ptr SPIRVGenerator::Generate(const char* path, } shader.setPreamble(macro.c_str()); - // shader->setAutoMapBindings(true); - // shader->setAutoMapLocations(true); + + if (addBindingOffset) + { + shader.setShiftBinding(glslang::TResourceType::EResSampler, 0); + shader.setShiftBinding(glslang::TResourceType::EResTexture, 100); + shader.setShiftBinding(glslang::TResourceType::EResImage, 200); + shader.setShiftBinding(glslang::TResourceType::EResUbo, 300); + shader.setShiftBinding(glslang::TResourceType::EResSsbo, 400); + shader.setShiftBinding(glslang::TResourceType::EResUav, 500); + } shader.setStrings(shaderStrings, 1); - const auto messages = static_cast(EShMsgSpvRules | EShMsgVulkanRules | EShMsgReadHlsl | EShOptFull); + const auto messages = static_cast(EShMsgSpvRules | EShMsgVulkanRules | EShMsgReadHlsl | EShOptFull | EShMsgHlslOffsets); DirStackFileIncluder includer(onLoad_); includer.pushExternalLocalDirectory(dirnameOf(path)); @@ -533,6 +626,14 @@ std::shared_ptr SPIRVGenerator::Generate(const char* path, return std::make_shared(program.getInfoLog()); } + if (addBindingOffset) + { + if (!program.mapIO()) + { + return std::make_shared(program.getInfoLog()); + } + } + std::vector spirv; glslang::SpvOptions spvOptions; spvOptions.optimizeSize = true; diff --git a/tools/ShaderTranspilerCore/ShaderTranspilerCore.h b/tools/ShaderTranspilerCore/ShaderTranspilerCore.h index 77863f35..1fd77ae0 100644 --- a/tools/ShaderTranspilerCore/ShaderTranspilerCore.h +++ b/tools/ShaderTranspilerCore/ShaderTranspilerCore.h @@ -95,6 +95,14 @@ class SPIRVToGLSLTranspiler : public SPIRVTranspiler bool Transpile(const std::shared_ptr& spirv, LLGI::ShaderStageType shaderStageType) override; }; +class SPIRVToWGSLTranspiler : public SPIRVTranspiler +{ +public: + SPIRVToWGSLTranspiler(); + ~SPIRVToWGSLTranspiler() override; + bool Transpile(const std::shared_ptr& spirv, LLGI::ShaderStageType shaderStageType) override; +}; + class SPIRVReflection : public SPIRVTranspiler { public: @@ -133,7 +141,8 @@ class SPIRVGenerator std::vector includeDirs, std::vector macros, ShaderStageType shaderStageType, - bool isYInverted); + bool isYInverted, + bool addBindingOffset); }; } // namespace LLGI From 58ee9faf2fa198583c76a8675eb8efd5c21a55a8 Mon Sep 17 00:00:00 2001 From: durswd Date: Sat, 2 May 2026 14:34:34 +0900 Subject: [PATCH 08/16] Update flow --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b83728ee..e012f81f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,5 @@ on: + workflow_dispatch: release: types: [published] push: @@ -35,7 +36,7 @@ jobs: name: Build on ${{ matrix.name }} runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true fetch-depth: 1 @@ -90,6 +91,7 @@ jobs: ./LLGI_Test.exe test - name: Upload + if: matrix.name == 'windows_x64' uses: actions/upload-artifact@v4 with: name: Test_Result_Windows From 18c67b79ce952c96410982c667a8966342ffbbbc Mon Sep 17 00:00:00 2001 From: durswd Date: Sat, 2 May 2026 14:46:01 +0900 Subject: [PATCH 09/16] Fix CI and glfw --- examples/thirdparty/glfw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/thirdparty/glfw b/examples/thirdparty/glfw index 1bd0a55a..b00e6a8a 160000 --- a/examples/thirdparty/glfw +++ b/examples/thirdparty/glfw @@ -1 +1 @@ -Subproject commit 1bd0a55aa76ceeda0537f7483bda1163405aa571 +Subproject commit b00e6a8a88ad1b60c0a045e696301deb92c9a13e From 838aa5c9ebe05c3ed2c601d1a1d9aa92c7e2ab73 Mon Sep 17 00:00:00 2001 From: durswd Date: Sat, 2 May 2026 14:58:45 +0900 Subject: [PATCH 10/16] Fix for linux --- examples/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2516de19..1d735f8a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1,8 @@ +if(UNIX AND NOT APPLE) + set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "Build support for Wayland" FORCE) + set(GLFW_BUILD_X11 ON CACHE BOOL "Build support for X11" FORCE) +endif() + add_subdirectory("thirdparty/glfw/") add_subdirectory("thirdparty/imgui/") add_subdirectory("ImGuiPlatform") From ee01e56b11f6a3ae12e3d144b8bace01617a86aa Mon Sep 17 00:00:00 2001 From: durswd Date: Sat, 2 May 2026 15:20:35 +0900 Subject: [PATCH 11/16] fix --- tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp b/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp index 308539e9..f482433e 100644 --- a/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp +++ b/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp @@ -50,6 +50,7 @@ std::string Replace(std::string target, std::string from_, std::string to_) return target; } +#if (ENABLE_WEBGPU) std::string NormalizeWGSLForLLGI(std::string code, ShaderStageType shaderStageType) { for (uint32_t i = 0; i < TextureSlotMax; i++) @@ -91,6 +92,7 @@ std::string NormalizeWGSLForLLGI(std::string code, ShaderStageType shaderStageTy return code; } +#endif // https://stackoverflow.com/questions/8518743/get-directory-from-file-path-c/14631366 std::string dirnameOf(const std::string& fname) From 8ec77802b1fd573b6635791c3c9c43bd9f30dd88 Mon Sep 17 00:00:00 2001 From: swd Date: Sat, 2 May 2026 19:08:43 +0900 Subject: [PATCH 12/16] Add browser WebGPU tests (#4) Add browser WebGPU tests --- .gitignore | 2 + CMakeLists.txt | 10 +- docs/WebGPU.md | 6 + docs/WebGPU_Browser_Test.md | 320 ++++++++++++ package-lock.json | 56 ++ package.json | 5 + src/CMakeLists.txt | 6 +- src/PC/LLGI.CreatePC.cpp | 26 + src/WebGPU/LLGI.BufferWebGPU.cpp | 20 + src/WebGPU/LLGI.CommandListWebGPU.cpp | 3 + src/WebGPU/LLGI.GraphicsWebGPU.cpp | 56 ++ src/WebGPU/LLGI.PlatformWebGPU.cpp | 50 +- src/WebGPU/LLGI.RenderPassWebGPU.cpp | 65 ++- src/WebGPU/LLGI.RenderPassWebGPU.h | 4 +- src/WebGPU/LLGI.TextureWebGPU.cpp | 20 + src_test/CMakeLists.txt | 43 +- src_test/browser/pre_webgpu_test.js | 61 +++ src_test/browser/run_webgpu_browser_test.mjs | 111 ++++ src_test/main.cpp | 25 +- src_test/test_webgpu_browser.cpp | 520 +++++++++++++++++++ 20 files changed, 1370 insertions(+), 39 deletions(-) create mode 100644 docs/WebGPU_Browser_Test.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src_test/browser/pre_webgpu_test.js create mode 100644 src_test/browser/run_webgpu_browser_test.mjs create mode 100644 src_test/test_webgpu_browser.cpp diff --git a/.gitignore b/.gitignore index 618aab6e..0e81970f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,9 @@ /msvc/*.user **/Debug /build +/build-webgpu-browser/ /build_clangformat /thirdparty/dawn/ +/node_modules/ .DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index b2f8279e..edb90f5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ project(LLGI) include(GNUInstallDirs) include(ExternalProject) +include(CTest) # linux flag if(UNIX AND NOT APPLE) @@ -12,6 +13,11 @@ if(UNIX AND NOT APPLE) endif() option(BUILD_WEBGPU "build webgpu backend" OFF) +option(BUILD_WEBGPU_BROWSER_TEST + "build a WebAssembly browser smoke test for the WebGPU backend" OFF) +if(BUILD_WEBGPU_BROWSER_TEST AND NOT BUILD_WEBGPU) + message(FATAL_ERROR "BUILD_WEBGPU_BROWSER_TEST requires BUILD_WEBGPU=ON.") +endif() set(WEBGPU_DAWN_SOURCE_DIR "" CACHE PATH "Path to a Dawn source checkout used when BUILD_WEBGPU is ON") @@ -254,7 +260,7 @@ if(BUILD_VULKAN) endif() endif() -if(BUILD_WEBGPU) +if(BUILD_WEBGPU AND NOT EMSCRIPTEN) set(DAWN_FETCH_DEPENDENCIES OFF CACHE BOOL @@ -293,7 +299,7 @@ endif() add_subdirectory("src") -if(BUILD_TEST) +if(BUILD_TEST OR BUILD_WEBGPU_BROWSER_TEST) add_subdirectory("src_test") endif() diff --git a/docs/WebGPU.md b/docs/WebGPU.md index bcf4baa5..33180cad 100644 --- a/docs/WebGPU.md +++ b/docs/WebGPU.md @@ -97,6 +97,12 @@ build-webgpu\src_test\Release\LLGI_Test.exe --webgpu --filter=SimpleRender.* Some environments need a visible GPU session for Dawn to create a WebGPU device. Headless CI may need Dawn-specific setup. +## Browser Smoke Test + +The native test path above uses Dawn directly. To compile the WebGPU backend to +WebAssembly and run it in a real browser WebGPU implementation, use the browser +test flow in `docs/WebGPU_Browser_Test.md`. + ## Shader Generation `ShaderTranspiler` can emit WGSL and compiled WGSL blobs for WebGPU tests. diff --git a/docs/WebGPU_Browser_Test.md b/docs/WebGPU_Browser_Test.md new file mode 100644 index 00000000..00417ef3 --- /dev/null +++ b/docs/WebGPU_Browser_Test.md @@ -0,0 +1,320 @@ +# WebGPU Browser Test + +This document describes how to build and run the browser-only WebGPU backend +tests. These tests compile LLGI to WebAssembly with Emscripten, run it in a real +Chromium-family browser, and use Emdawnwebgpu to access the browser WebGPU API. + +The native WebGPU path in `docs/WebGPU.md` uses Dawn directly. The browser path +covered here verifies a separate runtime: + +- Emscripten compilation and linking +- Emdawnwebgpu C/C++ bindings +- browser `navigator.gpu` adapter/device creation +- WGSL shader module creation +- render and compute pipeline creation +- browser canvas surface presentation +- offscreen render target readback +- storage buffer compute readback + +## Requirements + +- CMake 3.15 or newer +- Git +- Node.js +- Emscripten 4.x or newer with `--use-port=emdawnwebgpu` +- Playwright +- A WebGPU-capable Chromium, Chrome, or Edge browser + +WebGPU requires a secure context. The runner serves the generated files from +`localhost`; do not open `LLGI_Test.html` directly with `file://`. + +## Install Emscripten + +Install emsdk from the official repository: + +```bash +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +./emsdk install latest +./emsdk activate latest +source ./emsdk_env.sh +``` + +On Windows PowerShell: + +```powershell +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +.\emsdk install latest +.\emsdk activate latest +.\emsdk_env.ps1 +``` + +Check the installation: + +```bash +emcc --version +emcmake --version +``` + +Run the emsdk environment script again whenever you open a new shell. + +## Install Playwright + +From the LLGI repository root: + +```bash +npm install playwright +npx playwright install chromium +``` + +On Linux CI or minimal Linux installations, install browser system dependencies: + +```bash +npx playwright install-deps chromium +``` + +## Configure + +Use Emscripten's CMake wrapper: + +```bash +emcmake cmake -S . -B build-webgpu-browser \ + -DBUILD_WEBGPU=ON \ + -DBUILD_WEBGPU_BROWSER_TEST=ON \ + -DBUILD_TEST=ON +``` + +PowerShell equivalent: + +```powershell +emcmake cmake -S . -B build-webgpu-browser ` + -DBUILD_WEBGPU=ON ` + -DBUILD_WEBGPU_BROWSER_TEST=ON ` + -DBUILD_TEST=ON +``` + +`BUILD_WEBGPU_BROWSER_TEST=ON` requires an Emscripten toolchain and builds a +browser-focused `LLGI_Test.html`. It does not require a native Dawn checkout. + +## Build + +```bash +cmake --build build-webgpu-browser --target LLGI_Test +``` + +The generated files are: + +```text +build-webgpu-browser/src_test/LLGI_Test.html +build-webgpu-browser/src_test/LLGI_Test.js +build-webgpu-browser/src_test/LLGI_Test.wasm +build-webgpu-browser/src_test/LLGI_Test.data +``` + +On Windows, if emsdk is installed on a non-`C:` drive, keep `EM_CACHE` on the +same drive as emsdk. This avoids Emscripten port-build failures caused by +cross-drive relative paths: + +```powershell +$env:EM_CACHE = "D:\emscripten-cache-llgi" +cmake --build build-webgpu-browser --target LLGI_Test +``` + +For CI, cache `EM_CACHE` between runs to avoid rebuilding Emscripten system +libraries and the Emdawnwebgpu port every time. + +## Run Automated Tests + +Run the Playwright harness from the repository root: + +```bash +node src_test/browser/run_webgpu_browser_test.mjs \ + build-webgpu-browser/src_test/LLGI_Test.html +``` + +PowerShell: + +```powershell +node src_test/browser/run_webgpu_browser_test.mjs ` + build-webgpu-browser/src_test/LLGI_Test.html +``` + +The runner: + +- starts a temporary localhost HTTP server +- disables caching for generated `.html`, `.js`, `.wasm`, and `.data` files +- launches Chromium with WebGPU-friendly flags +- waits for the Emscripten module to report completion +- exits non-zero on failure + +The success log ends with: + +```text +LLGI_TEST_PASS completed +``` + +The default filter is: + +```text +WebGPUBrowser.* +``` + +Run one test with `--filter`: + +```bash +node src_test/browser/run_webgpu_browser_test.mjs \ + build-webgpu-browser/src_test/LLGI_Test.html \ + --filter=WebGPUBrowser.ScreenPresentation +``` + +## View in a Browser + +To see the canvas presentation test, serve the generated files: + +```bash +cd build-webgpu-browser/src_test +python -m http.server 8000 +``` + +Open: + +```text +http://localhost:8000/LLGI_Test.html?filter=WebGPUBrowser.ScreenPresentation +``` + +You should see a blue canvas with a colored polygon. Open browser DevTools and +check the Console for: + +```text +Start : WebGPUBrowser.ScreenPresentation +LLGI_TEST_PASS completed +``` + +## CTest + +When Node.js is found during CMake configure, CMake registers: + +```bash +ctest --test-dir build-webgpu-browser -R LLGI_WebGPU_Browser --output-on-failure +``` + +The CTest entry expects the `playwright` package to be available to Node.js. + +## Current Test Cases + +- `WebGPUBrowser.ComputeCompile` + - loads a WGSL compute shader + - compiles a compute pipeline +- `WebGPUBrowser.ComputeDispatch` + - uploads structured input data + - dispatches a storage-buffer compute shader + - copies output to a readback buffer + - maps and verifies computed values +- `WebGPUBrowser.OffscreenRender` + - creates an offscreen render texture + - loads WGSL vertex/fragment shaders + - compiles a render pipeline + - draws a rectangle +- `WebGPUBrowser.RenderReadback` + - clears an offscreen render target + - copies it to a readback buffer + - verifies pixel values +- `WebGPUBrowser.TextureAndConstantRender` + - uploads texture data with `Queue::WriteTexture` + - renders with texture and sampler bind groups + - renders again with vertex/pixel uniform buffers + - reads back pixels and verifies clear color and rendered output +- `WebGPUBrowser.ScreenPresentation` + - creates a browser canvas WebGPU surface + - renders through `PlatformWebGPU::GetCurrentScreen` + - leaves a visible blue canvas with a colored polygon + +Browser readback uses Asyncify so C++ test code can wait for `MapAsync` and +`Queue::OnSubmittedWorkDone` callbacks while the browser event loop continues to +run. + +## CI Notes + +A typical CI flow is: + +```bash +npm install playwright +npx playwright install chromium +emcmake cmake -S . -B build-webgpu-browser \ + -DBUILD_WEBGPU=ON \ + -DBUILD_WEBGPU_BROWSER_TEST=ON \ + -DBUILD_TEST=ON +cmake --build build-webgpu-browser --target LLGI_Test +node src_test/browser/run_webgpu_browser_test.mjs \ + build-webgpu-browser/src_test/LLGI_Test.html +``` + +The runner passes these Chromium flags: + +- `--enable-unsafe-webgpu` +- `--ignore-gpu-blocklist` +- `--enable-features=Vulkan,UseSkiaRenderer` +- `--use-vulkan=swiftshader` + +These help in headless or GPU-limited environments, but browser policy, +drivers, or CI sandboxing can still disable WebGPU. + +## Troubleshooting + +### `navigator.gpu is not available` + +Use `http://localhost` or HTTPS. WebGPU is unavailable from `file://`. + +Also check that the browser supports WebGPU and is not blocked by local GPU +policy. + +### Playwright fails to launch Chromium + +Install the browser: + +```bash +npx playwright install chromium +``` + +If Playwright's downloaded Chromium cannot launch, use an installed browser: + +```powershell +$env:CHROME_PATH = "C:\Program Files\Google\Chrome\Application\chrome.exe" +node src_test/browser/run_webgpu_browser_test.mjs ` + build-webgpu-browser/src_test/LLGI_Test.html +``` + +Common Windows alternatives: + +```powershell +$env:CHROME_PATH = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" +$env:CHROME_PATH = "C:\Program Files\Microsoft\Edge\Application\msedge.exe" +$env:CHROME_PATH = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" +``` + +### Emdawnwebgpu port build fails with a cross-drive path error + +Put `EM_CACHE` on the same drive as emsdk: + +```powershell +$env:EM_CACHE = "D:\emscripten-cache-llgi" +cmake --build build-webgpu-browser --target LLGI_Test +``` + +### Browser shows a dark or blank page + +Most tests render offscreen and validate results through readback. Only +`WebGPUBrowser.ScreenPresentation` intentionally draws to the visible canvas. + +Open: + +```text +http://localhost:8000/LLGI_Test.html?filter=WebGPUBrowser.ScreenPresentation +``` + +### Logs show `LLGI_TEST_FAIL` + +Check the preceding browser console lines. The test code prints `Abort on + : ` for assertion failures, and the runner also reports WebGPU +validation errors captured by the browser. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..f1e3a8fc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,56 @@ +{ + "name": "LLGI", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "playwright": "^1.59.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..8a8fbea3 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "playwright": "^1.59.1" + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 26b328d1..84703c13 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,7 @@ if(WIN32) elseif(APPLE) file(GLOB files_mac Mac/*.h Mac/*.cpp Mac/*.mm) list(APPEND files ${files_mac}) +elseif(EMSCRIPTEN) else() file(GLOB files_linux Linux/*.h Linux/*.cpp) list(APPEND files ${files_linux}) @@ -93,7 +94,10 @@ if(BUILD_VULKAN) endif() if(BUILD_WEBGPU) - if(TARGET dawn::webgpu_dawn) + if(EMSCRIPTEN) + target_compile_options(LLGI PUBLIC --use-port=emdawnwebgpu) + target_link_options(LLGI PUBLIC --use-port=emdawnwebgpu) + elseif(TARGET dawn::webgpu_dawn) target_link_libraries(LLGI PRIVATE dawn::webgpu_dawn) elseif(TARGET webgpu_dawn) target_link_libraries(LLGI PRIVATE webgpu_dawn) diff --git a/src/PC/LLGI.CreatePC.cpp b/src/PC/LLGI.CreatePC.cpp index e5d6efae..57e57e17 100644 --- a/src/PC/LLGI.CreatePC.cpp +++ b/src/PC/LLGI.CreatePC.cpp @@ -40,6 +40,29 @@ namespace LLGI { +#ifdef __EMSCRIPTEN__ +namespace +{ +class WindowEmscripten : public Window +{ + Vec2I windowSize_; + +public: + explicit WindowEmscripten(Vec2I windowSize) : windowSize_(windowSize) {} + + bool OnNewFrame() override { return true; } + + void* GetNativePtr(int32_t index) override + { + (void)index; + return nullptr; + } + + Vec2I GetWindowSize() const override { return windowSize_; } +}; +} // namespace +#endif + Window* CreateWindow(const char* title, Vec2I windowSize) { #ifdef _WIN32 @@ -54,6 +77,9 @@ Window* CreateWindow(const char* title, Vec2I windowSize) { return window; } +#elif defined(__EMSCRIPTEN__) + (void)title; + return new WindowEmscripten(windowSize); #elif __linux__ auto window = new WindowLinux(); if (window->Initialize(title, windowSize)) diff --git a/src/WebGPU/LLGI.BufferWebGPU.cpp b/src/WebGPU/LLGI.BufferWebGPU.cpp index 58272dbd..64b60bd5 100644 --- a/src/WebGPU/LLGI.BufferWebGPU.cpp +++ b/src/WebGPU/LLGI.BufferWebGPU.cpp @@ -3,6 +3,10 @@ #include #include +#if defined(__EMSCRIPTEN__) +#include +#endif + namespace LLGI { @@ -72,7 +76,11 @@ void* BufferWebGPU::Lock(int32_t offset, int32_t size) auto future = buffer_.MapAsync(wgpu::MapMode::Read, offset, size, +#if defined(__EMSCRIPTEN__) + wgpu::CallbackMode::AllowSpontaneous, +#else instance_ != nullptr ? wgpu::CallbackMode::WaitAnyOnly : wgpu::CallbackMode::AllowProcessEvents, +#endif [&completed, &succeeded](wgpu::MapAsyncStatus status, wgpu::StringView) { succeeded = status == wgpu::MapAsyncStatus::Success; completed = true; @@ -84,6 +92,17 @@ void* BufferWebGPU::Lock(int32_t offset, int32_t size) } else { +#if defined(__EMSCRIPTEN__) + const double waitStart = emscripten_get_now(); + while (!completed) + { + emscripten_sleep(1); + if (emscripten_get_now() - waitStart > 5000.0) + { + break; + } + } +#else const auto waitStart = std::chrono::steady_clock::now(); while (!completed) { @@ -94,6 +113,7 @@ void* BufferWebGPU::Lock(int32_t offset, int32_t size) } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } +#endif } return succeeded ? const_cast(buffer_.GetConstMappedRange(offset, size)) : nullptr; diff --git a/src/WebGPU/LLGI.CommandListWebGPU.cpp b/src/WebGPU/LLGI.CommandListWebGPU.cpp index 12d97d46..76930975 100644 --- a/src/WebGPU/LLGI.CommandListWebGPU.cpp +++ b/src/WebGPU/LLGI.CommandListWebGPU.cpp @@ -55,6 +55,7 @@ void CommandListWebGPU::End() void CommandListWebGPU::BeginRenderPass(RenderPass* renderPass) { auto rp = static_cast(renderPass); + rp->RefreshDescriptor(); const auto& desc = rp->GetDescriptor(); renderPassEncorder_ = commandEncorder_.BeginRenderPass(&desc); @@ -403,10 +404,12 @@ void CommandListWebGPU::CopyBuffer(Buffer* src, Buffer* dst) void CommandListWebGPU::WaitUntilCompleted() { +#if !defined(__EMSCRIPTEN__) if (device_ != nullptr) { device_.Tick(); } +#endif } } // namespace LLGI diff --git a/src/WebGPU/LLGI.GraphicsWebGPU.cpp b/src/WebGPU/LLGI.GraphicsWebGPU.cpp index ed4b814a..5efbf08d 100644 --- a/src/WebGPU/LLGI.GraphicsWebGPU.cpp +++ b/src/WebGPU/LLGI.GraphicsWebGPU.cpp @@ -13,6 +13,10 @@ #include #include +#if defined(__EMSCRIPTEN__) +#include +#endif + namespace LLGI { @@ -35,6 +39,34 @@ uint32_t GetFormatBytesPerPixel(TextureFormatType format) return 4; } } + +#if defined(__EMSCRIPTEN__) +void WaitForQueue(wgpu::Queue& queue) +{ + bool completed = false; + bool succeeded = false; + queue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous, + [&completed, &succeeded](wgpu::QueueWorkDoneStatus status, wgpu::StringView) { + succeeded = status == wgpu::QueueWorkDoneStatus::Success; + completed = true; + }); + + const double waitStart = emscripten_get_now(); + while (!completed) + { + emscripten_sleep(1); + if (emscripten_get_now() - waitStart > 5000.0) + { + break; + } + } + + if (!succeeded) + { + Log(LogType::Warning, "Timed out or failed while waiting for WebGPU queue completion."); + } +} +#endif } // namespace class SingleFrameMemoryPoolWebGPU : public SingleFrameMemoryPool @@ -86,10 +118,17 @@ void GraphicsWebGPU::Execute(CommandList* commandList) void GraphicsWebGPU::WaitFinish() { +#if defined(__EMSCRIPTEN__) + if (queue_ != nullptr) + { + WaitForQueue(queue_); + } +#else if (device_ != nullptr) { device_.Tick(); } +#endif } Buffer* GraphicsWebGPU::CreateBuffer(BufferUsageType usage, int32_t size) @@ -273,13 +312,18 @@ std::vector GraphicsWebGPU::CaptureRenderTarget(Texture* renderTarget) auto commandBuffer = encoder.Finish(); queue_.Submit(1, &commandBuffer); + WaitFinish(); bool completed = false; bool succeeded = false; auto future = readbackBuffer.MapAsync(wgpu::MapMode::Read, 0, bufferSize, +#if defined(__EMSCRIPTEN__) + wgpu::CallbackMode::AllowSpontaneous, +#else instance_ != nullptr ? wgpu::CallbackMode::WaitAnyOnly : wgpu::CallbackMode::AllowProcessEvents, +#endif [&completed, &succeeded](wgpu::MapAsyncStatus status, wgpu::StringView) { succeeded = status == wgpu::MapAsyncStatus::Success; completed = true; @@ -291,6 +335,17 @@ std::vector GraphicsWebGPU::CaptureRenderTarget(Texture* renderTarget) } else { +#if defined(__EMSCRIPTEN__) + const double waitStart = emscripten_get_now(); + while (!completed) + { + emscripten_sleep(1); + if (emscripten_get_now() - waitStart > 5000.0) + { + break; + } + } +#else const auto waitStart = std::chrono::steady_clock::now(); while (!completed) { @@ -301,6 +356,7 @@ std::vector GraphicsWebGPU::CaptureRenderTarget(Texture* renderTarget) } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } +#endif } std::vector ret(static_cast(unalignedBytesPerRow) * static_cast(size.Y)); diff --git a/src/WebGPU/LLGI.PlatformWebGPU.cpp b/src/WebGPU/LLGI.PlatformWebGPU.cpp index 1aad7ac2..486b2e0f 100644 --- a/src/WebGPU/LLGI.PlatformWebGPU.cpp +++ b/src/WebGPU/LLGI.PlatformWebGPU.cpp @@ -57,11 +57,13 @@ PlatformWebGPU::~PlatformWebGPU() { ResetCurrentScreen(); } void PlatformWebGPU::ResetCurrentScreen() { +#if !defined(__EMSCRIPTEN__) if (surface_ != nullptr && surfaceTexture_.texture != nullptr && isPresentRequested_ && !hasPresentedCurrentSurface_) { surface_.Present(); hasPresentedCurrentSurface_ = true; } +#endif SafeRelease(currentScreenRenderPass_); SafeRelease(currentScreenTexture_); @@ -72,10 +74,16 @@ void PlatformWebGPU::ResetCurrentScreen() bool PlatformWebGPU::ConfigureSurface(const Vec2I& windowSize) { - if (surface_ == nullptr || adapter_ == nullptr || device_ == nullptr || windowSize.X <= 0 || windowSize.Y <= 0) + if (surface_ == nullptr || device_ == nullptr || windowSize.X <= 0 || windowSize.Y <= 0) + { + return false; + } +#if !defined(__EMSCRIPTEN__) + if (adapter_ == nullptr) { return false; } +#endif wgpu::SurfaceCapabilities capabilities{}; surface_.GetCapabilities(adapter_, &capabilities); @@ -105,12 +113,50 @@ bool PlatformWebGPU::Initialize(Window* window, bool waitVSync) { waitVSync_ = waitVSync; window_ = window; +#if !defined(__EMSCRIPTEN__) if (window_ == nullptr) { return false; } +#endif -#if defined(_WIN32) && !defined(__EMSCRIPTEN__) +#if defined(__EMSCRIPTEN__) + wgpu::InstanceDescriptor instanceDescriptor{}; + instance_ = wgpu::CreateInstance(&instanceDescriptor); + if (instance_ == nullptr) + { + Log(LogType::Error, "Failed to create browser WebGPU instance."); + return false; + } + + auto device = wgpu::Device::Acquire(emscripten_webgpu_get_device()); + if (device == nullptr) + { + Log(LogType::Error, "Failed to get preinitialized browser WebGPU device."); + return false; + } + + device_ = device; + if (window_ != nullptr) + { + windowSize_ = window_->GetWindowSize(); + + wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector canvasSource{}; + canvasSource.selector = "#canvas"; + + wgpu::SurfaceDescriptor surfaceDescriptor{}; + surfaceDescriptor.nextInChain = &canvasSource; + surface_ = instance_.CreateSurface(&surfaceDescriptor); + if (surface_ == nullptr) + { + Log(LogType::Error, "Failed to create browser WebGPU canvas surface."); + return false; + } + + return ConfigureSurface(windowSize_); + } + return true; +#elif defined(_WIN32) wgpu::InstanceDescriptor instanceDescriptor{}; static constexpr auto timedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny; instanceDescriptor.requiredFeatureCount = 1; diff --git a/src/WebGPU/LLGI.RenderPassWebGPU.cpp b/src/WebGPU/LLGI.RenderPassWebGPU.cpp index 975e4d1a..08b73e7a 100644 --- a/src/WebGPU/LLGI.RenderPassWebGPU.cpp +++ b/src/WebGPU/LLGI.RenderPassWebGPU.cpp @@ -4,6 +4,42 @@ namespace LLGI { +void RenderPassWebGPU::RefreshDescriptor() +{ + for (int i = 0; i < descriptor_.colorAttachmentCount; i++) + { + if (GetIsColorCleared()) + { + colorAttachments_[i].loadOp = wgpu::LoadOp::Clear; + colorAttachments_[i].storeOp = wgpu::StoreOp::Store; + colorAttachments_[i].clearValue = { + GetClearColor().R / 255.0, GetClearColor().G / 255.0, GetClearColor().B / 255.0, GetClearColor().A / 255.0}; + } + else + { + colorAttachments_[i].loadOp = wgpu::LoadOp::Load; + colorAttachments_[i].storeOp = wgpu::StoreOp::Store; + colorAttachments_[i].clearValue = {0, 0, 0, 1}; + } + } + + if (descriptor_.depthStencilAttachment != nullptr) + { + if (GetIsDepthCleared()) + { + depthStencilAttachiment_.depthLoadOp = wgpu::LoadOp::Clear; + depthStencilAttachiment_.depthStoreOp = wgpu::StoreOp::Store; + depthStencilAttachiment_.depthClearValue = 1.0f; + } + else + { + depthStencilAttachiment_.depthLoadOp = wgpu::LoadOp::Load; + depthStencilAttachiment_.depthStoreOp = wgpu::StoreOp::Store; + depthStencilAttachiment_.depthClearValue = 1.0f; + } + } +} + bool RenderPassWebGPU::Initialize( Texture** textures, int textureCount, Texture* depthTexture, Texture* resolvedRenderTexture, Texture* resolvedDepthTexture) { @@ -69,20 +105,6 @@ bool RenderPassWebGPU::Initialize( { colorAttachments_[i].view = texturesImpl[i]->GetTextureView(); - if (GetIsColorCleared()) - { - colorAttachments_[i].loadOp = wgpu::LoadOp::Clear; - colorAttachments_[i].storeOp = wgpu::StoreOp::Store; - colorAttachments_[i].clearValue = { - GetClearColor().R / 255.0, GetClearColor().G / 255.0, GetClearColor().B / 255.0, GetClearColor().A / 255.0}; - } - else - { - colorAttachments_[i].loadOp = wgpu::LoadOp::Load; - colorAttachments_[i].storeOp = wgpu::StoreOp::Store; - colorAttachments_[i].clearValue = {0, 0, 0, 1}; - } - if (resolvedTextureImpl != nullptr) { colorAttachments_[i].resolveTarget = resolvedTextureImpl->GetTextureView(); @@ -94,19 +116,6 @@ bool RenderPassWebGPU::Initialize( { depthStencilAttachiment_.view = depthTextureImpl->GetTextureView(); - if (GetIsDepthCleared()) - { - depthStencilAttachiment_.depthLoadOp = wgpu::LoadOp::Clear; - depthStencilAttachiment_.depthStoreOp = wgpu::StoreOp::Store; - depthStencilAttachiment_.depthClearValue = 1.0f; - } - else - { - depthStencilAttachiment_.depthLoadOp = wgpu::LoadOp::Load; - depthStencilAttachiment_.depthStoreOp = wgpu::StoreOp::Store; - depthStencilAttachiment_.depthClearValue = 1.0f; - } - if (depthTextureImpl->GetFormat() == TextureFormatType::D24S8 || depthTextureImpl->GetFormat() == TextureFormatType::D32S8) { depthStencilAttachiment_.stencilLoadOp = GetIsDepthCleared() ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load; @@ -122,6 +131,8 @@ bool RenderPassWebGPU::Initialize( descriptor_.depthStencilAttachment = &depthStencilAttachiment_; } + RefreshDescriptor(); + return true; } diff --git a/src/WebGPU/LLGI.RenderPassWebGPU.h b/src/WebGPU/LLGI.RenderPassWebGPU.h index b1b4dcc2..23ee7d46 100644 --- a/src/WebGPU/LLGI.RenderPassWebGPU.h +++ b/src/WebGPU/LLGI.RenderPassWebGPU.h @@ -17,7 +17,9 @@ class RenderPassWebGPU : public RenderPass bool Initialize(Texture** textures, int textureCount, Texture* depthTexture, Texture* resolvedRenderTexture, Texture* resolvedDepthTexture); + void RefreshDescriptor(); + const wgpu::RenderPassDescriptor& GetDescriptor() const { return descriptor_; } }; -} // namespace LLGI \ No newline at end of file +} // namespace LLGI diff --git a/src/WebGPU/LLGI.TextureWebGPU.cpp b/src/WebGPU/LLGI.TextureWebGPU.cpp index 06fc90b1..eb66a3a0 100644 --- a/src/WebGPU/LLGI.TextureWebGPU.cpp +++ b/src/WebGPU/LLGI.TextureWebGPU.cpp @@ -4,6 +4,10 @@ #include #include +#if defined(__EMSCRIPTEN__) +#include +#endif + namespace LLGI { @@ -208,7 +212,11 @@ bool TextureWebGPU::GetData(std::vector& data) auto future = readbackBuffer.MapAsync(wgpu::MapMode::Read, 0, bufferSize, +#if defined(__EMSCRIPTEN__) + wgpu::CallbackMode::AllowSpontaneous, +#else instance_ != nullptr ? wgpu::CallbackMode::WaitAnyOnly : wgpu::CallbackMode::AllowProcessEvents, +#endif [&completed, &succeeded](wgpu::MapAsyncStatus status, wgpu::StringView) { succeeded = status == wgpu::MapAsyncStatus::Success; completed = true; @@ -220,6 +228,17 @@ bool TextureWebGPU::GetData(std::vector& data) } else { +#if defined(__EMSCRIPTEN__) + const double waitStart = emscripten_get_now(); + while (!completed) + { + emscripten_sleep(1); + if (emscripten_get_now() - waitStart > 5000.0) + { + break; + } + } +#else const auto waitStart = std::chrono::steady_clock::now(); while (!completed) { @@ -230,6 +249,7 @@ bool TextureWebGPU::GetData(std::vector& data) } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } +#endif } if (!succeeded) diff --git a/src_test/CMakeLists.txt b/src_test/CMakeLists.txt index a256422b..5d47f5ce 100644 --- a/src_test/CMakeLists.txt +++ b/src_test/CMakeLists.txt @@ -1,7 +1,16 @@ -file(GLOB files *.h *.cpp) +if(BUILD_WEBGPU_BROWSER_TEST) + set(files main.cpp TestHelper.cpp TestHelper.h test.h test_webgpu_browser.cpp) +else() + file(GLOB files *.h *.cpp) + list(FILTER files EXCLUDE REGEX "test_webgpu_browser\\.cpp$") +endif() add_executable(LLGI_Test ${files}) +if(BUILD_WEBGPU_BROWSER_TEST AND NOT EMSCRIPTEN) + message(FATAL_ERROR "BUILD_WEBGPU_BROWSER_TEST requires an Emscripten CMake toolchain.") +endif() + if(APPLE) find_library(COCOA_LIBRARY Cocoa) @@ -29,7 +38,17 @@ if(BUILD_VULKAN_COMPILER AND USE_THIRDPARTY_DIRECTORY) add_dependencies(LLGI_Test EP_glslang EP_SPIRV-Cross) endif() -if(MSVC) +if(BUILD_WEBGPU_BROWSER_TEST) + set_target_properties(LLGI_Test PROPERTIES SUFFIX ".html") + target_link_options( + LLGI_Test + PRIVATE + "--pre-js=${CMAKE_CURRENT_SOURCE_DIR}/browser/pre_webgpu_test.js" + "--preload-file=${CMAKE_CURRENT_SOURCE_DIR}/Shaders/WebGPU@/Shaders/WebGPU" + "-sALLOW_MEMORY_GROWTH=1" + "-sASYNCIFY=1" + "-sASSERTIONS=1") +elseif(MSVC) target_link_libraries(LLGI_Test PRIVATE) elseif(APPLE) target_link_libraries(LLGI_Test PRIVATE) @@ -39,13 +58,27 @@ else() X11-xcb) endif() -file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/Shaders - DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) +if(NOT EMSCRIPTEN) + file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/Shaders + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) +endif() clang_format(LLGI_Test) -if(MSVC) +if(EMSCRIPTEN) +elseif(MSVC) target_compile_options(LLGI_Test PRIVATE /W4 /WX /wd4100) else() target_compile_options(LLGI_Test PRIVATE -Wall -Werror) endif() + +if(BUILD_WEBGPU_BROWSER_TEST AND EMSCRIPTEN) + find_program(NODE_EXE node) + if(NODE_EXE) + add_test( + NAME LLGI_WebGPU_Browser + COMMAND + ${NODE_EXE} ${CMAKE_CURRENT_SOURCE_DIR}/browser/run_webgpu_browser_test.mjs + $ --filter=WebGPUBrowser.*) + endif() +endif() diff --git a/src_test/browser/pre_webgpu_test.js b/src_test/browser/pre_webgpu_test.js new file mode 100644 index 00000000..34a3f06d --- /dev/null +++ b/src_test/browser/pre_webgpu_test.js @@ -0,0 +1,61 @@ +if (typeof Module === 'undefined') { + var Module = {}; +} + +Module.arguments = Module.arguments || (function() { + var args = ['--webgpu', '--filter=WebGPUBrowser.*']; + if (typeof URLSearchParams !== 'undefined' && typeof location !== 'undefined') { + var params = new URLSearchParams(location.search); + var filter = params.get('filter'); + if (filter) { + args[1] = '--filter=' + filter; + } + } + return args; +})(); + +Module.preRun = Module.preRun || []; +Module.preRun.push(function() { + var dependency = 'llgi-webgpu-device'; + addRunDependency(dependency); + + (async function() { + if (!navigator.gpu) { + throw new Error('navigator.gpu is not available. Serve over localhost/https and use a WebGPU-capable browser.'); + } + + var adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + throw new Error('Failed to request a WebGPU adapter.'); + } + + var optionalFeatures = ['float32-filterable', 'texture-formats-tier2']; + var requiredFeatures = optionalFeatures.filter(function(feature) { + return adapter.features && adapter.features.has(feature); + }); + + Module.preinitializedWebGPUDevice = await adapter.requestDevice({ + requiredFeatures: requiredFeatures + }); + Module.preinitializedWebGPUDevice.addEventListener('uncapturederror', function(event) { + Module.llgiLastWebGPUError = event.error && event.error.message ? event.error.message : String(event.error); + console.error('LLGI_WEBGPU_ERROR', Module.llgiLastWebGPUError); + }); + removeRunDependency(dependency); + })().catch(function(error) { + Module.llgiTestResult = { + status: 'failed', + message: error && error.message ? error.message : String(error) + }; + console.error('LLGI_TEST_FAIL', Module.llgiTestResult.message); + removeRunDependency(dependency); + }); +}); + +Module.onAbort = function(reason) { + Module.llgiTestResult = { + status: 'failed', + message: reason ? String(reason) : 'aborted' + }; + console.error('LLGI_TEST_FAIL', Module.llgiTestResult.message); +}; diff --git a/src_test/browser/run_webgpu_browser_test.mjs b/src_test/browser/run_webgpu_browser_test.mjs new file mode 100644 index 00000000..4963678f --- /dev/null +++ b/src_test/browser/run_webgpu_browser_test.mjs @@ -0,0 +1,111 @@ +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; + +let chromium; +try { + ({chromium} = await import('playwright')); +} catch (error) { + console.error('The "playwright" package is required. Install it with: npm install playwright && npx playwright install chromium'); + process.exit(2); +} + +const args = process.argv.slice(2); +const htmlPath = args[0]; +const filterArg = args.find((arg) => arg.startsWith('--filter=')); +const filter = filterArg ? filterArg.substring('--filter='.length) : 'WebGPUBrowser.*'; + +if (!htmlPath) { + console.error('Usage: node run_webgpu_browser_test.mjs [--filter=WebGPUBrowser.*]'); + process.exit(2); +} + +const resolvedHtmlPath = path.resolve(htmlPath); +if (!fs.existsSync(resolvedHtmlPath)) { + console.error(`Not found: ${resolvedHtmlPath}`); + process.exit(2); +} + +const root = path.dirname(resolvedHtmlPath); +const htmlFile = path.basename(resolvedHtmlPath); +const executablePath = process.env.CHROME_PATH || process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; + +function contentType(filePath) { + if (filePath.endsWith('.html')) return 'text/html'; + if (filePath.endsWith('.js')) return 'application/javascript'; + if (filePath.endsWith('.wasm')) return 'application/wasm'; + if (filePath.endsWith('.data')) return 'application/octet-stream'; + return 'application/octet-stream'; +} + +const server = http.createServer((request, response) => { + const requestUrl = new URL(request.url, 'http://127.0.0.1'); + if (requestUrl.pathname === '/favicon.ico') { + response.writeHead(204); + response.end(); + return; + } + + const relativePath = decodeURIComponent(requestUrl.pathname === '/' ? `/${htmlFile}` : requestUrl.pathname); + const filePath = path.resolve(root, `.${relativePath}`); + + if (!filePath.startsWith(root) || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + response.writeHead(404); + response.end('Not found'); + return; + } + + response.writeHead(200, { + 'Content-Type': contentType(filePath), + 'Cache-Control': 'no-store, max-age=0', + 'Pragma': 'no-cache', + 'Expires': '0', + }); + fs.createReadStream(filePath).pipe(response); +}); + +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); +const {port} = server.address(); +const url = `http://127.0.0.1:${port}/${htmlFile}?filter=${encodeURIComponent(filter)}`; + +let browser; +try { + browser = await chromium.launch({ + headless: true, + executablePath, + args: [ + '--enable-unsafe-webgpu', + '--ignore-gpu-blocklist', + '--enable-features=Vulkan,UseSkiaRenderer', + '--use-vulkan=swiftshader', + ], + }); + + const page = await browser.newPage(); + page.on('console', (message) => console.log(`[browser:${message.type()}] ${message.text()}`)); + page.on('pageerror', (error) => console.error(`[browser:pageerror] ${error.message}`)); + + await page.goto(url, {waitUntil: 'load'}); + const result = await page.waitForFunction( + () => globalThis.Module && globalThis.Module.llgiTestResult, + null, + {timeout: 60000} + ); + const value = await result.jsonValue(); + if (!value || value.status !== 'passed') { + console.error(value && value.message ? value.message : 'LLGI browser WebGPU test failed.'); + process.exitCode = 1; + } else { + await page.waitForTimeout(500); + const lateError = await page.evaluate(() => globalThis.Module && globalThis.Module.llgiLastWebGPUError); + if (lateError) { + console.error(lateError); + process.exitCode = 1; + } + } +} finally { + if (browser) { + await browser.close(); + } + await new Promise((resolve) => server.close(resolve)); +} diff --git a/src_test/main.cpp b/src_test/main.cpp index 3a79b1c6..19782a0e 100644 --- a/src_test/main.cpp +++ b/src_test/main.cpp @@ -5,6 +5,21 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include + +EM_JS(void, llgi_report_test_result, (int result, const char* message), { + let text = UTF8ToString(message); + let status = result === 0 ? 'passed' : 'failed'; + if (status === 'passed' && Module.llgiLastWebGPUError) { + status = 'failed'; + text = Module.llgiLastWebGPUError; + } + Module.llgiTestResult = { status: status, message: text }; + console.log(status === 'passed' ? 'LLGI_TEST_PASS' : 'LLGI_TEST_FAIL', text); +}); +#endif + #ifdef _WIN32 #pragma comment(lib, "d3dcompiler.lib") @@ -14,7 +29,7 @@ #endif -#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32) +#if defined(__linux__) || defined(__APPLE__) || defined(_WIN32) || defined(__EMSCRIPTEN__) int main(int argc, char* argv[]) { @@ -28,6 +43,9 @@ int main(int argc, char* argv[]) // make shaders folder path from __FILE__ { +#if defined(__EMSCRIPTEN__) + TestHelper::SetRoot("/Shaders/WebGPU/"); +#else auto path = std::string(__FILE__); #if defined(WIN32) auto pos = path.find_last_of("\\"); @@ -57,6 +75,7 @@ int main(int argc, char* argv[]) { TestHelper::SetRoot((path + "/Shaders/WebGPU/").c_str()); } +#endif } LLGI::SetLogger([](LLGI::LogType logType, const std::string& message) { std::cerr << message << std::endl; }); @@ -69,6 +88,10 @@ int main(int argc, char* argv[]) TestHelper::Dispose(); +#ifdef __EMSCRIPTEN__ + llgi_report_test_result(0, "completed"); +#endif + return 0; } #endif diff --git a/src_test/test_webgpu_browser.cpp b/src_test/test_webgpu_browser.cpp new file mode 100644 index 00000000..af1cac16 --- /dev/null +++ b/src_test/test_webgpu_browser.cpp @@ -0,0 +1,520 @@ +#include "TestHelper.h" +#include "test.h" + +#include +#include +#include + +namespace +{ +struct BrowserComputeInput +{ + float value1; + float value2; +}; + +struct BrowserComputeOutput +{ + float value; +}; + +struct BrowserConstant +{ + float values[4]; +}; + +void configureSimpleVertexLayout(LLGI::PipelineState* pipelineState) +{ + pipelineState->VertexLayouts[0] = LLGI::VertexLayoutFormat::R32G32B32_FLOAT; + pipelineState->VertexLayouts[1] = LLGI::VertexLayoutFormat::R32G32_FLOAT; + pipelineState->VertexLayouts[2] = LLGI::VertexLayoutFormat::R8G8B8A8_UNORM; + pipelineState->VertexLayoutNames[0] = "POSITION"; + pipelineState->VertexLayoutNames[1] = "UV"; + pipelineState->VertexLayoutNames[2] = "COLOR"; + pipelineState->VertexLayoutCount = 3; +} + +void test_webgpu_browser_offscreen_render(LLGI::DeviceType deviceType) +{ + VERIFY(deviceType == LLGI::DeviceType::WebGPU); + + LLGI::PlatformParameter pp; + pp.Device = deviceType; + pp.WaitVSync = false; + + auto platform = LLGI::CreateSharedPtr(LLGI::CreatePlatform(pp, nullptr)); + VERIFY(platform != nullptr); + + auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); + VERIFY(graphics != nullptr); + + LLGI::RenderTextureInitializationParameter renderTextureParam; + renderTextureParam.Size = LLGI::Vec2I(64, 64); + renderTextureParam.Format = LLGI::TextureFormatType::R8G8B8A8_UNORM; + auto renderTexture = LLGI::CreateSharedPtr(graphics->CreateRenderTexture(renderTextureParam)); + VERIFY(renderTexture != nullptr); + + auto renderPass = LLGI::CreateSharedPtr(graphics->CreateRenderPass(renderTexture.get(), nullptr, nullptr, nullptr)); + VERIFY(renderPass != nullptr); + renderPass->SetClearColor(LLGI::Color8(32, 64, 96, 255)); + renderPass->SetIsColorCleared(true); + + std::shared_ptr shaderVS = nullptr; + std::shared_ptr shaderPS = nullptr; + TestHelper::CreateShader(graphics.get(), deviceType, "simple_rectangle.vert", "simple_rectangle.frag", shaderVS, shaderPS); + VERIFY(shaderVS != nullptr); + VERIFY(shaderPS != nullptr); + + std::shared_ptr vertexBuffer; + std::shared_ptr indexBuffer; + TestHelper::CreateRectangle(graphics.get(), + LLGI::Vec3F(-0.5f, 0.5f, 0.5f), + LLGI::Vec3F(0.5f, -0.5f, 0.5f), + LLGI::Color8(255, 255, 255, 255), + LLGI::Color8(0, 255, 0, 255), + vertexBuffer, + indexBuffer); + VERIFY(vertexBuffer != nullptr); + VERIFY(indexBuffer != nullptr); + + auto renderPassPipelineState = LLGI::CreateSharedPtr(graphics->CreateRenderPassPipelineState(renderPass.get())); + VERIFY(renderPassPipelineState != nullptr); + + auto pipelineState = LLGI::CreateSharedPtr(graphics->CreatePiplineState()); + VERIFY(pipelineState != nullptr); + configureSimpleVertexLayout(pipelineState.get()); + pipelineState->SetShader(LLGI::ShaderStageType::Vertex, shaderVS.get()); + pipelineState->SetShader(LLGI::ShaderStageType::Pixel, shaderPS.get()); + pipelineState->SetRenderPassPipelineState(renderPassPipelineState.get()); + VERIFY(pipelineState->Compile()); + + auto sfMemoryPool = LLGI::CreateSharedPtr(graphics->CreateSingleFrameMemoryPool(1024 * 1024, 16)); + VERIFY(sfMemoryPool != nullptr); + sfMemoryPool->NewFrame(); + + auto commandList = LLGI::CreateSharedPtr(graphics->CreateCommandList(sfMemoryPool.get())); + VERIFY(commandList != nullptr); + commandList->Begin(); + commandList->BeginRenderPass(renderPass.get()); + commandList->SetVertexBuffer(vertexBuffer.get(), sizeof(SimpleVertex), 0); + commandList->SetIndexBuffer(indexBuffer.get(), 2); + commandList->SetPipelineState(pipelineState.get()); + commandList->Draw(2); + commandList->EndRenderPass(); + commandList->End(); + + graphics->Execute(commandList.get()); + graphics->WaitFinish(); +} + +void test_webgpu_browser_texture_and_constant_render(LLGI::DeviceType deviceType) +{ + VERIFY(deviceType == LLGI::DeviceType::WebGPU); + + LLGI::PlatformParameter pp; + pp.Device = deviceType; + pp.WaitVSync = false; + + auto platform = LLGI::CreateSharedPtr(LLGI::CreatePlatform(pp, nullptr)); + VERIFY(platform != nullptr); + + auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); + VERIFY(graphics != nullptr); + + LLGI::TextureInitializationParameter textureParam; + textureParam.Size = LLGI::Vec2I(256, 256); + textureParam.Format = LLGI::TextureFormatType::R8G8B8A8_UNORM; + auto texture = LLGI::CreateSharedPtr(graphics->CreateTexture(textureParam)); + VERIFY(texture != nullptr); + TestHelper::WriteDummyTexture(texture.get()); + + LLGI::RenderTextureInitializationParameter renderTextureParam; + renderTextureParam.Size = LLGI::Vec2I(128, 128); + renderTextureParam.Format = LLGI::TextureFormatType::R8G8B8A8_UNORM; + auto renderTexture = LLGI::CreateSharedPtr(graphics->CreateRenderTexture(renderTextureParam)); + VERIFY(renderTexture != nullptr); + + auto renderPass = LLGI::CreateSharedPtr(graphics->CreateRenderPass(renderTexture.get(), nullptr, nullptr, nullptr)); + VERIFY(renderPass != nullptr); + renderPass->SetClearColor(LLGI::Color8(8, 16, 24, 255)); + renderPass->SetIsColorCleared(true); + + std::shared_ptr textureVS = nullptr; + std::shared_ptr texturePS = nullptr; + TestHelper::CreateShader(graphics.get(), deviceType, "simple_texture_rectangle.vert", "simple_texture_rectangle.frag", textureVS, texturePS); + VERIFY(textureVS != nullptr); + VERIFY(texturePS != nullptr); + + std::shared_ptr constantVS = nullptr; + std::shared_ptr constantPS = nullptr; + TestHelper::CreateShader(graphics.get(), deviceType, "simple_constant_rectangle.vert", "simple_constant_rectangle.frag", constantVS, constantPS); + VERIFY(constantVS != nullptr); + VERIFY(constantPS != nullptr); + + std::shared_ptr vertexBuffer; + std::shared_ptr indexBuffer; + TestHelper::CreateRectangle(graphics.get(), + LLGI::Vec3F(-0.8f, 0.8f, 0.5f), + LLGI::Vec3F(0.8f, -0.8f, 0.5f), + LLGI::Color8(255, 255, 255, 255), + LLGI::Color8(0, 255, 0, 255), + vertexBuffer, + indexBuffer); + VERIFY(vertexBuffer != nullptr); + VERIFY(indexBuffer != nullptr); + + auto renderPassPipelineState = LLGI::CreateSharedPtr(graphics->CreateRenderPassPipelineState(renderPass.get())); + VERIFY(renderPassPipelineState != nullptr); + + auto texturePipeline = LLGI::CreateSharedPtr(graphics->CreatePiplineState()); + VERIFY(texturePipeline != nullptr); + configureSimpleVertexLayout(texturePipeline.get()); + texturePipeline->SetShader(LLGI::ShaderStageType::Vertex, textureVS.get()); + texturePipeline->SetShader(LLGI::ShaderStageType::Pixel, texturePS.get()); + texturePipeline->SetRenderPassPipelineState(renderPassPipelineState.get()); + VERIFY(texturePipeline->Compile()); + + auto constantPipeline = LLGI::CreateSharedPtr(graphics->CreatePiplineState()); + VERIFY(constantPipeline != nullptr); + configureSimpleVertexLayout(constantPipeline.get()); + constantPipeline->SetShader(LLGI::ShaderStageType::Vertex, constantVS.get()); + constantPipeline->SetShader(LLGI::ShaderStageType::Pixel, constantPS.get()); + constantPipeline->SetRenderPassPipelineState(renderPassPipelineState.get()); + VERIFY(constantPipeline->Compile()); + + auto vertexConstantBuffer = LLGI::CreateSharedPtr( + graphics->CreateBuffer(LLGI::BufferUsageType::Constant | LLGI::BufferUsageType::MapWrite, sizeof(BrowserConstant))); + auto pixelConstantBuffer = LLGI::CreateSharedPtr( + graphics->CreateBuffer(LLGI::BufferUsageType::Constant | LLGI::BufferUsageType::MapWrite, sizeof(BrowserConstant))); + VERIFY(vertexConstantBuffer != nullptr); + VERIFY(pixelConstantBuffer != nullptr); + + { + auto data = static_cast(vertexConstantBuffer->Lock()); + data->values[0] = 0.15f; + data->values[1] = -0.10f; + data->values[2] = 0.0f; + data->values[3] = 0.0f; + vertexConstantBuffer->Unlock(); + } + { + auto data = static_cast(pixelConstantBuffer->Lock()); + data->values[0] = 0.05f; + data->values[1] = 0.10f; + data->values[2] = 0.15f; + data->values[3] = 0.0f; + pixelConstantBuffer->Unlock(); + } + + auto sfMemoryPool = LLGI::CreateSharedPtr(graphics->CreateSingleFrameMemoryPool(1024 * 1024, 16)); + VERIFY(sfMemoryPool != nullptr); + sfMemoryPool->NewFrame(); + + auto commandList = LLGI::CreateSharedPtr(graphics->CreateCommandList(sfMemoryPool.get())); + VERIFY(commandList != nullptr); + commandList->Begin(); + commandList->BeginRenderPass(renderPass.get()); + commandList->SetVertexBuffer(vertexBuffer.get(), sizeof(SimpleVertex), 0); + commandList->SetIndexBuffer(indexBuffer.get(), 2); + commandList->SetPipelineState(texturePipeline.get()); + commandList->SetTexture(texture.get(), LLGI::TextureWrapMode::Clamp, LLGI::TextureMinMagFilter::Linear, 0); + commandList->Draw(2); + commandList->ResetTextures(); + commandList->SetPipelineState(constantPipeline.get()); + commandList->SetConstantBuffer(vertexConstantBuffer.get(), 0); + commandList->SetConstantBuffer(pixelConstantBuffer.get(), 1); + commandList->Draw(2); + commandList->EndRenderPass(); + commandList->End(); + + graphics->Execute(commandList.get()); + graphics->WaitFinish(); + + const auto data = graphics->CaptureRenderTarget(renderTexture.get()); + VERIFY(data.size() == 128 * 128 * 4); + + const auto verifyPixel = [&data](int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + const auto* pixel = data.data() + (x + y * 128) * 4; + const bool matched = std::abs(static_cast(pixel[0]) - static_cast(r)) <= 1 && + std::abs(static_cast(pixel[1]) - static_cast(g)) <= 1 && + std::abs(static_cast(pixel[2]) - static_cast(b)) <= 1 && + std::abs(static_cast(pixel[3]) - static_cast(a)) <= 1; + if (!matched) + { + std::cout << "Pixel mismatch at " << x << "," << y << " actual=" << static_cast(pixel[0]) << "," + << static_cast(pixel[1]) << "," << static_cast(pixel[2]) << "," << static_cast(pixel[3]) + << " expected=" << static_cast(r) << "," << static_cast(g) << "," << static_cast(b) << "," + << static_cast(a) << std::endl; + } + VERIFY(matched); + }; + + verifyPixel(64, 64, 155, 255, 188, 255); + verifyPixel(2, 2, 8, 16, 24, 255); +} + +void test_webgpu_browser_compute_compile(LLGI::DeviceType deviceType) +{ + VERIFY(deviceType == LLGI::DeviceType::WebGPU); + + LLGI::PlatformParameter pp; + pp.Device = deviceType; + pp.WaitVSync = false; + + auto platform = LLGI::CreateSharedPtr(LLGI::CreatePlatform(pp, nullptr)); + VERIFY(platform != nullptr); + + auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); + VERIFY(graphics != nullptr); + + std::shared_ptr shaderCS = nullptr; + TestHelper::CreateComputeShader(graphics.get(), deviceType, "basic.comp", shaderCS); + VERIFY(shaderCS != nullptr); + + auto pipelineState = LLGI::CreateSharedPtr(graphics->CreatePiplineState()); + VERIFY(pipelineState != nullptr); + pipelineState->SetShader(LLGI::ShaderStageType::Compute, shaderCS.get()); + VERIFY(pipelineState->Compile()); +} + +void test_webgpu_browser_compute_dispatch(LLGI::DeviceType deviceType) +{ + VERIFY(deviceType == LLGI::DeviceType::WebGPU); + + LLGI::PlatformParameter pp; + pp.Device = deviceType; + pp.WaitVSync = false; + + auto platform = LLGI::CreateSharedPtr(LLGI::CreatePlatform(pp, nullptr)); + VERIFY(platform != nullptr); + + auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); + VERIFY(graphics != nullptr); + + std::shared_ptr shaderCS = nullptr; + TestHelper::CreateComputeShader(graphics.get(), deviceType, "basic.comp", shaderCS); + VERIFY(shaderCS != nullptr); + + auto pipelineState = LLGI::CreateSharedPtr(graphics->CreatePiplineState()); + VERIFY(pipelineState != nullptr); + pipelineState->SetShader(LLGI::ShaderStageType::Compute, shaderCS.get()); + VERIFY(pipelineState->Compile()); + + const int dataSize = 32; + auto uploadBuffer = LLGI::CreateSharedPtr( + graphics->CreateBuffer(LLGI::BufferUsageType::MapWrite | LLGI::BufferUsageType::CopySrc, sizeof(BrowserComputeInput) * dataSize)); + auto inputBuffer = LLGI::CreateSharedPtr( + graphics->CreateBuffer(LLGI::BufferUsageType::ComputeWrite | LLGI::BufferUsageType::CopyDst, sizeof(BrowserComputeInput) * dataSize)); + auto outputBuffer = LLGI::CreateSharedPtr( + graphics->CreateBuffer(LLGI::BufferUsageType::ComputeWrite | LLGI::BufferUsageType::CopySrc, sizeof(BrowserComputeOutput) * dataSize)); + auto readbackTarget = LLGI::CreateSharedPtr( + graphics->CreateBuffer(LLGI::BufferUsageType::MapRead | LLGI::BufferUsageType::CopyDst, sizeof(BrowserComputeOutput) * dataSize)); + auto constantBuffer = LLGI::CreateSharedPtr( + graphics->CreateBuffer(LLGI::BufferUsageType::Constant | LLGI::BufferUsageType::MapWrite, sizeof(float))); + VERIFY(uploadBuffer != nullptr); + VERIFY(inputBuffer != nullptr); + VERIFY(outputBuffer != nullptr); + VERIFY(readbackTarget != nullptr); + VERIFY(constantBuffer != nullptr); + + { + auto data = static_cast(uploadBuffer->Lock()); + for (int i = 0; i < dataSize; i++) + { + data[i].value1 = static_cast(i + 1); + data[i].value2 = static_cast(i + 3); + } + uploadBuffer->Unlock(); + } + { + auto data = static_cast(constantBuffer->Lock()); + *data = 7.0f; + constantBuffer->Unlock(); + } + + auto sfMemoryPool = LLGI::CreateSharedPtr(graphics->CreateSingleFrameMemoryPool(1024 * 1024, 16)); + VERIFY(sfMemoryPool != nullptr); + sfMemoryPool->NewFrame(); + + auto commandList = LLGI::CreateSharedPtr(graphics->CreateCommandList(sfMemoryPool.get())); + VERIFY(commandList != nullptr); + commandList->Begin(); + commandList->CopyBuffer(uploadBuffer.get(), inputBuffer.get()); + commandList->BeginComputePass(); + commandList->SetPipelineState(pipelineState.get()); + commandList->SetComputeBuffer(inputBuffer.get(), sizeof(BrowserComputeInput), 0, false); + commandList->SetComputeBuffer(outputBuffer.get(), sizeof(BrowserComputeOutput), 1, false); + commandList->SetConstantBuffer(constantBuffer.get(), 0); + commandList->Dispatch(dataSize, 1, 1, 1, 1, 1); + commandList->EndComputePass(); + commandList->CopyBuffer(outputBuffer.get(), readbackTarget.get()); + commandList->End(); + + graphics->Execute(commandList.get()); + graphics->WaitFinish(); + + auto result = static_cast(readbackTarget->Lock()); + VERIFY(result != nullptr); + for (int i = 0; i < dataSize; i++) + { + const float expected = static_cast(i + 1) * static_cast(i + 3) + 7.0f; + VERIFY(std::fabs(result[i].value - expected) < 0.001f); + } + readbackTarget->Unlock(); +} + +void test_webgpu_browser_render_readback(LLGI::DeviceType deviceType) +{ + VERIFY(deviceType == LLGI::DeviceType::WebGPU); + + LLGI::PlatformParameter pp; + pp.Device = deviceType; + pp.WaitVSync = false; + + auto platform = LLGI::CreateSharedPtr(LLGI::CreatePlatform(pp, nullptr)); + VERIFY(platform != nullptr); + + auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); + VERIFY(graphics != nullptr); + + LLGI::RenderTextureInitializationParameter renderTextureParam; + renderTextureParam.Size = LLGI::Vec2I(16, 16); + renderTextureParam.Format = LLGI::TextureFormatType::R8G8B8A8_UNORM; + auto renderTexture = LLGI::CreateSharedPtr(graphics->CreateRenderTexture(renderTextureParam)); + VERIFY(renderTexture != nullptr); + + auto renderPass = LLGI::CreateSharedPtr(graphics->CreateRenderPass(renderTexture.get(), nullptr, nullptr, nullptr)); + VERIFY(renderPass != nullptr); + renderPass->SetClearColor(LLGI::Color8(11, 22, 33, 255)); + renderPass->SetIsColorCleared(true); + + auto sfMemoryPool = LLGI::CreateSharedPtr(graphics->CreateSingleFrameMemoryPool(1024 * 1024, 16)); + VERIFY(sfMemoryPool != nullptr); + sfMemoryPool->NewFrame(); + + auto commandList = LLGI::CreateSharedPtr(graphics->CreateCommandList(sfMemoryPool.get())); + VERIFY(commandList != nullptr); + commandList->Begin(); + commandList->BeginRenderPass(renderPass.get()); + commandList->EndRenderPass(); + commandList->End(); + + graphics->Execute(commandList.get()); + graphics->WaitFinish(); + + const auto data = graphics->CaptureRenderTarget(renderTexture.get()); + VERIFY(data.size() == 16 * 16 * 4); + for (int i = 0; i < 16 * 16; i++) + { + const auto* pixel = data.data() + i * 4; + VERIFY(pixel[0] == 11); + VERIFY(pixel[1] == 22); + VERIFY(pixel[2] == 33); + VERIFY(pixel[3] == 255); + } +} + +void test_webgpu_browser_screen_presentation(LLGI::DeviceType deviceType) +{ + VERIFY(deviceType == LLGI::DeviceType::WebGPU); + + LLGI::PlatformParameter pp; + pp.Device = deviceType; + pp.WaitVSync = false; + + auto window = std::unique_ptr(LLGI::CreateWindow("WebGPU Browser", LLGI::Vec2I(640, 360))); + VERIFY(window != nullptr); + + auto platform = LLGI::CreateSharedPtr(LLGI::CreatePlatform(pp, window.get())); + VERIFY(platform != nullptr); + + auto graphics = LLGI::CreateSharedPtr(platform->CreateGraphics()); + VERIFY(graphics != nullptr); + + auto sfMemoryPool = LLGI::CreateSharedPtr(graphics->CreateSingleFrameMemoryPool(1024 * 1024, 16)); + VERIFY(sfMemoryPool != nullptr); + + std::shared_ptr shaderVS = nullptr; + std::shared_ptr shaderPS = nullptr; + TestHelper::CreateShader(graphics.get(), deviceType, "simple_rectangle.vert", "simple_rectangle.frag", shaderVS, shaderPS); + VERIFY(shaderVS != nullptr); + VERIFY(shaderPS != nullptr); + + std::shared_ptr vertexBuffer; + std::shared_ptr indexBuffer; + TestHelper::CreateRectangle(graphics.get(), + LLGI::Vec3F(-0.6f, 0.6f, 0.5f), + LLGI::Vec3F(0.6f, -0.6f, 0.5f), + LLGI::Color8(255, 255, 255, 255), + LLGI::Color8(0, 255, 96, 255), + vertexBuffer, + indexBuffer); + VERIFY(vertexBuffer != nullptr); + VERIFY(indexBuffer != nullptr); + + std::shared_ptr renderPassPipelineState; + std::shared_ptr pipelineState; + + for (int frame = 0; frame < 3; frame++) + { + VERIFY(platform->NewFrame()); + sfMemoryPool->NewFrame(); + + const LLGI::Color8 color(28, 96, 180, 255); + auto renderPass = platform->GetCurrentScreen(color, true, false); + VERIFY(renderPass != nullptr); + if (pipelineState == nullptr) + { + renderPassPipelineState = LLGI::CreateSharedPtr(graphics->CreateRenderPassPipelineState(renderPass)); + VERIFY(renderPassPipelineState != nullptr); + + pipelineState = LLGI::CreateSharedPtr(graphics->CreatePiplineState()); + VERIFY(pipelineState != nullptr); + configureSimpleVertexLayout(pipelineState.get()); + pipelineState->SetShader(LLGI::ShaderStageType::Vertex, shaderVS.get()); + pipelineState->SetShader(LLGI::ShaderStageType::Pixel, shaderPS.get()); + pipelineState->SetRenderPassPipelineState(renderPassPipelineState.get()); + VERIFY(pipelineState->Compile()); + } + + auto commandList = LLGI::CreateSharedPtr(graphics->CreateCommandList(sfMemoryPool.get())); + VERIFY(commandList != nullptr); + commandList->Begin(); + commandList->BeginRenderPass(renderPass); + commandList->SetVertexBuffer(vertexBuffer.get(), sizeof(SimpleVertex), 0); + commandList->SetIndexBuffer(indexBuffer.get(), 2); + commandList->SetPipelineState(pipelineState.get()); + commandList->Draw(2); + commandList->EndRenderPass(); + commandList->End(); + + graphics->Execute(commandList.get()); + platform->Present(); + graphics->WaitFinish(); + } +} +} // namespace + +TestRegister WebGPUBrowser_OffscreenRender( + "WebGPUBrowser.OffscreenRender", + [](LLGI::DeviceType device) -> void { test_webgpu_browser_offscreen_render(device); }); + +TestRegister WebGPUBrowser_ComputeCompile( + "WebGPUBrowser.ComputeCompile", + [](LLGI::DeviceType device) -> void { test_webgpu_browser_compute_compile(device); }); + +TestRegister WebGPUBrowser_TextureAndConstantRender( + "WebGPUBrowser.TextureAndConstantRender", + [](LLGI::DeviceType device) -> void { test_webgpu_browser_texture_and_constant_render(device); }); + +TestRegister WebGPUBrowser_ComputeDispatch( + "WebGPUBrowser.ComputeDispatch", + [](LLGI::DeviceType device) -> void { test_webgpu_browser_compute_dispatch(device); }); + +TestRegister WebGPUBrowser_RenderReadback( + "WebGPUBrowser.RenderReadback", + [](LLGI::DeviceType device) -> void { test_webgpu_browser_render_readback(device); }); + +TestRegister WebGPUBrowser_ScreenPresentation( + "WebGPUBrowser.ScreenPresentation", + [](LLGI::DeviceType device) -> void { test_webgpu_browser_screen_presentation(device); }); From dfaf25f222dc9f7e9f4a965f720cc9df96199295 Mon Sep 17 00:00:00 2001 From: durswd Date: Sat, 2 May 2026 23:06:36 +0900 Subject: [PATCH 13/16] Update about WebGPU --- docs/WebGPU_Browser_Test.md | 219 ++++++++++------- package-lock.json | 56 ----- package.json | 5 - src/WebGPU/LLGI.PlatformWebGPU.cpp | 3 + src_test/browser/run_webgpu_browser_test.mjs | 235 ++++++++++++++++--- 5 files changed, 336 insertions(+), 182 deletions(-) delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/docs/WebGPU_Browser_Test.md b/docs/WebGPU_Browser_Test.md index 00417ef3..08c64053 100644 --- a/docs/WebGPU_Browser_Test.md +++ b/docs/WebGPU_Browser_Test.md @@ -1,32 +1,74 @@ # WebGPU Browser Test -This document describes how to build and run the browser-only WebGPU backend -tests. These tests compile LLGI to WebAssembly with Emscripten, run it in a real -Chromium-family browser, and use Emdawnwebgpu to access the browser WebGPU API. +This document explains how to build and run LLGI's browser-only WebGPU tests. +The browser test target compiles LLGI to WebAssembly with Emscripten, runs it in +an installed Chrome or Edge browser, and accesses the browser WebGPU API through +Emdawnwebgpu. -The native WebGPU path in `docs/WebGPU.md` uses Dawn directly. The browser path -covered here verifies a separate runtime: +The native WebGPU path in `docs/WebGPU.md` uses Dawn directly. This document is +for the browser runtime, which verifies: - Emscripten compilation and linking - Emdawnwebgpu C/C++ bindings -- browser `navigator.gpu` adapter/device creation +- browser `navigator.gpu` adapter and device creation - WGSL shader module creation - render and compute pipeline creation -- browser canvas surface presentation - offscreen render target readback - storage buffer compute readback +- browser canvas surface presentation ## Requirements - CMake 3.15 or newer - Git -- Node.js +- Node.js 22 or newer - Emscripten 4.x or newer with `--use-port=emdawnwebgpu` -- Playwright -- A WebGPU-capable Chromium, Chrome, or Edge browser +- An installed WebGPU-capable Chrome or Edge browser + +The automated runner does not use Playwright. It starts an installed browser +directly and controls it through the Chrome DevTools Protocol. + +WebGPU also requires a secure context. The runner serves the generated files +from `127.0.0.1`; do not open `LLGI_Test.html` directly with `file://`. + +## Quick Start + +From the repository root, after activating emsdk: + +```bash +emcmake cmake -S . -B build-webgpu-browser \ + -DBUILD_WEBGPU=ON \ + -DBUILD_WEBGPU_BROWSER_TEST=ON \ + -DBUILD_TEST=ON +cmake --build build-webgpu-browser --target LLGI_Test +node src_test/browser/run_webgpu_browser_test.mjs \ + build-webgpu-browser/src_test/LLGI_Test.html +``` + +PowerShell: -WebGPU requires a secure context. The runner serves the generated files from -`localhost`; do not open `LLGI_Test.html` directly with `file://`. +```powershell +emcmake cmake -S . -B build-webgpu-browser ` + -DBUILD_WEBGPU=ON ` + -DBUILD_WEBGPU_BROWSER_TEST=ON ` + -DBUILD_TEST=ON +cmake --build build-webgpu-browser --target LLGI_Test +node src_test/browser/run_webgpu_browser_test.mjs ` + build-webgpu-browser/src_test/LLGI_Test.html +``` + +If Chrome or Edge is installed in a non-standard location, set `CHROME_PATH` +before running the Node.js runner: + +```powershell +$env:CHROME_PATH = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" +``` + +A successful run ends with: + +```text +LLGI_TEST_PASS completed +``` ## Install Emscripten @@ -59,21 +101,29 @@ emcmake --version Run the emsdk environment script again whenever you open a new shell. -## Install Playwright +## Select a Browser -From the LLGI repository root: +The runner auto-detects common Chrome and Edge installation paths. You can +override the browser path with `CHROME_PATH`: -```bash -npm install playwright -npx playwright install chromium +```powershell +$env:CHROME_PATH = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" ``` -On Linux CI or minimal Linux installations, install browser system dependencies: +Common Windows paths are: -```bash -npx playwright install-deps chromium +```powershell +$env:CHROME_PATH = "C:\Program Files\Google\Chrome\Application\chrome.exe" +$env:CHROME_PATH = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" +$env:CHROME_PATH = "C:\Program Files\Microsoft\Edge\Application\msedge.exe" +$env:CHROME_PATH = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" ``` +On this Windows environment, Playwright's downloaded Chromium/headless shell may +expose `navigator.gpu` but still return no adapter from +`navigator.gpu.requestAdapter()`. Use installed Chrome or Edge when running +these tests. + ## Configure Use Emscripten's CMake wrapper: @@ -85,7 +135,7 @@ emcmake cmake -S . -B build-webgpu-browser \ -DBUILD_TEST=ON ``` -PowerShell equivalent: +PowerShell: ```powershell emcmake cmake -S . -B build-webgpu-browser ` @@ -94,7 +144,7 @@ emcmake cmake -S . -B build-webgpu-browser ` -DBUILD_TEST=ON ``` -`BUILD_WEBGPU_BROWSER_TEST=ON` requires an Emscripten toolchain and builds a +`BUILD_WEBGPU_BROWSER_TEST=ON` requires the Emscripten toolchain and builds a browser-focused `LLGI_Test.html`. It does not require a native Dawn checkout. ## Build @@ -124,9 +174,9 @@ cmake --build build-webgpu-browser --target LLGI_Test For CI, cache `EM_CACHE` between runs to avoid rebuilding Emscripten system libraries and the Emdawnwebgpu port every time. -## Run Automated Tests +## Run Tests -Run the Playwright harness from the repository root: +Run the browser harness from the repository root: ```bash node src_test/browser/run_webgpu_browser_test.mjs \ @@ -142,18 +192,15 @@ node src_test/browser/run_webgpu_browser_test.mjs ` The runner: -- starts a temporary localhost HTTP server +- starts a temporary `127.0.0.1` HTTP server - disables caching for generated `.html`, `.js`, `.wasm`, and `.data` files -- launches Chromium with WebGPU-friendly flags +- launches installed Chrome or Edge in headless mode +- passes WebGPU-friendly browser flags +- connects to the browser through the Chrome DevTools Protocol +- forwards browser console and page errors - waits for the Emscripten module to report completion - exits non-zero on failure -The success log ends with: - -```text -LLGI_TEST_PASS completed -``` - The default filter is: ```text @@ -168,9 +215,10 @@ node src_test/browser/run_webgpu_browser_test.mjs \ --filter=WebGPUBrowser.ScreenPresentation ``` -## View in a Browser +## View a Canvas Test -To see the canvas presentation test, serve the generated files: +Most tests render offscreen and validate the result through readback. To see the +visible canvas presentation test, serve the generated files: ```bash cd build-webgpu-browser/src_test @@ -199,48 +247,34 @@ When Node.js is found during CMake configure, CMake registers: ctest --test-dir build-webgpu-browser -R LLGI_WebGPU_Browser --output-on-failure ``` -The CTest entry expects the `playwright` package to be available to Node.js. - -## Current Test Cases - -- `WebGPUBrowser.ComputeCompile` - - loads a WGSL compute shader - - compiles a compute pipeline -- `WebGPUBrowser.ComputeDispatch` - - uploads structured input data - - dispatches a storage-buffer compute shader - - copies output to a readback buffer - - maps and verifies computed values -- `WebGPUBrowser.OffscreenRender` - - creates an offscreen render texture - - loads WGSL vertex/fragment shaders - - compiles a render pipeline - - draws a rectangle -- `WebGPUBrowser.RenderReadback` - - clears an offscreen render target - - copies it to a readback buffer - - verifies pixel values -- `WebGPUBrowser.TextureAndConstantRender` - - uploads texture data with `Queue::WriteTexture` - - renders with texture and sampler bind groups - - renders again with vertex/pixel uniform buffers - - reads back pixels and verifies clear color and rendered output -- `WebGPUBrowser.ScreenPresentation` - - creates a browser canvas WebGPU surface - - renders through `PlatformWebGPU::GetCurrentScreen` - - leaves a visible blue canvas with a colored polygon +The CTest entry uses the same Node.js runner. It expects a WebGPU-capable Chrome +or Edge executable. Set `CHROME_PATH` in the test environment when +auto-detection is not enough. + +## Test Cases + +- `WebGPUBrowser.ComputeCompile`: loads a WGSL compute shader and compiles a + compute pipeline +- `WebGPUBrowser.ComputeDispatch`: dispatches a storage-buffer compute shader + and verifies mapped readback data +- `WebGPUBrowser.OffscreenRender`: renders to an offscreen texture +- `WebGPUBrowser.RenderReadback`: clears an offscreen render target and verifies + copied pixel data +- `WebGPUBrowser.TextureAndConstantRender`: uploads texture and uniform data, + renders, and verifies readback pixels +- `WebGPUBrowser.ScreenPresentation`: renders through + `PlatformWebGPU::GetCurrentScreen` to the browser canvas Browser readback uses Asyncify so C++ test code can wait for `MapAsync` and `Queue::OnSubmittedWorkDone` callbacks while the browser event loop continues to -run. +run. The browser WebGPU instance enables `TimedWaitAny` because the readback +paths use timeout-based `Instance::WaitAny` calls. ## CI Notes A typical CI flow is: ```bash -npm install playwright -npx playwright install chromium emcmake cmake -S . -B build-webgpu-browser \ -DBUILD_WEBGPU=ON \ -DBUILD_WEBGPU_BROWSER_TEST=ON \ @@ -250,49 +284,60 @@ node src_test/browser/run_webgpu_browser_test.mjs \ build-webgpu-browser/src_test/LLGI_Test.html ``` -The runner passes these Chromium flags: +The runner passes these browser flags: +- `--headless=new` - `--enable-unsafe-webgpu` - `--ignore-gpu-blocklist` -- `--enable-features=Vulkan,UseSkiaRenderer` -- `--use-vulkan=swiftshader` +- `--use-angle=d3d11` -These help in headless or GPU-limited environments, but browser policy, -drivers, or CI sandboxing can still disable WebGPU. +`--use-angle=d3d11` is important on the tested Windows environment. Vulkan or +SwiftShader flags can leave `navigator.gpu.requestAdapter()` returning `null`. ## Troubleshooting ### `navigator.gpu is not available` -Use `http://localhost` or HTTPS. WebGPU is unavailable from `file://`. - -Also check that the browser supports WebGPU and is not blocked by local GPU -policy. - -### Playwright fails to launch Chromium +Use `http://localhost`, `http://127.0.0.1`, or HTTPS. WebGPU is unavailable from +`file://`. -Install the browser: +Also check that the selected browser supports WebGPU and is not blocked by local +GPU policy. -```bash -npx playwright install chromium -``` +### `No available adapters` -If Playwright's downloaded Chromium cannot launch, use an installed browser: +Use an installed Chrome or Edge browser instead of Playwright's downloaded +Chromium/headless shell. On Windows: ```powershell -$env:CHROME_PATH = "C:\Program Files\Google\Chrome\Application\chrome.exe" +$env:CHROME_PATH = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" node src_test/browser/run_webgpu_browser_test.mjs ` build-webgpu-browser/src_test/LLGI_Test.html ``` -Common Windows alternatives: +The runner uses `--use-angle=d3d11`. Vulkan or SwiftShader flags can leave +`requestAdapter()` returning `null` on this machine. + +### `A WebGPU-capable Chrome or Edge executable is required` + +Set `CHROME_PATH` to the browser executable: ```powershell $env:CHROME_PATH = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -$env:CHROME_PATH = "C:\Program Files\Microsoft\Edge\Application\msedge.exe" -$env:CHROME_PATH = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" ``` +### `TimedWaitAny not enabled at wgpuCreateInstance` + +Rebuild `LLGI_Test` after changing WebGPU source files: + +```bash +cmake --build build-webgpu-browser --target LLGI_Test +``` + +The browser path requires `wgpu::InstanceFeatureName::TimedWaitAny` when +creating the Emscripten WebGPU instance because readback waits use timeout-based +`Instance::WaitAny`. + ### Emdawnwebgpu port build fails with a cross-drive path error Put `EM_CACHE` on the same drive as emsdk: diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f1e3a8fc..00000000 --- a/package-lock.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "LLGI", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "playwright": "^1.59.1" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.59.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 8a8fbea3..00000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "playwright": "^1.59.1" - } -} diff --git a/src/WebGPU/LLGI.PlatformWebGPU.cpp b/src/WebGPU/LLGI.PlatformWebGPU.cpp index 486b2e0f..dec71796 100644 --- a/src/WebGPU/LLGI.PlatformWebGPU.cpp +++ b/src/WebGPU/LLGI.PlatformWebGPU.cpp @@ -122,6 +122,9 @@ bool PlatformWebGPU::Initialize(Window* window, bool waitVSync) #if defined(__EMSCRIPTEN__) wgpu::InstanceDescriptor instanceDescriptor{}; + static constexpr auto timedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny; + instanceDescriptor.requiredFeatureCount = 1; + instanceDescriptor.requiredFeatures = &timedWaitAny; instance_ = wgpu::CreateInstance(&instanceDescriptor); if (instance_ == nullptr) { diff --git a/src_test/browser/run_webgpu_browser_test.mjs b/src_test/browser/run_webgpu_browser_test.mjs index 4963678f..eea89d89 100644 --- a/src_test/browser/run_webgpu_browser_test.mjs +++ b/src_test/browser/run_webgpu_browser_test.mjs @@ -1,14 +1,8 @@ import http from 'node:http'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; - -let chromium; -try { - ({chromium} = await import('playwright')); -} catch (error) { - console.error('The "playwright" package is required. Install it with: npm install playwright && npx playwright install chromium'); - process.exit(2); -} +import {spawn} from 'node:child_process'; const args = process.argv.slice(2); const htmlPath = args[0]; @@ -28,7 +22,29 @@ if (!fs.existsSync(resolvedHtmlPath)) { const root = path.dirname(resolvedHtmlPath); const htmlFile = path.basename(resolvedHtmlPath); -const executablePath = process.env.CHROME_PATH || process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; +const executablePath = findBrowserExecutable(); + +function findBrowserExecutable() { + const candidates = [ + process.env.CHROME_PATH, + process.env.EDGE_PATH, + 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', + 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', + ].filter(Boolean); + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + + console.error('A WebGPU-capable Chrome or Edge executable is required.'); + console.error('Set CHROME_PATH, for example:'); + console.error('$env:CHROME_PATH = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"'); + process.exit(2); +} function contentType(filePath) { if (filePath.endsWith('.html')) return 'text/html'; @@ -68,44 +84,195 @@ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const {port} = server.address(); const url = `http://127.0.0.1:${port}/${htmlFile}?filter=${encodeURIComponent(filter)}`; -let browser; +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function getFreePort() { + const probe = http.createServer(); + await new Promise((resolve) => probe.listen(0, '127.0.0.1', resolve)); + const freePort = probe.address().port; + await new Promise((resolve) => probe.close(resolve)); + return freePort; +} + +async function waitForDevToolsHttp(devtoolsPort, chrome, getStderr) { + const start = Date.now(); + while (Date.now() - start < 30000) { + if (chrome.exitCode !== null) { + throw new Error(`Chrome exited before DevTools was ready.${getStderr() ? `\n${getStderr()}` : ''}`); + } + try { + const response = await fetch(`http://127.0.0.1:${devtoolsPort}/json/version`); + if (response.ok) { + return; + } + } catch { + } + await delay(100); + } + throw new Error('Timed out waiting for Chrome DevTools HTTP endpoint.'); +} + +async function createPage(devtoolsPort, pageUrl) { + const response = await fetch(`http://127.0.0.1:${devtoolsPort}/json/new?${encodeURIComponent(pageUrl)}`, {method: 'PUT'}); + if (!response.ok) { + throw new Error(`Failed to create Chrome page: ${response.status} ${response.statusText}`); + } + return await response.json(); +} + +class CdpClient { + constructor(webSocketUrl) { + this.nextId = 1; + this.pending = new Map(); + this.handlers = new Map(); + this.socket = new WebSocket(webSocketUrl); + this.socket.addEventListener('message', (event) => this.onMessage(event)); + } + + async open() { + if (this.socket.readyState === WebSocket.OPEN) { + return; + } + await new Promise((resolve, reject) => { + this.socket.addEventListener('open', resolve, {once: true}); + this.socket.addEventListener('error', reject, {once: true}); + }); + } + + on(eventName, handler) { + const handlers = this.handlers.get(eventName) || []; + handlers.push(handler); + this.handlers.set(eventName, handlers); + } + + onMessage(event) { + const message = JSON.parse(event.data); + if (message.id && this.pending.has(message.id)) { + const {resolve, reject} = this.pending.get(message.id); + this.pending.delete(message.id); + if (message.error) { + reject(new Error(message.error.message)); + } else { + resolve(message.result); + } + return; + } + + for (const handler of this.handlers.get(message.method) || []) { + handler(message.params || {}); + } + } + + send(method, params = {}) { + const id = this.nextId++; + this.socket.send(JSON.stringify({id, method, params})); + return new Promise((resolve, reject) => { + this.pending.set(id, {resolve, reject}); + }); + } + + close() { + this.socket.close(); + } +} + +function consoleArgumentToString(argument) { + if ('value' in argument) { + return String(argument.value); + } + return argument.description || argument.unserializableValue || ''; +} + +async function evaluateJson(client, expression) { + const result = await client.send('Runtime.evaluate', { + expression: `JSON.stringify((${expression}))`, + awaitPromise: true, + returnByValue: true, + }); + return result.result && result.result.value ? JSON.parse(result.result.value) : null; +} + +async function waitForTestResult(client, timeoutMs) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const value = await evaluateJson(client, 'globalThis.Module && globalThis.Module.llgiTestResult || null'); + if (value) { + return value; + } + await delay(100); + } + throw new Error('Timed out waiting for LLGI browser WebGPU test result.'); +} + +let chrome; +let client; +let userDataDir; +let chromeExit; +let chromeStderr = ''; try { - browser = await chromium.launch({ - headless: true, - executablePath, - args: [ - '--enable-unsafe-webgpu', - '--ignore-gpu-blocklist', - '--enable-features=Vulkan,UseSkiaRenderer', - '--use-vulkan=swiftshader', - ], + userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'llgi-webgpu-chrome-')); + const devtoolsPort = await getFreePort(); + chrome = spawn(executablePath, [ + '--headless=new', + `--remote-debugging-port=${devtoolsPort}`, + '--remote-debugging-address=127.0.0.1', + `--user-data-dir=${userDataDir}`, + '--no-first-run', + '--no-default-browser-check', + '--enable-unsafe-webgpu', + '--ignore-gpu-blocklist', + '--use-angle=d3d11', + 'about:blank', + ], {stdio: ['ignore', 'ignore', 'pipe']}); + chrome.stderr.on('data', (chunk) => { + chromeStderr += chunk.toString(); + }); + chromeExit = new Promise((resolve) => chrome.once('exit', resolve)); + + await waitForDevToolsHttp(devtoolsPort, chrome, () => chromeStderr.trim()); + const page = await createPage(devtoolsPort, url); + client = new CdpClient(page.webSocketDebuggerUrl); + await client.open(); + client.on('Runtime.consoleAPICalled', (params) => { + const text = (params.args || []).map(consoleArgumentToString).join(' '); + console.log(`[browser:${params.type}] ${text}`); + }); + client.on('Runtime.exceptionThrown', (params) => { + const details = params.exceptionDetails || {}; + const exception = details.exception || {}; + console.error(`[browser:pageerror] ${exception.description || details.text || 'Unhandled exception'}`); }); - const page = await browser.newPage(); - page.on('console', (message) => console.log(`[browser:${message.type()}] ${message.text()}`)); - page.on('pageerror', (error) => console.error(`[browser:pageerror] ${error.message}`)); - - await page.goto(url, {waitUntil: 'load'}); - const result = await page.waitForFunction( - () => globalThis.Module && globalThis.Module.llgiTestResult, - null, - {timeout: 60000} - ); - const value = await result.jsonValue(); + await client.send('Runtime.enable'); + await client.send('Page.enable'); + const value = await waitForTestResult(client, 60000); if (!value || value.status !== 'passed') { console.error(value && value.message ? value.message : 'LLGI browser WebGPU test failed.'); process.exitCode = 1; } else { - await page.waitForTimeout(500); - const lateError = await page.evaluate(() => globalThis.Module && globalThis.Module.llgiLastWebGPUError); + await delay(500); + const lateError = await evaluateJson(client, 'globalThis.Module && globalThis.Module.llgiLastWebGPUError || null'); if (lateError) { console.error(lateError); process.exitCode = 1; } } } finally { - if (browser) { - await browser.close(); + if (client) { + await client.send('Browser.close').catch(() => {}); + client.close(); + } + if (chrome) { + await Promise.race([chromeExit, delay(2000)]); + if (chrome.exitCode === null) { + chrome.kill(); + await Promise.race([chromeExit, delay(2000)]); + } + } + if (userDataDir) { + fs.rmSync(userDataDir, {recursive: true, force: true, maxRetries: 10, retryDelay: 100}); } await new Promise((resolve) => server.close(resolve)); } From b25adf1d583d68a0539dc0b5790016fb018a7357 Mon Sep 17 00:00:00 2001 From: swd Date: Wed, 6 May 2026 00:51:35 +0900 Subject: [PATCH 14/16] Extend for WebGPU (#5) Extend for WebGPU --- .gitignore | 4 +- CMakeLists.txt | 87 ++++++- docs/WebGPU.md | 21 +- scripts/fetch_dawn.py | 11 +- src/CMakeLists.txt | 37 ++- src/DX12/LLGI.CommandListDX12.cpp | 4 +- src/DX12/LLGI.CompilerDX12.cpp | 8 +- src/DX12/LLGI.GraphicsDX12.cpp | 11 +- src/DX12/LLGI.PlatformDX12.cpp | 10 +- src/DX12/LLGI.RenderPassDX12.cpp | 4 +- src/DX12/LLGI.TextureDX12.cpp | 18 +- src/LLGI.Graphics.h | 2 + src/LLGI.PipelineState.h | 1 + src/Metal/LLGI.PipelineStateMetal.mm | 3 +- src/Vulkan/LLGI.PipelineStateVulkan.cpp | 2 +- src/WebGPU/LLGI.BaseWebGPU.cpp | 16 +- src/WebGPU/LLGI.CommandListWebGPU.cpp | 105 +++++++- src/WebGPU/LLGI.CommandListWebGPU.h | 4 +- src/WebGPU/LLGI.GraphicsWebGPU.h | 2 + src/WebGPU/LLGI.PipelineStateWebGPU.cpp | 185 +++++++++++++- src/WebGPU/LLGI.PipelineStateWebGPU.h | 9 + src/WebGPU/LLGI.PlatformWebGPU.cpp | 7 +- src/WebGPU/LLGI.ShaderWebGPU.cpp | 234 ++++++++++++++++++ src/WebGPU/LLGI.ShaderWebGPU.h | 34 ++- src/WebGPU/LLGI.TextureWebGPU.cpp | 96 ++++++- src/WebGPU/LLGI.TextureWebGPU.h | 1 + tools/CMakeLists.txt | 4 +- tools/ShaderTranspiler/main.cpp | 8 +- tools/ShaderTranspilerCore/CMakeLists.txt | 21 +- .../ShaderTranspilerCore.cpp | 27 +- .../ShaderTranspilerCore.h | 4 +- 31 files changed, 894 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index 0e81970f..ba917607 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,8 @@ /msvc/*.user **/Debug /build -/build-webgpu-browser/ -/build_clangformat +/build-* +/build_* /thirdparty/dawn/ /node_modules/ diff --git a/CMakeLists.txt b/CMakeLists.txt index edb90f5d..80ccd7e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,7 @@ if(MSVC) cmake_policy(GET CMP0091 CMP0091_STATE) - if(CMP0091_STATE EQUAL NEW) + if(CMP0091_STATE STREQUAL "NEW") if(USE_MSVC_RUNTIME_LIBRARY_DLL) cmake_policy(SET CMP0091 NEW) @@ -111,6 +111,8 @@ if(BUILD_VULKAN_COMPILER OR BUILD_TOOL) ${CMAKE_CURRENT_BINARY_DIR}/EP/Install/glslang/include) list(APPEND LLGI_THIRDPARTY_LIBRARY_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}/EP/Install/glslang/lib) + set(LLGI_GLSLANG_LIBRARY_DIR + ${CMAKE_CURRENT_BINARY_DIR}/EP/Install/glslang/lib) if ("${CMAKE_BUILD_TYPE}" STREQUAL "" AND NOT WIN32) ExternalProject_Add( @@ -121,6 +123,7 @@ if(BUILD_VULKAN_COMPILER OR BUILD_TOOL) CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}/EP/Install/glslang -DCMAKE_USER_MAKE_RULES_OVERRIDE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/CFlagOverrides.cmake + -DCMAKE_MSVC_RUNTIME_LIBRARY=${CMAKE_MSVC_RUNTIME_LIBRARY} -DUSE_MSVC_RUNTIME_LIBRARY_DLL=${USE_MSVC_RUNTIME_LIBRARY_DLL} -DENABLE_OPT=OFF -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} @@ -147,6 +150,7 @@ if(BUILD_VULKAN_COMPILER OR BUILD_TOOL) CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}/EP/Install/glslang -DCMAKE_USER_MAKE_RULES_OVERRIDE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/CFlagOverrides.cmake + -DCMAKE_MSVC_RUNTIME_LIBRARY=${CMAKE_MSVC_RUNTIME_LIBRARY} -DUSE_MSVC_RUNTIME_LIBRARY_DLL=${USE_MSVC_RUNTIME_LIBRARY_DLL} -DENABLE_OPT=OFF -DCMAKE_DEBUG_POSTFIX=d @@ -190,6 +194,7 @@ if(BUILD_VULKAN_COMPILER OR BUILD_TOOL) CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}/EP/Install/SPIRV-Cross -DCMAKE_USER_MAKE_RULES_OVERRIDE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/CFlagOverrides.cmake + -DCMAKE_MSVC_RUNTIME_LIBRARY=${CMAKE_MSVC_RUNTIME_LIBRARY} -DUSE_MSVC_RUNTIME_LIBRARY_DLL=${USE_MSVC_RUNTIME_LIBRARY_DLL} -DCMAKE_DEBUG_POSTFIX=d -DBUILD_SHARED_LIBS=OFF @@ -203,6 +208,8 @@ if(BUILD_VULKAN_COMPILER OR BUILD_TOOL) ${CMAKE_CURRENT_BINARY_DIR}/EP/Install/SPIRV-Cross/include) list(APPEND LLGI_THIRDPARTY_LIBRARY_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}/EP/Install/SPIRV-Cross/lib) + set(LLGI_SPIRV_CROSS_LIBRARY_DIR + ${CMAKE_CURRENT_BINARY_DIR}/EP/Install/SPIRV-Cross/lib) list( APPEND @@ -240,6 +247,66 @@ if(BUILD_VULKAN_COMPILER OR BUILD_TOOL) optimized spirv-cross-util) + if(WIN32) + set(LLGI_THIRDPARTY_LIBRARY_FILES + debug + ${LLGI_GLSLANG_LIBRARY_DIR}/glslangd.lib + debug + ${LLGI_GLSLANG_LIBRARY_DIR}/glslang-default-resource-limitsd.lib + debug + ${LLGI_GLSLANG_LIBRARY_DIR}/MachineIndependentd.lib + debug + ${LLGI_GLSLANG_LIBRARY_DIR}/SPIRVd.lib + debug + ${LLGI_GLSLANG_LIBRARY_DIR}/OSDependentd.lib + debug + ${LLGI_GLSLANG_LIBRARY_DIR}/GenericCodeGend.lib + optimized + ${LLGI_GLSLANG_LIBRARY_DIR}/glslang.lib + optimized + ${LLGI_GLSLANG_LIBRARY_DIR}/glslang-default-resource-limits.lib + optimized + ${LLGI_GLSLANG_LIBRARY_DIR}/MachineIndependent.lib + optimized + ${LLGI_GLSLANG_LIBRARY_DIR}/SPIRV.lib + optimized + ${LLGI_GLSLANG_LIBRARY_DIR}/OSDependent.lib + optimized + ${LLGI_GLSLANG_LIBRARY_DIR}/GenericCodeGen.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-cored.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-cd.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-cppd.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-hlsld.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-glsld.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-msld.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-reflectd.lib + debug + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-utild.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-core.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-c.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-cpp.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-hlsl.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-glsl.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-msl.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-reflect.lib + optimized + ${LLGI_SPIRV_CROSS_LIBRARY_DIR}/spirv-cross-util.lib) + endif() + else() list(APPEND LLGI_THIRDPARTY_INCLUDES ${GLSLANG_INCLUDE_DIR} ${SPIRVCROSS_INCLUDE_DIR}) @@ -250,7 +317,9 @@ endif() if(BUILD_VULKAN) add_compile_definitions(ENABLE_VULKAN) +endif() +if(BUILD_VULKAN OR BUILD_WEBGPU) if(GLSLANG_WITHOUT_INSTALL) add_compile_definitions(ENABLE_GLSLANG_WITHOUT_INSTALL) endif() @@ -273,7 +342,17 @@ if(BUILD_WEBGPU AND NOT EMSCRIPTEN) CACHE BOOL "Allow Dawn to load Windows system components from System32" FORCE) set(DAWN_BUILD_TESTS OFF CACHE BOOL "Build Dawn tests" FORCE) - set(DAWN_ENABLE_INSTALL OFF CACHE BOOL "Install Dawn targets" FORCE) + set(DAWN_BUILD_PROTOBUF OFF CACHE BOOL "Build Dawn protobuf dependencies" FORCE) + set(TINT_BUILD_TESTS OFF CACHE BOOL "Build Tint tests" FORCE) + set(TINT_BUILD_IR_BINARY OFF CACHE BOOL "Build Tint IR binary support" FORCE) + set(DAWN_ENABLE_INSTALL ON CACHE BOOL "Install Dawn targets" FORCE) + if(USE_MSVC_RUNTIME_LIBRARY_DLL) + set(ABSL_MSVC_STATIC_RUNTIME OFF + CACHE BOOL "Use the static MSVC runtime for Abseil" FORCE) + else() + set(ABSL_MSVC_STATIC_RUNTIME ON + CACHE BOOL "Use the static MSVC runtime for Abseil" FORCE) + endif() set(LLGI_DAWN_SOURCE_DIR "") if(WEBGPU_DAWN_SOURCE_DIR) @@ -299,6 +378,10 @@ endif() add_subdirectory("src") +if(BUILD_WEBGPU AND NOT TARGET ShaderTranspilerCore) + add_subdirectory("tools/ShaderTranspilerCore") +endif() + if(BUILD_TEST OR BUILD_WEBGPU_BROWSER_TEST) add_subdirectory("src_test") endif() diff --git a/docs/WebGPU.md b/docs/WebGPU.md index 33180cad..322a26f9 100644 --- a/docs/WebGPU.md +++ b/docs/WebGPU.md @@ -34,6 +34,16 @@ into `thirdparty/dawn` and runs Dawn's dependency fetch script: python scripts/fetch_dawn.py ``` +By default, the helper passes `--no-shallow` to Dawn's dependency fetch script. +This is slower, but avoids partially-created dependency checkouts on Git +installations that do not support Dawn's shallow fetch flow well. Use shallow +dependency fetches only when the local Git/depot_tools setup is known to handle +them: + +```bash +python scripts/fetch_dawn.py --shallow-dependencies +``` + Pin a Dawn revision for reproducible builds: ```bash @@ -50,6 +60,10 @@ cmake -S . -B build-webgpu \ cmake --build build-webgpu --config Release ``` +When `BUILD_WEBGPU=ON`, LLGI enables Dawn's CMake install/export metadata so +parent projects can generate their own install exports without disabling install +rules. + ### Existing Dawn Checkout You can use a separate Dawn checkout if its dependencies have already been @@ -143,9 +157,10 @@ rewrites are not applied. - If configure fails because Dawn is missing, run `python scripts/fetch_dawn.py`, add `thirdparty/dawn`, or set `WEBGPU_DAWN_SOURCE_DIR`. -- If Dawn dependency sync fails, install `depot_tools` and ensure `gclient` is - available on `PATH`, or use a Dawn checkout whose dependencies are already - synced. +- If Dawn dependency sync fails, rerun `python scripts/fetch_dawn.py` so the + helper uses Dawn's `--no-shallow` dependency mode. Install `depot_tools` and + ensure `gclient` is available on `PATH` if you use Dawn's official `gclient` + flow instead. - If WebGPU device creation fails on Windows with `d3dcompiler_47.dll`, rebuild with `WEBGPU_DAWN_FORCE_SYSTEM_COMPONENT_LOAD=ON`. - If shader creation fails, regenerate WGSL with the current `ShaderTranspiler` diff --git a/scripts/fetch_dawn.py b/scripts/fetch_dawn.py index 203953cd..8f22487c 100644 --- a/scripts/fetch_dawn.py +++ b/scripts/fetch_dawn.py @@ -42,6 +42,11 @@ def main(): action="store_true", help="Only clone/update Dawn; do not run Dawn dependency fetch.", ) + parser.add_argument( + "--shallow-dependencies", + action="store_true", + help="Allow Dawn's dependency fetch script to use shallow clones. By default, dependencies are fetched with --no-shallow for broader Git compatibility.", + ) args = parser.parse_args() dawn_dir = os.path.abspath(args.directory) @@ -70,7 +75,11 @@ def main(): file=sys.stderr, ) return 1 - run([sys.executable, dependency_script], cwd=dawn_dir) + + dependency_args = [sys.executable, dependency_script] + if not args.shallow_dependencies: + dependency_args.append("--no-shallow") + run(dependency_args, cwd=dawn_dir) print(f"Dawn is ready: {dawn_dir}") return 0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 84703c13..60687065 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,7 +18,7 @@ else() list(APPEND files ${files_linux}) endif() -if(MSVC AND NOT BUILD_WEBGPU) +if(MSVC) file(GLOB files_dx12 DX12/*.h DX12/*.cpp) list(APPEND files ${files_dx12}) endif() @@ -98,9 +98,9 @@ if(BUILD_WEBGPU) target_compile_options(LLGI PUBLIC --use-port=emdawnwebgpu) target_link_options(LLGI PUBLIC --use-port=emdawnwebgpu) elseif(TARGET dawn::webgpu_dawn) - target_link_libraries(LLGI PRIVATE dawn::webgpu_dawn) + target_link_libraries(LLGI PUBLIC dawn::webgpu_dawn) elseif(TARGET webgpu_dawn) - target_link_libraries(LLGI PRIVATE webgpu_dawn) + target_link_libraries(LLGI PUBLIC webgpu_dawn) else() message(FATAL_ERROR "Dawn targets were not found. Expected dawn::webgpu_dawn or webgpu_dawn.") endif() @@ -123,22 +123,19 @@ endif() # -------------------- # Install -# Because dawn doesn't specify export -if(NOT BUILD_WEBGPU) - install( - TARGETS LLGI - EXPORT LLGI-export - INCLUDES - DESTINATION include/LLGI - PUBLIC_HEADER DESTINATION include/LLGI - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib) - - install( - EXPORT LLGI-export - FILE LLGI-config.cmake - DESTINATION lib/cmake - EXPORT_LINK_INTERFACE_LIBRARIES) -endif() +install( + TARGETS LLGI + EXPORT LLGI-export + INCLUDES + DESTINATION include/LLGI + PUBLIC_HEADER DESTINATION include/LLGI + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib) + +install( + EXPORT LLGI-export + FILE LLGI-config.cmake + DESTINATION lib/cmake + EXPORT_LINK_INTERFACE_LIBRARIES) clang_format(LLGI) diff --git a/src/DX12/LLGI.CommandListDX12.cpp b/src/DX12/LLGI.CommandListDX12.cpp index f5243823..f374507c 100644 --- a/src/DX12/LLGI.CommandListDX12.cpp +++ b/src/DX12/LLGI.CommandListDX12.cpp @@ -153,12 +153,13 @@ CommandListDX12::~CommandListDX12() bool CommandListDX12::Initialize(GraphicsDX12* graphics, int32_t drawingCount) { HRESULT hr; + ID3D12CommandAllocator* commandAllocator = nullptr; + ID3D12GraphicsCommandList* commandList = nullptr; SafeAddRef(graphics); graphics_ = CreateSharedPtr(graphics); // Command Allocator - ID3D12CommandAllocator* commandAllocator = nullptr; hr = graphics_->GetDevice()->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); if (FAILED(hr)) { @@ -171,7 +172,6 @@ bool CommandListDX12::Initialize(GraphicsDX12* graphics, int32_t drawingCount) commandAllocator_ = CreateSharedPtr(commandAllocator); // Command List - ID3D12GraphicsCommandList* commandList = nullptr; hr = graphics_->GetDevice()->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator, NULL, IID_PPV_ARGS(&commandList)); if (FAILED(hr)) { diff --git a/src/DX12/LLGI.CompilerDX12.cpp b/src/DX12/LLGI.CompilerDX12.cpp index bccb43f0..efa617d2 100644 --- a/src/DX12/LLGI.CompilerDX12.cpp +++ b/src/DX12/LLGI.CompilerDX12.cpp @@ -75,10 +75,10 @@ void CompilerDX12::Initialize() {} void CompilerDX12::Compile(CompilerResult& result, const char* code, ShaderStageType shaderStage) { - char* vs_target = "vs_5_0"; - char* ps_target = "ps_5_0"; - char* cs_target = "cs_5_0"; - char* target = nullptr; + const char* vs_target = "vs_5_0"; + const char* ps_target = "ps_5_0"; + const char* cs_target = "cs_5_0"; + const char* target = nullptr; if (shaderStage == ShaderStageType::Vertex) { diff --git a/src/DX12/LLGI.GraphicsDX12.cpp b/src/DX12/LLGI.GraphicsDX12.cpp index 3a9a0fd0..d41f85ca 100644 --- a/src/DX12/LLGI.GraphicsDX12.cpp +++ b/src/DX12/LLGI.GraphicsDX12.cpp @@ -312,9 +312,12 @@ std::vector GraphicsDX12::CaptureRenderTarget(Texture* renderTarget) ID3D12CommandAllocator* commandAllocator = nullptr; ID3D12GraphicsCommandList* commandList = nullptr; + ID3D12CommandList* commandLists[] = {nullptr}; D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{}; UINT64 totalSize{}; D3D12_RESOURCE_DESC textureDesc{}; + HRESULT hr = S_OK; + void* locked = nullptr; BufferDX12 dstBuffer; if (!dstBuffer.Initialize(this, BufferUsageType::CopyDst | BufferUsageType::MapRead, dstFootprint.RowPitch * dstFootprint.Height)) @@ -324,7 +327,7 @@ std::vector GraphicsDX12::CaptureRenderTarget(Texture* renderTarget) goto FAILED_EXIT; } - auto hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); + hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); if (FAILED(hr)) { auto msg = (std::string("Error : ") + std::string(__FILE__) + " : " + std::to_string(__LINE__) + std::string(" : ") + @@ -359,15 +362,15 @@ std::vector GraphicsDX12::CaptureRenderTarget(Texture* renderTarget) texture->ResourceBarrier(commandList, D3D12_RESOURCE_STATE_GENERIC_READ); commandList->Close(); - ID3D12CommandList* list[] = {commandList}; - GetCommandQueue()->ExecuteCommandLists(1, list); + commandLists[0] = commandList; + GetCommandQueue()->ExecuteCommandLists(1, commandLists); // TODO optimize it WaitFinish(); SafeRelease(commandList); SafeRelease(commandAllocator); - auto locked = dstBuffer.Lock(); + locked = dstBuffer.Lock(); if (locked) { diff --git a/src/DX12/LLGI.PlatformDX12.cpp b/src/DX12/LLGI.PlatformDX12.cpp index 92348d54..2ef0a3d1 100644 --- a/src/DX12/LLGI.PlatformDX12.cpp +++ b/src/DX12/LLGI.PlatformDX12.cpp @@ -140,6 +140,7 @@ bool PlatformDX12::GenerateSwapBuffer() DXGISwapChainDesc.SampleDesc.Quality = 0; IDXGISwapChain* swapChain_ = nullptr; + UINT descriptorHandleIncrementSize = 0; auto hr = dxgiFactory->CreateSwapChain(commandQueue, &DXGISwapChainDesc, &swapChain_); if (FAILED(hr)) { @@ -176,7 +177,7 @@ bool PlatformDX12::GenerateSwapBuffer() goto FAILED_EXIT; } - auto descriptorHandleIncrementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + descriptorHandleIncrementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); for (int32_t i = 0; i < SwapBufferCount; ++i) { @@ -221,6 +222,10 @@ bool PlatformDX12::Initialize(Window* window, bool waitVSync) // DirectX12 HRESULT hr; + IDXGIAdapter1* adapter = nullptr; +#if defined(_DEBUG) + ID3D12InfoQueue* infoQueue = nullptr; +#endif UINT flagsDXGI = 0; @@ -269,8 +274,6 @@ bool PlatformDX12::Initialize(Window* window, bool waitVSync) // device - IDXGIAdapter1* adapter = nullptr; - for (UINT adapterIndex = 0; dxgiFactory->EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND; adapterIndex++) { DXGI_ADAPTER_DESC1 desc; @@ -299,7 +302,6 @@ bool PlatformDX12::Initialize(Window* window, bool waitVSync) } #if defined(_DEBUG) - ID3D12InfoQueue* infoQueue = nullptr; if (SUCCEEDED(device->QueryInterface(&infoQueue))) { D3D12_MESSAGE_SEVERITY severities[] = {D3D12_MESSAGE_SEVERITY_INFO}; diff --git a/src/DX12/LLGI.RenderPassDX12.cpp b/src/DX12/LLGI.RenderPassDX12.cpp index c5f53e3b..a3faa864 100644 --- a/src/DX12/LLGI.RenderPassDX12.cpp +++ b/src/DX12/LLGI.RenderPassDX12.cpp @@ -88,14 +88,14 @@ bool RenderPassDX12::ReinitializeRenderTargetViews(CommandListDX12* commandList, if (!rtDescriptorHeap->Allocate(heapRTV, cpuDescriptorHandleRTV, gpuDescriptorHandleRTV, numRenderTarget_)) { - return nullptr; + return false; } if (GetHasDepthTexture()) { if (!dtDescriptorHeap->Allocate(heapDSV, cpuDescriptorHandleDSV, gpuDescriptorHandleDSV, 1)) { - return nullptr; + return false; } } diff --git a/src/DX12/LLGI.TextureDX12.cpp b/src/DX12/LLGI.TextureDX12.cpp index 7f210635..942cf1e1 100644 --- a/src/DX12/LLGI.TextureDX12.cpp +++ b/src/DX12/LLGI.TextureDX12.cpp @@ -240,6 +240,8 @@ void TextureDX12::Unlock() ID3D12CommandAllocator* commandAllocator = nullptr; ID3D12GraphicsCommandList* commandList = nullptr; ID3D12Fence* fence = nullptr; + ID3D12CommandList* commandLists[] = {nullptr}; + HRESULT hr = S_OK; D3D12_TEXTURE_COPY_LOCATION src = {}, dst = {}; @@ -252,7 +254,7 @@ void TextureDX12::Unlock() goto FAILED_EXIT; } - auto hr = device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); + hr = device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); if (FAILED(hr)) { SHOW_DX12_ERROR(hr, device_); @@ -288,8 +290,8 @@ void TextureDX12::Unlock() ResourceBarrier(commandList, D3D12_RESOURCE_STATE_GENERIC_READ); commandList->Close(); - ID3D12CommandList* list[] = {commandList}; - commandQueue_->ExecuteCommandLists(1, list); + commandLists[0] = commandList; + commandQueue_->ExecuteCommandLists(1, commandLists); // TODO optimize it hr = commandQueue_->Signal(fence, 1); @@ -313,6 +315,9 @@ bool TextureDX12::GetData(std::vector& data) ID3D12CommandAllocator* commandAllocator = nullptr; ID3D12GraphicsCommandList* commandList = nullptr; ID3D12Fence* fence = nullptr; + ID3D12CommandList* commandLists[] = {nullptr}; + HRESULT hr = S_OK; + uint8_t* ptr = nullptr; D3D12_TEXTURE_COPY_LOCATION src = {}, dst = {}; @@ -325,7 +330,7 @@ bool TextureDX12::GetData(std::vector& data) goto FAILED_EXIT; } - auto hr = device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); + hr = device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)); if (FAILED(hr)) { SHOW_DX12_ERROR(hr, device_); @@ -361,15 +366,14 @@ bool TextureDX12::GetData(std::vector& data) ResourceBarrier(commandList, D3D12_RESOURCE_STATE_GENERIC_READ); commandList->Close(); - ID3D12CommandList* list[] = {commandList}; - commandQueue_->ExecuteCommandLists(1, list); + commandLists[0] = commandList; + commandQueue_->ExecuteCommandLists(1, commandLists); // TODO optimize it hr = commandQueue_->Signal(fence, 1); fence->SetEventOnCompletion(1, event); WaitForSingleObject(event, INFINITE); - uint8_t* ptr = nullptr; buffer_for_readback_->Map(0, nullptr, (void**)&ptr); if (ptr != nullptr) { diff --git a/src/LLGI.Graphics.h b/src/LLGI.Graphics.h index f25e8d6d..abb4317a 100644 --- a/src/LLGI.Graphics.h +++ b/src/LLGI.Graphics.h @@ -238,6 +238,8 @@ class Graphics : public ReferenceObject */ virtual void WaitFinish() {} + virtual bool GetIsMipmapGenerationSupportedOnTextureLoad() const { return false; } + virtual Buffer* CreateBuffer(BufferUsageType usage, int32_t size); virtual Shader* CreateShader(DataStructure* data, int32_t count); diff --git a/src/LLGI.PipelineState.h b/src/LLGI.PipelineState.h index ca059a19..aa30f123 100644 --- a/src/LLGI.PipelineState.h +++ b/src/LLGI.PipelineState.h @@ -50,6 +50,7 @@ class PipelineState : public ReferenceObject //! only for DirectX12 std::array VertexLayoutSemantics; int32_t VertexLayoutCount = 0; + int32_t VertexBufferStride = 0; virtual void SetShader(ShaderStageType stage, Shader* shader); diff --git a/src/Metal/LLGI.PipelineStateMetal.mm b/src/Metal/LLGI.PipelineStateMetal.mm index 1109991b..810ec712 100644 --- a/src/Metal/LLGI.PipelineStateMetal.mm +++ b/src/Metal/LLGI.PipelineStateMetal.mm @@ -92,7 +92,8 @@ vertexDescriptor.layouts[VertexBufferIndex].stepRate = 1; vertexDescriptor.layouts[VertexBufferIndex].stepFunction = MTLVertexStepFunctionPerVertex; - vertexDescriptor.layouts[VertexBufferIndex].stride = vertexOffset; + vertexDescriptor.layouts[VertexBufferIndex].stride = + pipstate->VertexBufferStride > 0 ? pipstate->VertexBufferStride : vertexOffset; pipelineStateDescriptor_.vertexDescriptor = vertexDescriptor; diff --git a/src/Vulkan/LLGI.PipelineStateVulkan.cpp b/src/Vulkan/LLGI.PipelineStateVulkan.cpp index 7634a274..1ce59beb 100644 --- a/src/Vulkan/LLGI.PipelineStateVulkan.cpp +++ b/src/Vulkan/LLGI.PipelineStateVulkan.cpp @@ -184,7 +184,7 @@ bool PipelineStateVulkan::CreateGraphicsPipeline() vk::VertexInputBindingDescription bindDesc; bindDesc.binding = 0; - bindDesc.stride = vertexOffset; + bindDesc.stride = VertexBufferStride > 0 ? VertexBufferStride : vertexOffset; bindDesc.inputRate = vk::VertexInputRate::eVertex; bindDescs.push_back(bindDesc); diff --git a/src/WebGPU/LLGI.BaseWebGPU.cpp b/src/WebGPU/LLGI.BaseWebGPU.cpp index 2e9a3417..00fcdb68 100644 --- a/src/WebGPU/LLGI.BaseWebGPU.cpp +++ b/src/WebGPU/LLGI.BaseWebGPU.cpp @@ -143,8 +143,8 @@ wgpu::VertexFormat Convert(VertexLayoutFormat format) if (format == VertexLayoutFormat::R32G32B32_FLOAT) return wgpu::VertexFormat::Float32x3; - if (format == VertexLayoutFormat::R32G32B32_FLOAT) - return wgpu::VertexFormat::Float32x3; + if (format == VertexLayoutFormat::R32G32B32A32_FLOAT) + return wgpu::VertexFormat::Float32x4; if (format == VertexLayoutFormat::R8G8B8A8_UNORM) return wgpu::VertexFormat::Unorm8x4; @@ -219,6 +219,9 @@ wgpu::TextureFormat ConvertFormat(TextureFormatType format) if (format == TextureFormatType::BC3) return wgpu::TextureFormat::BC3RGBAUnorm; + if (format == TextureFormatType::BC7) + return wgpu::TextureFormat::BC7RGBAUnorm; + if (format == TextureFormatType::BC1_SRGB) return wgpu::TextureFormat::BC1RGBAUnormSrgb; @@ -228,6 +231,9 @@ wgpu::TextureFormat ConvertFormat(TextureFormatType format) if (format == TextureFormatType::BC3_SRGB) return wgpu::TextureFormat::BC3RGBAUnormSrgb; + if (format == TextureFormatType::BC7_SRGB) + return wgpu::TextureFormat::BC7RGBAUnormSrgb; + if (format == TextureFormatType::D32) return wgpu::TextureFormat::Depth32Float; @@ -278,6 +284,9 @@ TextureFormatType ConvertFormat(wgpu::TextureFormat format) if (format == wgpu::TextureFormat::BC3RGBAUnorm) return TextureFormatType::BC3; + if (format == wgpu::TextureFormat::BC7RGBAUnorm) + return TextureFormatType::BC7; + if (format == wgpu::TextureFormat::BC1RGBAUnormSrgb) return TextureFormatType::BC1_SRGB; @@ -287,6 +296,9 @@ TextureFormatType ConvertFormat(wgpu::TextureFormat format) if (format == wgpu::TextureFormat::BC3RGBAUnormSrgb) return TextureFormatType::BC3_SRGB; + if (format == wgpu::TextureFormat::BC7RGBAUnormSrgb) + return TextureFormatType::BC7_SRGB; + if (format == wgpu::TextureFormat::Depth32Float) return TextureFormatType::D32; diff --git a/src/WebGPU/LLGI.CommandListWebGPU.cpp b/src/WebGPU/LLGI.CommandListWebGPU.cpp index 76930975..ef95da36 100644 --- a/src/WebGPU/LLGI.CommandListWebGPU.cpp +++ b/src/WebGPU/LLGI.CommandListWebGPU.cpp @@ -8,10 +8,50 @@ namespace LLGI { +namespace +{ +ShaderBindingResourceTypeWebGPU GetTextureBindingResourceType(TextureWebGPU* texture) +{ + if (texture != nullptr && BitwiseContains(texture->GetParameter().Usage, TextureUsageType::Storage)) + { + return ShaderBindingResourceTypeWebGPU::StorageTexture; + } + return ShaderBindingResourceTypeWebGPU::Texture; +} + +bool NeedsTextureSampler(TextureWebGPU* texture) +{ + return texture == nullptr || !BitwiseContains(texture->GetParameter().Usage, TextureUsageType::Storage); +} +} // namespace CommandListWebGPU::CommandListWebGPU(wgpu::Device device) : device_(device) { - for (int w = 0; w < 2; w++) + wgpu::TextureDescriptor fallbackTextureDesc{}; + fallbackTextureDesc.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst; + fallbackTextureDesc.dimension = wgpu::TextureDimension::e2D; + fallbackTextureDesc.format = wgpu::TextureFormat::RGBA8Unorm; + fallbackTextureDesc.mipLevelCount = 1; + fallbackTextureDesc.sampleCount = 1; + fallbackTextureDesc.size.width = 1; + fallbackTextureDesc.size.height = 1; + fallbackTextureDesc.size.depthOrArrayLayers = 1; + fallbackTexture_ = device_.CreateTexture(&fallbackTextureDesc); + fallbackTextureView_ = fallbackTexture_.CreateView(); + + const uint8_t fallbackTexel[4] = {255, 255, 255, 255}; + wgpu::TexelCopyTextureInfo fallbackDst{}; + fallbackDst.texture = fallbackTexture_; + fallbackDst.aspect = wgpu::TextureAspect::All; + wgpu::TexelCopyBufferLayout fallbackLayout{}; + fallbackLayout.bytesPerRow = 4; + wgpu::Extent3D fallbackExtent{}; + fallbackExtent.width = 1; + fallbackExtent.height = 1; + fallbackExtent.depthOrArrayLayers = 1; + device_.GetQueue().WriteTexture(&fallbackDst, fallbackTexel, sizeof(fallbackTexel), &fallbackLayout, &fallbackExtent); + + for (int w = 0; w < 3; w++) { for (int f = 0; f < 2; f++) { @@ -19,14 +59,19 @@ CommandListWebGPU::CommandListWebGPU(wgpu::Device device) : device_(device) filters[0] = wgpu::FilterMode::Nearest; filters[1] = wgpu::FilterMode::Linear; - std::array am; + std::array am; am[0] = wgpu::AddressMode::ClampToEdge; am[1] = wgpu::AddressMode::Repeat; + am[2] = wgpu::AddressMode::MirrorRepeat; wgpu::SamplerDescriptor samplerDesc; samplerDesc.magFilter = filters[f]; samplerDesc.minFilter = filters[f]; + // LLGI exposes min/mag filtering only, so keep mip selection point-filtered. + samplerDesc.mipmapFilter = wgpu::MipmapFilterMode::Nearest; + samplerDesc.lodMinClamp = 0.0f; + samplerDesc.lodMaxClamp = 32.0f; samplerDesc.maxAnisotropy = 1; samplerDesc.addressModeU = am[w]; samplerDesc.addressModeV = am[w]; @@ -54,6 +99,8 @@ void CommandListWebGPU::End() void CommandListWebGPU::BeginRenderPass(RenderPass* renderPass) { + EndComputePass(); + auto rp = static_cast(renderPass); rp->RefreshDescriptor(); const auto& desc = rp->GetDescriptor(); @@ -76,6 +123,11 @@ void CommandListWebGPU::EndRenderPass() void CommandListWebGPU::BeginComputePass() { + if (computePassEncorder_ != nullptr) + { + return; + } + wgpu::ComputePassDescriptor desc{}; computePassEncorder_ = commandEncorder_.BeginComputePass(&desc); } @@ -137,6 +189,10 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) { continue; } + if (!pip->HasBinding(0, static_cast(unit_ind), ShaderBindingResourceTypeWebGPU::UniformBuffer)) + { + continue; + } wgpu::BindGroupEntry entry = {}; entry.binding = static_cast(unit_ind); @@ -161,20 +217,25 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) for (int unit_ind = 0; unit_ind < static_cast(currentTextures_.size()); unit_ind++) { - if (currentTextures_[unit_ind].texture == nullptr) - continue; auto texture = static_cast(currentTextures_[unit_ind].texture); + const auto resourceType = GetTextureBindingResourceType(texture); + if (!pip->HasBinding(1, static_cast(unit_ind), resourceType)) + continue; auto wm = (int32_t)currentTextures_[unit_ind].wrapMode; auto mm = (int32_t)currentTextures_[unit_ind].minMagFilter; wgpu::BindGroupEntry textureEntry = {}; textureEntry.binding = unit_ind; - textureEntry.textureView = texture->GetTextureView(); + textureEntry.textureView = texture != nullptr ? texture->GetTextureView() : fallbackTextureView_; textureGroupEntries.push_back(textureEntry); wgpu::BindGroupEntry samplerEntry = {}; - if (!BitwiseContains(texture->GetParameter().Usage, TextureUsageType::Storage)) + if (NeedsTextureSampler(texture)) { + if (!pip->HasBinding(2, static_cast(unit_ind), ShaderBindingResourceTypeWebGPU::Sampler)) + { + continue; + } samplerEntry.binding = unit_ind; samplerEntry.sampler = samplers_[wm][mm]; samplerGroupEntries.push_back(samplerEntry); @@ -187,6 +248,10 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) { continue; } + if (!pip->HasBinding(1, static_cast(unit_ind), ShaderBindingResourceTypeWebGPU::StorageBuffer)) + { + continue; + } auto buffer = static_cast(computeBuffers_[unit_ind].computeBuffer); wgpu::BindGroupEntry bufferEntry = {}; @@ -246,11 +311,16 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, bool isPipDirtied = false; GetCurrentPipelineState(bpip, isPipDirtied); auto pip = static_cast(bpip); - if (pip == nullptr || computePassEncorder_ == nullptr) + if (pip == nullptr) { return; } + if (computePassEncorder_ == nullptr) + { + BeginComputePass(); + } + computePassEncorder_.SetPipeline(pip->GetComputePipeline()); std::vector constantBindGroupEntries; @@ -261,6 +331,10 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, { continue; } + if (!pip->HasBinding(0, static_cast(unit_ind), ShaderBindingResourceTypeWebGPU::UniformBuffer)) + { + continue; + } wgpu::BindGroupEntry entry{}; entry.binding = static_cast(unit_ind); @@ -285,22 +359,27 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, for (int unit_ind = 0; unit_ind < static_cast(currentTextures_.size()); unit_ind++) { - if (currentTextures_[unit_ind].texture == nullptr) + auto texture = static_cast(currentTextures_[unit_ind].texture); + const auto resourceType = GetTextureBindingResourceType(texture); + if (!pip->HasBinding(1, static_cast(unit_ind), resourceType)) { continue; } - auto texture = static_cast(currentTextures_[unit_ind].texture); auto wm = (int32_t)currentTextures_[unit_ind].wrapMode; auto mm = (int32_t)currentTextures_[unit_ind].minMagFilter; wgpu::BindGroupEntry textureEntry{}; textureEntry.binding = unit_ind; - textureEntry.textureView = texture->GetTextureView(); + textureEntry.textureView = texture != nullptr ? texture->GetTextureView() : fallbackTextureView_; textureGroupEntries.push_back(textureEntry); - if (!BitwiseContains(texture->GetParameter().Usage, TextureUsageType::Storage)) + if (NeedsTextureSampler(texture)) { + if (!pip->HasBinding(2, static_cast(unit_ind), ShaderBindingResourceTypeWebGPU::Sampler)) + { + continue; + } wgpu::BindGroupEntry samplerEntry{}; samplerEntry.binding = unit_ind; samplerEntry.sampler = samplers_[wm][mm]; @@ -314,6 +393,10 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, { continue; } + if (!pip->HasBinding(2, static_cast(unit_ind), ShaderBindingResourceTypeWebGPU::StorageBuffer)) + { + continue; + } auto buffer = static_cast(computeBuffers_[unit_ind].computeBuffer); wgpu::BindGroupEntry entry{}; diff --git a/src/WebGPU/LLGI.CommandListWebGPU.h b/src/WebGPU/LLGI.CommandListWebGPU.h index 64280e27..d443e3cc 100644 --- a/src/WebGPU/LLGI.CommandListWebGPU.h +++ b/src/WebGPU/LLGI.CommandListWebGPU.h @@ -13,7 +13,9 @@ class CommandListWebGPU : public CommandList wgpu::CommandEncoder commandEncorder_; wgpu::RenderPassEncoder renderPassEncorder_; wgpu::ComputePassEncoder computePassEncorder_; - wgpu::Sampler samplers_[2][2]; + wgpu::Sampler samplers_[3][2]; + wgpu::Texture fallbackTexture_; + wgpu::TextureView fallbackTextureView_; public: CommandListWebGPU(wgpu::Device device); diff --git a/src/WebGPU/LLGI.GraphicsWebGPU.h b/src/WebGPU/LLGI.GraphicsWebGPU.h index f5c638e8..8f9b6403 100644 --- a/src/WebGPU/LLGI.GraphicsWebGPU.h +++ b/src/WebGPU/LLGI.GraphicsWebGPU.h @@ -56,6 +56,8 @@ class GraphicsWebGPU : public Graphics std::vector CaptureRenderTarget(Texture* renderTarget) override; + bool GetIsMipmapGenerationSupportedOnTextureLoad() const override { return true; } + RenderPassPipelineState* CreateRenderPassPipelineState(RenderPass* renderPass) override; RenderPassPipelineState* CreateRenderPassPipelineState(const RenderPassPipelineStateKey& key) override; diff --git a/src/WebGPU/LLGI.PipelineStateWebGPU.cpp b/src/WebGPU/LLGI.PipelineStateWebGPU.cpp index 7483f684..b7fe76df 100644 --- a/src/WebGPU/LLGI.PipelineStateWebGPU.cpp +++ b/src/WebGPU/LLGI.PipelineStateWebGPU.cpp @@ -1,10 +1,121 @@ #include "LLGI.PipelineStateWebGPU.h" #include "LLGI.RenderPassPipelineStateWebGPU.h" -#include "LLGI.ShaderWebGPU.h" +#include +#include #include namespace LLGI { +namespace +{ +bool BuildBindGroupLayoutEntry(const ShaderBindingWebGPU& binding, wgpu::ShaderStage visibility, wgpu::BindGroupLayoutEntry& entry) +{ + entry = {}; + entry.binding = binding.Binding; + entry.visibility = visibility; + + switch (binding.ResourceType) + { + case ShaderBindingResourceTypeWebGPU::UniformBuffer: + entry.buffer.type = wgpu::BufferBindingType::Uniform; + entry.buffer.hasDynamicOffset = false; + entry.buffer.minBindingSize = 0; + return true; + case ShaderBindingResourceTypeWebGPU::StorageBuffer: + entry.buffer.type = wgpu::BufferBindingType::Storage; + entry.buffer.hasDynamicOffset = false; + entry.buffer.minBindingSize = 0; + return true; + case ShaderBindingResourceTypeWebGPU::ReadOnlyStorageBuffer: + entry.buffer.type = wgpu::BufferBindingType::ReadOnlyStorage; + entry.buffer.hasDynamicOffset = false; + entry.buffer.minBindingSize = 0; + return true; + case ShaderBindingResourceTypeWebGPU::Texture: + entry.texture.sampleType = wgpu::TextureSampleType::Float; + entry.texture.viewDimension = binding.TextureViewDimension; + entry.texture.multisampled = false; + return true; + case ShaderBindingResourceTypeWebGPU::StorageTexture: + entry.storageTexture.access = + binding.StorageTextureAccess != wgpu::StorageTextureAccess::Undefined ? binding.StorageTextureAccess : wgpu::StorageTextureAccess::WriteOnly; + entry.storageTexture.format = + binding.StorageTextureFormat != wgpu::TextureFormat::Undefined ? binding.StorageTextureFormat : wgpu::TextureFormat::RGBA32Float; + entry.storageTexture.viewDimension = binding.TextureViewDimension; + return true; + case ShaderBindingResourceTypeWebGPU::Sampler: + entry.sampler.type = wgpu::SamplerBindingType::Filtering; + return true; + default: + return false; + } +} + +bool ContainsBinding(const std::vector& bindings, const ShaderBindingWebGPU& binding) +{ + for (const auto& existingBinding : bindings) + { + if (existingBinding.Group != binding.Group || existingBinding.Binding != binding.Binding || existingBinding.ResourceType != binding.ResourceType) + { + continue; + } + + if ((binding.ResourceType == ShaderBindingResourceTypeWebGPU::Texture || + binding.ResourceType == ShaderBindingResourceTypeWebGPU::StorageTexture) && + existingBinding.TextureViewDimension != binding.TextureViewDimension) + { + continue; + } + + if (binding.ResourceType == ShaderBindingResourceTypeWebGPU::StorageTexture && + (existingBinding.StorageTextureFormat != binding.StorageTextureFormat || + existingBinding.StorageTextureAccess != binding.StorageTextureAccess)) + { + continue; + } + + return true; + } + + return false; +} + +wgpu::PipelineLayout CreatePipelineLayout(wgpu::Device& device, + const std::vector& bindings, + wgpu::ShaderStage visibility, + std::array& bindGroupLayouts) +{ + std::array, 3> bindGroupLayoutEntries; + uint32_t bindGroupLayoutCount = 0; + for (const auto& binding : bindings) + { + if (binding.Group >= bindGroupLayoutEntries.size()) + { + continue; + } + + wgpu::BindGroupLayoutEntry entry{}; + if (BuildBindGroupLayoutEntry(binding, visibility, entry)) + { + bindGroupLayoutEntries[binding.Group].push_back(entry); + bindGroupLayoutCount = std::max(bindGroupLayoutCount, binding.Group + 1); + } + } + + for (uint32_t i = 0; i < bindGroupLayoutCount; i++) + { + wgpu::BindGroupLayoutDescriptor bindGroupLayoutDesc{}; + bindGroupLayoutDesc.entryCount = static_cast(bindGroupLayoutEntries[i].size()); + bindGroupLayoutDesc.entries = bindGroupLayoutEntries[i].data(); + bindGroupLayouts[i] = device.CreateBindGroupLayout(&bindGroupLayoutDesc); + } + + wgpu::PipelineLayoutDescriptor pipelineLayoutDesc{}; + pipelineLayoutDesc.bindGroupLayoutCount = bindGroupLayoutCount; + pipelineLayoutDesc.bindGroupLayouts = bindGroupLayouts.data(); + return device.CreatePipelineLayout(&pipelineLayoutDesc); +} +} // namespace PipelineStateWebGPU::PipelineStateWebGPU(wgpu::Device device) : device_(device) { shaders_.fill(nullptr); } @@ -23,14 +134,79 @@ void PipelineStateWebGPU::SetShader(ShaderStageType stage, Shader* shader) shaders_[static_cast(stage)] = shader; } +bool PipelineStateWebGPU::HasBinding(uint32_t group, uint32_t binding) const +{ + if (!hasBindingReflection_) + { + return true; + } + + for (const auto& b : bindings_) + { + if (b.Group == group && b.Binding == binding) + { + return true; + } + } + + return false; +} + +bool PipelineStateWebGPU::HasBinding(uint32_t group, uint32_t binding, ShaderBindingResourceTypeWebGPU resourceType) const +{ + if (!hasBindingReflection_) + { + return true; + } + + for (const auto& b : bindings_) + { + const bool isCompatibleStorageBuffer = + resourceType == ShaderBindingResourceTypeWebGPU::StorageBuffer && + b.ResourceType == ShaderBindingResourceTypeWebGPU::ReadOnlyStorageBuffer; + if (b.Group == group && b.Binding == binding && + (b.ResourceType == resourceType || b.ResourceType == ShaderBindingResourceTypeWebGPU::Unknown || isCompatibleStorageBuffer)) + { + return true; + } + } + + return false; +} + bool PipelineStateWebGPU::Compile() { + bindings_.clear(); + hasBindingReflection_ = false; + for (auto shader : shaders_) + { + auto shaderWebGPU = static_cast(shader); + if (shaderWebGPU == nullptr || !shaderWebGPU->HasBindingReflection()) + { + continue; + } + + hasBindingReflection_ = true; + for (const auto& reflectedBinding : shaderWebGPU->GetBindings()) + { + if (!ContainsBinding(bindings_, reflectedBinding)) + { + bindings_.push_back(reflectedBinding); + } + } + } + const char* entryPointName = "main"; auto computeShader = static_cast(shaders_[static_cast(ShaderStageType::Compute)]); if (computeShader != nullptr) { wgpu::ComputePipelineDescriptor desc{}; desc.layout = nullptr; + if (hasBindingReflection_) + { + pipelineLayout_ = CreatePipelineLayout(device_, bindings_, wgpu::ShaderStage::Compute, bindGroupLayouts_); + desc.layout = pipelineLayout_; + } desc.compute.module = computeShader->GetShaderModule(); desc.compute.entryPoint = entryPointName; computePipeline_ = device_.CreateComputePipeline(&desc); @@ -47,6 +223,11 @@ bool PipelineStateWebGPU::Compile() desc.multisample.mask = std::numeric_limits::max(); desc.multisample.alphaToCoverageEnabled = false; desc.layout = nullptr; // is it correct? + if (hasBindingReflection_) + { + pipelineLayout_ = CreatePipelineLayout(device_, bindings_, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, bindGroupLayouts_); + desc.layout = pipelineLayout_; + } auto vertexShader = static_cast(shaders_[static_cast(ShaderStageType::Vertex)]); @@ -72,7 +253,7 @@ bool PipelineStateWebGPU::Compile() attributes[i].shaderLocation = i; offset += GetSize(VertexLayouts[i]); } - bufferLayouts[0].arrayStride = offset; + bufferLayouts[0].arrayStride = VertexBufferStride > 0 ? VertexBufferStride : offset; auto pixelShader = static_cast(shaders_[static_cast(ShaderStageType::Pixel)]); diff --git a/src/WebGPU/LLGI.PipelineStateWebGPU.h b/src/WebGPU/LLGI.PipelineStateWebGPU.h index e0411774..64c4eee6 100644 --- a/src/WebGPU/LLGI.PipelineStateWebGPU.h +++ b/src/WebGPU/LLGI.PipelineStateWebGPU.h @@ -2,6 +2,9 @@ #include "../LLGI.PipelineState.h" #include "LLGI.BaseWebGPU.h" +#include "LLGI.ShaderWebGPU.h" +#include +#include namespace LLGI { @@ -14,6 +17,10 @@ class PipelineStateWebGPU : public PipelineState wgpu::RenderPipeline renderPipeline_; wgpu::ComputePipeline computePipeline_; + std::array bindGroupLayouts_; + wgpu::PipelineLayout pipelineLayout_; + std::vector bindings_; + bool hasBindingReflection_ = false; public: PipelineStateWebGPU(wgpu::Device device); @@ -25,6 +32,8 @@ class PipelineStateWebGPU : public PipelineState wgpu::RenderPipeline GetRenderPipeline() { return renderPipeline_; } wgpu::ComputePipeline GetComputePipeline() { return computePipeline_; } + bool HasBinding(uint32_t group, uint32_t binding) const; + bool HasBinding(uint32_t group, uint32_t binding, ShaderBindingResourceTypeWebGPU resourceType) const; }; } // namespace LLGI diff --git a/src/WebGPU/LLGI.PlatformWebGPU.cpp b/src/WebGPU/LLGI.PlatformWebGPU.cpp index dec71796..b05b6a18 100644 --- a/src/WebGPU/LLGI.PlatformWebGPU.cpp +++ b/src/WebGPU/LLGI.PlatformWebGPU.cpp @@ -207,7 +207,12 @@ bool PlatformWebGPU::Initialize(Window* window, bool waitVSync) wgpu::DeviceDescriptor deviceDescriptor{}; std::vector requiredFeatures; - for (auto feature : {wgpu::FeatureName::Float32Filterable, wgpu::FeatureName::TextureFormatsTier2}) + const wgpu::FeatureName optionalFeatures[] = { + wgpu::FeatureName::Float32Filterable, + wgpu::FeatureName::TextureFormatsTier2, + wgpu::FeatureName::TextureCompressionBC, + }; + for (auto feature : optionalFeatures) { if (adapter_.HasFeature(feature)) { diff --git a/src/WebGPU/LLGI.ShaderWebGPU.cpp b/src/WebGPU/LLGI.ShaderWebGPU.cpp index dc01a7d7..344f74f0 100644 --- a/src/WebGPU/LLGI.ShaderWebGPU.cpp +++ b/src/WebGPU/LLGI.ShaderWebGPU.cpp @@ -1,9 +1,237 @@ #include "LLGI.ShaderWebGPU.h" +#include #include #include namespace LLGI { +namespace +{ +bool ParseAttributeIndex(const std::string& code, const char* attribute, size_t offset, uint32_t& value) +{ + const auto attributePos = code.find(attribute, offset); + if (attributePos == std::string::npos) + { + return false; + } + + const auto openPos = code.find('(', attributePos); + if (openPos == std::string::npos) + { + return false; + } + + auto digitPos = openPos + 1; + while (digitPos < code.size() && std::isspace(static_cast(code[digitPos])) != 0) + { + digitPos++; + } + + if (digitPos >= code.size() || std::isdigit(static_cast(code[digitPos])) == 0) + { + return false; + } + + uint32_t parsed = 0; + while (digitPos < code.size() && std::isdigit(static_cast(code[digitPos])) != 0) + { + parsed = parsed * 10 + static_cast(code[digitPos] - '0'); + digitPos++; + } + + value = parsed; + return true; +} + +wgpu::StorageTextureAccess ParseStorageTextureAccess(const std::string& statement) +{ + if (statement.find("read_write") != std::string::npos) + { + return wgpu::StorageTextureAccess::ReadWrite; + } + if (statement.find("read") != std::string::npos) + { + return wgpu::StorageTextureAccess::ReadOnly; + } + return wgpu::StorageTextureAccess::WriteOnly; +} + +wgpu::TextureFormat ParseStorageTextureFormat(const std::string& statement) +{ + if (statement.find("rgba32float") != std::string::npos) + { + return wgpu::TextureFormat::RGBA32Float; + } + if (statement.find("rgba32uint") != std::string::npos) + { + return wgpu::TextureFormat::RGBA32Uint; + } + if (statement.find("rgba32sint") != std::string::npos) + { + return wgpu::TextureFormat::RGBA32Sint; + } + if (statement.find("rgba16float") != std::string::npos) + { + return wgpu::TextureFormat::RGBA16Float; + } + if (statement.find("rgba8unorm") != std::string::npos) + { + return wgpu::TextureFormat::RGBA8Unorm; + } + if (statement.find("rgba8snorm") != std::string::npos) + { + return wgpu::TextureFormat::RGBA8Snorm; + } + if (statement.find("rgba8uint") != std::string::npos) + { + return wgpu::TextureFormat::RGBA8Uint; + } + if (statement.find("rgba8sint") != std::string::npos) + { + return wgpu::TextureFormat::RGBA8Sint; + } + if (statement.find("r32float") != std::string::npos) + { + return wgpu::TextureFormat::R32Float; + } + if (statement.find("r32uint") != std::string::npos) + { + return wgpu::TextureFormat::R32Uint; + } + if (statement.find("r32sint") != std::string::npos) + { + return wgpu::TextureFormat::R32Sint; + } + return wgpu::TextureFormat::Undefined; +} + +wgpu::TextureViewDimension ParseTextureViewDimension(const std::string& statement) +{ + if (statement.find("texture_1d") != std::string::npos) + { + return wgpu::TextureViewDimension::e1D; + } + if (statement.find("texture_2d_array") != std::string::npos || statement.find("texture_storage_2d_array") != std::string::npos) + { + return wgpu::TextureViewDimension::e2DArray; + } + if (statement.find("texture_3d") != std::string::npos || statement.find("texture_storage_3d") != std::string::npos) + { + return wgpu::TextureViewDimension::e3D; + } + if (statement.find("texture_cube_array") != std::string::npos) + { + return wgpu::TextureViewDimension::CubeArray; + } + if (statement.find("texture_cube") != std::string::npos) + { + return wgpu::TextureViewDimension::Cube; + } + return wgpu::TextureViewDimension::e2D; +} + +bool IsSameBinding(const ShaderBindingWebGPU& lhs, const ShaderBindingWebGPU& rhs) +{ + if (lhs.Group != rhs.Group || lhs.Binding != rhs.Binding || lhs.ResourceType != rhs.ResourceType) + { + return false; + } + + if ((lhs.ResourceType == ShaderBindingResourceTypeWebGPU::Texture || + lhs.ResourceType == ShaderBindingResourceTypeWebGPU::StorageTexture) && + lhs.TextureViewDimension != rhs.TextureViewDimension) + { + return false; + } + + if (lhs.ResourceType == ShaderBindingResourceTypeWebGPU::StorageTexture) + { + return lhs.StorageTextureFormat == rhs.StorageTextureFormat && + lhs.StorageTextureAccess == rhs.StorageTextureAccess; + } + + return true; +} + +std::vector ReflectBindings(const std::string& code) +{ + std::vector bindings; + + size_t offset = 0; + while (true) + { + const auto groupPos = code.find("@group", offset); + if (groupPos == std::string::npos) + { + break; + } + + uint32_t group = 0; + uint32_t binding = 0; + if (ParseAttributeIndex(code, "@group", groupPos, group) && + ParseAttributeIndex(code, "@binding", groupPos, binding)) + { + const auto statementEnd = code.find(';', groupPos); + const auto statementLength = statementEnd == std::string::npos ? std::string::npos : statementEnd - groupPos; + const auto statement = code.substr(groupPos, statementLength); + ShaderBindingWebGPU reflected; + reflected.Group = group; + reflected.Binding = binding; + if (statement.find("var") != std::string::npos) + { + reflected.ResourceType = ShaderBindingResourceTypeWebGPU::UniformBuffer; + } + else if (statement.find("var(&wgslDesc); + bindings_ = ReflectBindings(wgslCode); + hasBindingReflection_ = true; } else { @@ -63,4 +293,8 @@ bool ShaderWebGPU::Initialize(wgpu::Device& device, DataStructure* data, int32_t wgpu::ShaderModule& ShaderWebGPU::GetShaderModule() { return shaderModule_; } +const std::vector& ShaderWebGPU::GetBindings() const { return bindings_; } + +bool ShaderWebGPU::HasBindingReflection() const { return hasBindingReflection_; } + } // namespace LLGI diff --git a/src/WebGPU/LLGI.ShaderWebGPU.h b/src/WebGPU/LLGI.ShaderWebGPU.h index 0cd74907..42176e14 100644 --- a/src/WebGPU/LLGI.ShaderWebGPU.h +++ b/src/WebGPU/LLGI.ShaderWebGPU.h @@ -2,22 +2,48 @@ #include "LLGI.BaseWebGPU.h" #include "../LLGI.Shader.h" +#include namespace LLGI { +enum class ShaderBindingResourceTypeWebGPU +{ + Unknown, + UniformBuffer, + Texture, + Sampler, + StorageBuffer, + ReadOnlyStorageBuffer, + StorageTexture, +}; + +struct ShaderBindingWebGPU +{ + uint32_t Group = 0; + uint32_t Binding = 0; + ShaderBindingResourceTypeWebGPU ResourceType = ShaderBindingResourceTypeWebGPU::Unknown; + wgpu::TextureViewDimension TextureViewDimension = wgpu::TextureViewDimension::e2D; + wgpu::TextureFormat StorageTextureFormat = wgpu::TextureFormat::Undefined; + wgpu::StorageTextureAccess StorageTextureAccess = wgpu::StorageTextureAccess::Undefined; +}; + class ShaderWebGPU : public Shader { private: - wgpu::ShaderModule shaderModule_; + wgpu::ShaderModule shaderModule_; + std::vector bindings_; + bool hasBindingReflection_ = false; public: ShaderWebGPU(); ~ShaderWebGPU() override; - bool Initialize(wgpu::Device& device, DataStructure* data, int32_t count); + bool Initialize(wgpu::Device& device, DataStructure* data, int32_t count); - wgpu::ShaderModule& GetShaderModule(); + wgpu::ShaderModule& GetShaderModule(); + const std::vector& GetBindings() const; + bool HasBindingReflection() const; }; -} \ No newline at end of file +} diff --git a/src/WebGPU/LLGI.TextureWebGPU.cpp b/src/WebGPU/LLGI.TextureWebGPU.cpp index eb66a3a0..88af6e61 100644 --- a/src/WebGPU/LLGI.TextureWebGPU.cpp +++ b/src/WebGPU/LLGI.TextureWebGPU.cpp @@ -1,5 +1,6 @@ #include "LLGI.TextureWebGPU.h" +#include #include #include #include @@ -17,6 +18,57 @@ uint32_t AlignTo(uint32_t value, uint32_t alignment) { return (value + alignment - 1) / alignment * alignment; } + +int32_t GetMipmapPixelSize(TextureFormatType format) +{ + switch (format) + { + case TextureFormatType::R8_UNORM: + return 1; + case TextureFormatType::R8G8B8A8_UNORM: + case TextureFormatType::B8G8R8A8_UNORM: + case TextureFormatType::R8G8B8A8_UNORM_SRGB: + case TextureFormatType::B8G8R8A8_UNORM_SRGB: + return 4; + default: + return 0; + } +} + +std::vector GenerateNextMipmap(const std::vector& src, int32_t srcWidth, int32_t srcHeight, int32_t pixelSize) +{ + const auto dstWidth = std::max(srcWidth / 2, 1); + const auto dstHeight = std::max(srcHeight / 2, 1); + std::vector dst(static_cast(dstWidth) * static_cast(dstHeight) * pixelSize); + + for (int32_t y = 0; y < dstHeight; y++) + { + for (int32_t x = 0; x < dstWidth; x++) + { + for (int32_t c = 0; c < pixelSize; c++) + { + int32_t sum = 0; + int32_t count = 0; + for (int32_t oy = 0; oy < 2; oy++) + { + const auto sy = std::min(y * 2 + oy, srcHeight - 1); + for (int32_t ox = 0; ox < 2; ox++) + { + const auto sx = std::min(x * 2 + ox, srcWidth - 1); + const auto srcIndex = (static_cast(sy) * srcWidth + sx) * pixelSize + c; + sum += src[srcIndex]; + count++; + } + } + + const auto dstIndex = (static_cast(y) * dstWidth + x) * pixelSize + c; + dst[dstIndex] = static_cast((sum + count / 2) / count); + } + } + } + + return dst; +} } // namespace bool TextureWebGPU::Initialize(wgpu::Device& device, const TextureParameter& parameter, wgpu::Instance instance) @@ -57,7 +109,7 @@ bool TextureWebGPU::Initialize(wgpu::Device& device, const TextureParameter& par wgpu::TextureDescriptor texDesc{}; texDesc.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::CopySrc; - if ((parameter.Usage & TextureUsageType::RenderTarget) != TextureUsageType::NoneFlag) + if (IsDepthFormat(parameter.Format) || (parameter.Usage & TextureUsageType::RenderTarget) != TextureUsageType::NoneFlag) { texDesc.usage |= wgpu::TextureUsage::RenderAttachment; } @@ -162,6 +214,7 @@ void TextureWebGPU::Unlock() { wgpu::TexelCopyTextureInfo imageCopyTexture{}; imageCopyTexture.texture = texture_; + imageCopyTexture.mipLevel = 0; imageCopyTexture.aspect = wgpu::TextureAspect::All; wgpu::TexelCopyBufferLayout textureDataLayout; @@ -173,6 +226,47 @@ void TextureWebGPU::Unlock() device_.GetQueue().WriteTexture(&imageCopyTexture, temp_buffer_.data(), temp_buffer_.size(), &textureDataLayout, &extent); } +void TextureWebGPU::GenerateMipMaps() +{ + if (parameter_.MipLevelCount <= 1 || parameter_.Dimension != 2 || parameter_.Size.Z != 1) + { + return; + } + + const auto pixelSize = GetMipmapPixelSize(format_); + if (pixelSize == 0 || temp_buffer_.empty()) + { + return; + } + + auto srcData = temp_buffer_; + auto srcWidth = parameter_.Size.X; + auto srcHeight = parameter_.Size.Y; + + for (uint32_t mipLevel = 1; mipLevel < static_cast(parameter_.MipLevelCount); mipLevel++) + { + auto mipData = GenerateNextMipmap(srcData, srcWidth, srcHeight, pixelSize); + srcWidth = std::max(srcWidth / 2, 1); + srcHeight = std::max(srcHeight / 2, 1); + + wgpu::TexelCopyTextureInfo imageCopyTexture{}; + imageCopyTexture.texture = texture_; + imageCopyTexture.mipLevel = mipLevel; + imageCopyTexture.aspect = wgpu::TextureAspect::All; + + wgpu::TexelCopyBufferLayout textureDataLayout{}; + textureDataLayout.bytesPerRow = static_cast(srcWidth * pixelSize); + + wgpu::Extent3D extent{}; + extent.width = static_cast(srcWidth); + extent.height = static_cast(srcHeight); + extent.depthOrArrayLayers = 1; + device_.GetQueue().WriteTexture(&imageCopyTexture, mipData.data(), mipData.size(), &textureDataLayout, &extent); + + srcData = std::move(mipData); + } +} + bool TextureWebGPU::GetData(std::vector& data) { const auto bytesPerRowUnaligned = static_cast(GetTextureRowPitch(format_, parameter_.Size)); diff --git a/src/WebGPU/LLGI.TextureWebGPU.h b/src/WebGPU/LLGI.TextureWebGPU.h index 76354bb9..54f19c9a 100644 --- a/src/WebGPU/LLGI.TextureWebGPU.h +++ b/src/WebGPU/LLGI.TextureWebGPU.h @@ -22,6 +22,7 @@ class TextureWebGPU : public Texture bool InitializeFromSurfaceTexture(wgpu::Device& device, wgpu::Texture texture, const TextureParameter& parameter); void* Lock() override; void Unlock() override; + void GenerateMipMaps() override; bool GetData(std::vector& data) override; Vec2I GetSizeAs2D() const override; bool IsRenderTexture() const override; diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index edfc0525..b01dbb15 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,6 +2,8 @@ if(BUILD_WEBGPU) add_definitions(-DENABLE_WEBGPU) endif() -add_subdirectory(ShaderTranspilerCore) +if(NOT TARGET ShaderTranspilerCore) + add_subdirectory(ShaderTranspilerCore) +endif() add_subdirectory(ShaderTranspiler) install(TARGETS ShaderTranspiler DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/tools/ShaderTranspiler/main.cpp b/tools/ShaderTranspiler/main.cpp index 492e7d1d..c2679521 100644 --- a/tools/ShaderTranspiler/main.cpp +++ b/tools/ShaderTranspiler/main.cpp @@ -35,6 +35,7 @@ int main(int argc, char* argv[]) bool isES = false; bool isDX12 = false; bool plain = false; + bool fixWGSLMatrixDirection = false; int shaderModel = 0; std::vector includeDir; std::vector macros; @@ -116,6 +117,11 @@ int main(int argc, char* argv[]) isDX12 = true; i += 1; } + else if (args[i] == "--fix-wgsl-matrix-direction") + { + fixWGSLMatrixDirection = true; + i += 1; + } else if (args[i] == "--input") { if (i == args.size() - 1) @@ -227,7 +233,7 @@ int main(int argc, char* argv[]) } else if (outputType == OutputType::WGSL) { - transpiler = std::make_shared(); + transpiler = std::make_shared(fixWGSLMatrixDirection); } std::cout << inputPath << " -> " << outputPath << " ShaderModel=" << shaderModel << std::endl; diff --git a/tools/ShaderTranspilerCore/CMakeLists.txt b/tools/ShaderTranspilerCore/CMakeLists.txt index a09a6189..2d338904 100644 --- a/tools/ShaderTranspilerCore/CMakeLists.txt +++ b/tools/ShaderTranspilerCore/CMakeLists.txt @@ -9,12 +9,18 @@ if(BUILD_WEBGPU) else() target_compile_features(ShaderTranspilerCore PUBLIC cxx_std_17) endif() -target_include_directories(ShaderTranspilerCore - PUBLIC ${LLGI_THIRDPARTY_INCLUDES}) +target_include_directories( + ShaderTranspilerCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + ${LLGI_THIRDPARTY_INCLUDES}) if(USE_THIRDPARTY_DIRECTORY) - target_link_libraries(ShaderTranspilerCore - PUBLIC ${LLGI_THIRDPARTY_LIBRARIES}) + if(LLGI_THIRDPARTY_LIBRARY_FILES) + target_link_libraries(ShaderTranspilerCore + PUBLIC ${LLGI_THIRDPARTY_LIBRARY_FILES}) + else() + target_link_libraries(ShaderTranspilerCore + PUBLIC ${LLGI_THIRDPARTY_LIBRARIES}) + endif() target_link_directories(ShaderTranspilerCore PUBLIC ${LLGI_THIRDPARTY_LIBRARY_DIRECTORIES}) endif() @@ -29,7 +35,12 @@ else() endif() if(USE_THIRDPARTY_DIRECTORY) - add_dependencies(ShaderTranspilerCore EP_glslang EP_SPIRV-Cross) + if(TARGET EP_glslang) + add_dependencies(ShaderTranspilerCore EP_glslang) + endif() + if(TARGET EP_SPIRV-Cross) + add_dependencies(ShaderTranspilerCore EP_SPIRV-Cross) + endif() endif() if(MSVC) diff --git a/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp b/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp index f482433e..b059a586 100644 --- a/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp +++ b/tools/ShaderTranspilerCore/ShaderTranspilerCore.cpp @@ -30,6 +30,7 @@ #endif #include +#include #include namespace LLGI @@ -51,7 +52,22 @@ std::string Replace(std::string target, std::string from_, std::string to_) } #if (ENABLE_WEBGPU) -std::string NormalizeWGSLForLLGI(std::string code, ShaderStageType shaderStageType) +std::string NormalizeWGSLMatrixDirection(std::string code) +{ + static const std::regex modelMatrixRegex(R"(\((localPos|localPosition|worldPos|localNormal|localBinormal|localTangent) \* (mModel)\))"); + static const std::regex cameraMatrixRegex(R"(\((\w+) \* (v\._\w+_mCameraProj)\))"); + static const std::regex cameraMatrixExpressionRegex(R"(\(\(([^()]+ \+ [^()]+)\) \* (v\._\w+_mCameraProj)\))"); + + code = std::regex_replace(code, modelMatrixRegex, "($2 * $1)"); + code = std::regex_replace(code, cameraMatrixRegex, "($2 * $1)"); + code = std::regex_replace(code, cameraMatrixExpressionRegex, "($2 * ($1))"); + code = Replace(code, "worldNormal = normalize(worldNormal);", "worldNormal = vec4(normalize(worldNormal.xyz), 0.0f);"); + code = Replace(code, "worldBinormal = normalize(worldBinormal);", "worldBinormal = vec4(normalize(worldBinormal.xyz), 0.0f);"); + code = Replace(code, "worldTangent = normalize(worldTangent);", "worldTangent = vec4(normalize(worldTangent.xyz), 0.0f);"); + return code; +} + +std::string NormalizeWGSLForLLGI(std::string code, ShaderStageType shaderStageType, bool fixMatrixDirection) { for (uint32_t i = 0; i < TextureSlotMax; i++) { @@ -90,6 +106,11 @@ std::string NormalizeWGSLForLLGI(std::string code, ShaderStageType shaderStageTy } code = output.str(); + if (fixMatrixDirection) + { + code = NormalizeWGSLMatrixDirection(code); + } + return code; } #endif @@ -442,7 +463,7 @@ bool SPIRVToGLSLTranspiler::Transpile(const std::shared_ptr& spirv, LLGI: return true; } -SPIRVToWGSLTranspiler::SPIRVToWGSLTranspiler() +SPIRVToWGSLTranspiler::SPIRVToWGSLTranspiler(bool fixMatrixDirection) : fixMatrixDirection_(fixMatrixDirection) { #if (ENABLE_WEBGPU) tint::Initialize(); @@ -469,7 +490,7 @@ bool SPIRVToWGSLTranspiler::Transpile(const std::shared_ptr& spirv, LLGI: return false; } - code_ = NormalizeWGSLForLLGI(result.Get(), shaderStageType); + code_ = NormalizeWGSLForLLGI(result.Get(), shaderStageType, fixMatrixDirection_); return true; #else errorCode_ = "WGSL output requires ShaderTranspilerCore to be built with BUILD_WEBGPU=ON."; diff --git a/tools/ShaderTranspilerCore/ShaderTranspilerCore.h b/tools/ShaderTranspilerCore/ShaderTranspilerCore.h index 1fd77ae0..9d2704b7 100644 --- a/tools/ShaderTranspilerCore/ShaderTranspilerCore.h +++ b/tools/ShaderTranspilerCore/ShaderTranspilerCore.h @@ -97,8 +97,10 @@ class SPIRVToGLSLTranspiler : public SPIRVTranspiler class SPIRVToWGSLTranspiler : public SPIRVTranspiler { + bool fixMatrixDirection_ = false; + public: - SPIRVToWGSLTranspiler(); + SPIRVToWGSLTranspiler(bool fixMatrixDirection = false); ~SPIRVToWGSLTranspiler() override; bool Transpile(const std::shared_ptr& spirv, LLGI::ShaderStageType shaderStageType) override; }; From a5184b41039c13171e32ec20e3965b38778d930f Mon Sep 17 00:00:00 2001 From: durswd Date: Thu, 7 May 2026 02:56:43 +0900 Subject: [PATCH 15/16] Update for WebGPU. Implement mipmap --- src/CMakeLists.txt | 18 ++ src/DX12/LLGI.BaseDX12.cpp | 5 +- src/DX12/LLGI.BaseDX12.h | 3 +- src/DX12/LLGI.CommandListDX12.cpp | 266 ++++++++++++++++- src/DX12/LLGI.CommandListDX12.h | 6 + src/DX12/LLGI.TextureDX12.cpp | 17 +- src/LLGI.Texture.h | 5 - src/Vulkan/LLGI.PlatformVulkan.cpp | 50 ++-- src/Vulkan/LLGI.PlatformVulkan.h | 3 +- src/WebGPU/LLGI.CommandListWebGPU.cpp | 149 +++++++++- src/WebGPU/LLGI.CommandListWebGPU.h | 31 ++ src/WebGPU/LLGI.GraphicsWebGPU.cpp | 28 +- src/WebGPU/LLGI.TextureWebGPU.cpp | 275 +++++++++++++----- src/WebGPU/LLGI.TextureWebGPU.h | 11 +- src_test/test_compute_shader.cpp | 24 +- tools/DX12MipmapShaderCompiler/CMakeLists.txt | 6 + tools/DX12MipmapShaderCompiler/main.cpp | 132 +++++++++ 17 files changed, 882 insertions(+), 147 deletions(-) create mode 100644 tools/DX12MipmapShaderCompiler/CMakeLists.txt create mode 100644 tools/DX12MipmapShaderCompiler/main.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 60687065..6cfa7002 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,20 @@ endif() if(MSVC) file(GLOB files_dx12 DX12/*.h DX12/*.cpp) list(APPEND files ${files_dx12}) + + set(LLGI_DX12_MIPMAP_SHADER_HEADER + "${CMAKE_CURRENT_BINARY_DIR}/generated/LLGI.DX12MipmapShader.h") + add_subdirectory("../tools/DX12MipmapShaderCompiler" + "${CMAKE_CURRENT_BINARY_DIR}/DX12MipmapShaderCompiler") + add_custom_command( + OUTPUT "${LLGI_DX12_MIPMAP_SHADER_HEADER}" + COMMAND ${CMAKE_COMMAND} -E make_directory + "${CMAKE_CURRENT_BINARY_DIR}/generated" + COMMAND $ + "${LLGI_DX12_MIPMAP_SHADER_HEADER}" + DEPENDS LLGI_DX12MipmapShaderCompiler + VERBATIM) + list(APPEND files "${LLGI_DX12_MIPMAP_SHADER_HEADER}") endif() if(BUILD_VULKAN) @@ -72,6 +86,10 @@ endforeach() add_library(LLGI STATIC ${files}) +if(MSVC) + target_include_directories(LLGI PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated") +endif() + file(GLOB LOCAL_HEADERS *.h) set_target_properties(LLGI PROPERTIES PUBLIC_HEADER "${LOCAL_HEADERS}") if(BUILD_WEBGPU) diff --git a/src/DX12/LLGI.BaseDX12.cpp b/src/DX12/LLGI.BaseDX12.cpp index d80e1ff6..a7bc0a14 100644 --- a/src/DX12/LLGI.BaseDX12.cpp +++ b/src/DX12/LLGI.BaseDX12.cpp @@ -71,7 +71,8 @@ ID3D12Resource* CreateResourceBuffer(ID3D12Device* device, D3D12_RESOURCE_STATES resourceState, D3D12_RESOURCE_FLAGS flags, Vec3I size, - int32_t samplingCount) + int32_t samplingCount, + int32_t mipLevelCount) { D3D12_HEAP_PROPERTIES heapProps = {}; D3D12_RESOURCE_DESC resDesc = {}; @@ -104,7 +105,7 @@ ID3D12Resource* CreateResourceBuffer(ID3D12Device* device, resDesc.Width = size.X; resDesc.Height = size.Y; resDesc.DepthOrArraySize = static_cast(size.Z); - resDesc.MipLevels = 1; + resDesc.MipLevels = static_cast(mipLevelCount); resDesc.Format = DirectX12::GetGeneratedFormat(format); resDesc.Layout = (resourceDimention == D3D12_RESOURCE_DIMENSION_BUFFER ? D3D12_TEXTURE_LAYOUT_ROW_MAJOR : D3D12_TEXTURE_LAYOUT_UNKNOWN); resDesc.SampleDesc.Count = samplingCount; diff --git a/src/DX12/LLGI.BaseDX12.h b/src/DX12/LLGI.BaseDX12.h index a3556c7c..46f84541 100644 --- a/src/DX12/LLGI.BaseDX12.h +++ b/src/DX12/LLGI.BaseDX12.h @@ -47,7 +47,8 @@ ID3D12Resource* CreateResourceBuffer(ID3D12Device* device, D3D12_RESOURCE_STATES resourceState, D3D12_RESOURCE_FLAGS flags, Vec3I size, - int32_t samplingCount); + int32_t samplingCount, + int32_t mipLevelCount = 1); DXGI_FORMAT ConvertFormat(TextureFormatType format); diff --git a/src/DX12/LLGI.CommandListDX12.cpp b/src/DX12/LLGI.CommandListDX12.cpp index f374507c..182c0c2d 100644 --- a/src/DX12/LLGI.CommandListDX12.cpp +++ b/src/DX12/LLGI.CommandListDX12.cpp @@ -6,9 +6,73 @@ #include "LLGI.RenderPassDX12.h" #include "LLGI.TextureDX12.h" #include "LLGI.QueryDX12.h" +#include "LLGI.DX12MipmapShader.h" +#include namespace LLGI { +namespace +{ + +bool CanGenerateMipMap(const TextureDX12* texture) +{ + return texture != nullptr && texture->GetMipmapCount() > 1 && texture->GetParameter().Dimension == 2 && + texture->GetParameter().SampleCount == 1; +} + +D3D12_SHADER_RESOURCE_VIEW_DESC CreateMipmapSRVDesc(const TextureDX12* texture, int32_t mip) +{ + D3D12_SHADER_RESOURCE_VIEW_DESC desc = {}; + desc.Format = DirectX12::GetShaderResourceViewFormat(texture->GetDXGIFormat()); + desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + desc.Texture2D.MostDetailedMip = mip - 1; + desc.Texture2D.MipLevels = 1; + desc.Texture2D.ResourceMinLODClamp = 0.0f; + return desc; +} + +D3D12_RENDER_TARGET_VIEW_DESC CreateMipmapRTVDesc(const TextureDX12* texture, int32_t mip) +{ + D3D12_RENDER_TARGET_VIEW_DESC desc = {}; + desc.Format = texture->GetDXGIFormat(); + desc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D; + desc.Texture2D.MipSlice = mip; + desc.Texture2D.PlaneSlice = 0; + return desc; +} + +D3D12_RESOURCE_BARRIER CreateMipmapTransitionBarrier( + TextureDX12* texture, int32_t mip, D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after) +{ + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = texture->Get(); + barrier.Transition.Subresource = mip; + barrier.Transition.StateBefore = before; + barrier.Transition.StateAfter = after; + return barrier; +} + +D3D12_VIEWPORT CreateMipmapViewport(int32_t width, int32_t height) +{ + D3D12_VIEWPORT viewport = {}; + viewport.Width = static_cast(width); + viewport.Height = static_cast(height); + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + return viewport; +} + +D3D12_RECT CreateMipmapScissor(int32_t width, int32_t height) +{ + D3D12_RECT scissor = {}; + scissor.right = width; + scissor.bottom = height; + return scissor; +} + +} // namespace D3D12_SHADER_RESOURCE_VIEW_DESC CommandListDX12::GetSRVDescFromTexture(const TextureDX12* texture) { @@ -17,7 +81,7 @@ D3D12_SHADER_RESOURCE_VIEW_DESC CommandListDX12::GetSRVDescFromTexture(const Tex if (texture->GetParameter().Dimension == 3) { srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; - srvDesc.Texture3D.MipLevels = 1; + srvDesc.Texture3D.MipLevels = texture->GetMipmapCount(); srvDesc.Texture3D.MostDetailedMip = 0; srvDesc.Texture3D.ResourceMinLODClamp = 0.0f; } @@ -33,7 +97,7 @@ D3D12_SHADER_RESOURCE_VIEW_DESC CommandListDX12::GetSRVDescFromTexture(const Tex } srvDesc.Texture2DArray.ArraySize = texture->GetParameter().Size.Z; srvDesc.Texture2DArray.FirstArraySlice = 0; - srvDesc.Texture2DArray.MipLevels = 1; + srvDesc.Texture2DArray.MipLevels = texture->GetMipmapCount(); srvDesc.Texture2DArray.MostDetailedMip = 0; srvDesc.Texture2DArray.PlaneSlice = 0; srvDesc.Texture2DArray.ResourceMinLODClamp = 0.0f; @@ -48,7 +112,7 @@ D3D12_SHADER_RESOURCE_VIEW_DESC CommandListDX12::GetSRVDescFromTexture(const Tex { srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; } - srvDesc.Texture2D.MipLevels = 1; + srvDesc.Texture2D.MipLevels = texture->GetMipmapCount(); srvDesc.Texture2D.MostDetailedMip = 0; } @@ -57,6 +121,108 @@ D3D12_SHADER_RESOURCE_VIEW_DESC CommandListDX12::GetSRVDescFromTexture(const Tex return srvDesc; } +bool CommandListDX12::CreateMipmapRootSignature() +{ + if (mipmapRootSignature_ != nullptr) + { + return true; + } + + D3D12_DESCRIPTOR_RANGE range = {}; + range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + range.NumDescriptors = 1; + range.BaseShaderRegister = 0; + range.RegisterSpace = 0; + range.OffsetInDescriptorsFromTableStart = 0; + + D3D12_ROOT_PARAMETER rootParameter = {}; + rootParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + rootParameter.DescriptorTable.NumDescriptorRanges = 1; + rootParameter.DescriptorTable.pDescriptorRanges = ⦥ + rootParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + D3D12_STATIC_SAMPLER_DESC sampler = {}; + sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.MipLODBias = 0.0f; + sampler.MaxAnisotropy = 1; + sampler.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + sampler.BorderColor = D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK; + sampler.MinLOD = 0.0f; + sampler.MaxLOD = D3D12_FLOAT32_MAX; + sampler.ShaderRegister = 0; + sampler.RegisterSpace = 0; + sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + D3D12_ROOT_SIGNATURE_DESC desc = {}; + desc.NumParameters = 1; + desc.pParameters = &rootParameter; + desc.NumStaticSamplers = 1; + desc.pStaticSamplers = &sampler; + desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; + + ID3DBlob* signature = nullptr; + ID3DBlob* error = nullptr; + auto hr = D3D12SerializeRootSignature(&desc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error); + if (FAILED(hr)) + { + SafeRelease(error); + return false; + } + + hr = graphics_->GetDevice()->CreateRootSignature( + 0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&mipmapRootSignature_)); + SafeRelease(signature); + SafeRelease(error); + return SUCCEEDED(hr); +} + +ID3D12PipelineState* CommandListDX12::GetMipmapPipelineState(DXGI_FORMAT format) +{ + auto found = mipmapPipelineStates_.find(format); + if (found != mipmapPipelineStates_.end()) + { + return found->second; + } + + if (!CreateMipmapRootSignature()) + { + return nullptr; + } + + D3D12_GRAPHICS_PIPELINE_STATE_DESC desc = {}; + desc.pRootSignature = mipmapRootSignature_; + desc.VS.pShaderBytecode = DX12MipmapShader::VertexShader; + desc.VS.BytecodeLength = DX12MipmapShader::VertexShaderSize; + desc.PS.pShaderBytecode = DX12MipmapShader::PixelShader; + desc.PS.BytecodeLength = DX12MipmapShader::PixelShaderSize; + desc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + desc.SampleMask = UINT_MAX; + desc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + desc.RasterizerState.DepthClipEnable = TRUE; + desc.DepthStencilState.DepthEnable = FALSE; + desc.DepthStencilState.StencilEnable = FALSE; + desc.InputLayout.NumElements = 0; + desc.InputLayout.pInputElementDescs = nullptr; + desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + desc.NumRenderTargets = 1; + desc.RTVFormats[0] = format; + desc.SampleDesc.Count = 1; + + ID3D12PipelineState* pipelineState = nullptr; + auto hr = graphics_->GetDevice()->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&pipelineState)); + if (FAILED(hr)) + { + return nullptr; + } + + mipmapPipelineStates_[format] = pipelineState; + return pipelineState; +} + D3D12_SAMPLER_DESC CommandListDX12::GeSamplerDescFromBindingTexture(const CommandList::BindingTexture& texture) { auto wrapMode = texture.wrapMode; @@ -142,6 +308,12 @@ CommandListDX12::CommandListDX12() CommandListDX12::~CommandListDX12() { SafeRelease(fence_); + SafeRelease(mipmapRootSignature_); + for (auto& pipelineState : mipmapPipelineStates_) + { + SafeRelease(pipelineState.second); + } + mipmapPipelineStates_.clear(); if (fenceEvent_ != nullptr) { @@ -674,6 +846,94 @@ void CommandListDX12::CopyTexture( RegisterReferencedObject(dst); } +void CommandListDX12::GenerateMipMap(Texture* src) +{ + if (isInRenderPass_) + { + Log(LogType::Error, "Please call GenerateMipMap outside of RenderPass"); + return; + } + + auto srcTex = static_cast(src); + if (!CanGenerateMipMap(srcTex)) + { + return; + } + + auto pipelineState = GetMipmapPipelineState(srcTex->GetDXGIFormat()); + if (pipelineState == nullptr) + { + Log(LogType::Error, "Failed to create a DX12 mipmap pipeline."); + return; + } + + srcTex->ResourceBarrier(currentCommandList_, D3D12_RESOURCE_STATE_GENERIC_READ); + + currentCommandList_->SetGraphicsRootSignature(mipmapRootSignature_); + currentCommandList_->SetPipelineState(pipelineState); + currentCommandList_->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + const auto mipmapCount = srcTex->GetMipmapCount(); + auto mipWidth = srcTex->GetSizeAs2D().X; + auto mipHeight = srcTex->GetSizeAs2D().Y; + + for (int32_t mip = 1; mip < mipmapCount; mip++) + { + const auto dstWidth = std::max(1, mipWidth / 2); + const auto dstHeight = std::max(1, mipHeight / 2); + + ID3D12DescriptorHeap* srvHeap = nullptr; + std::array cpuSrvHandles; + std::array gpuSrvHandles; + if (!cbDescriptorHeap_->Allocate(srvHeap, cpuSrvHandles, gpuSrvHandles, 1)) + { + Log(LogType::Error, "Failed to allocate a DX12 mipmap SRV descriptor."); + return; + } + + ID3D12DescriptorHeap* rtvHeap = nullptr; + std::array cpuRtvHandles; + std::array gpuRtvHandles; + if (!rtDescriptorHeap_->Allocate(rtvHeap, cpuRtvHandles, gpuRtvHandles, 1)) + { + Log(LogType::Error, "Failed to allocate a DX12 mipmap RTV descriptor."); + return; + } + + const auto srvDesc = CreateMipmapSRVDesc(srcTex, mip); + graphics_->GetDevice()->CreateShaderResourceView(srcTex->Get(), &srvDesc, cpuSrvHandles[0]); + + const auto rtvDesc = CreateMipmapRTVDesc(srcTex, mip); + graphics_->GetDevice()->CreateRenderTargetView(srcTex->Get(), &rtvDesc, cpuRtvHandles[0]); + + auto barrier = + CreateMipmapTransitionBarrier(srcTex, mip, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_RENDER_TARGET); + currentCommandList_->ResourceBarrier(1, &barrier); + + ID3D12DescriptorHeap* heaps[] = {srvHeap}; + currentCommandList_->SetDescriptorHeaps(1, heaps); + currentCommandList_->SetGraphicsRootDescriptorTable(0, gpuSrvHandles[0]); + currentCommandList_->OMSetRenderTargets(1, &cpuRtvHandles[0], FALSE, nullptr); + + const auto viewport = CreateMipmapViewport(dstWidth, dstHeight); + currentCommandList_->RSSetViewports(1, &viewport); + + const auto scissor = CreateMipmapScissor(dstWidth, dstHeight); + currentCommandList_->RSSetScissorRects(1, &scissor); + + currentCommandList_->DrawInstanced(3, 1, 0, 0); + + barrier = + CreateMipmapTransitionBarrier(srcTex, mip, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_GENERIC_READ); + currentCommandList_->ResourceBarrier(1, &barrier); + + mipWidth = dstWidth; + mipHeight = dstHeight; + } + + RegisterReferencedObject(src); +} + void CommandListDX12::CopyBuffer(Buffer* src, Buffer* dst) { auto srcBuf = static_cast(src); diff --git a/src/DX12/LLGI.CommandListDX12.h b/src/DX12/LLGI.CommandListDX12.h index 8b530491..495fd0ba 100644 --- a/src/DX12/LLGI.CommandListDX12.h +++ b/src/DX12/LLGI.CommandListDX12.h @@ -7,6 +7,7 @@ #include "LLGI.GraphicsDX12.h" #include "LLGI.PipelineStateDX12.h" #include "LLGI.RenderPassDX12.h" +#include namespace LLGI { @@ -37,10 +38,14 @@ class CommandListDX12 : public CommandList std::shared_ptr renderPass_; ID3D12GraphicsCommandList* currentCommandList_ = nullptr; + ID3D12RootSignature* mipmapRootSignature_ = nullptr; + std::unordered_map mipmapPipelineStates_; D3D12_SHADER_RESOURCE_VIEW_DESC GetSRVDescFromTexture(const TextureDX12* texture); D3D12_SAMPLER_DESC GeSamplerDescFromBindingTexture(const BindingTexture& texture); D3D12_SHADER_RESOURCE_VIEW_DESC GetSRVDescFromBindingBuffer(const BindingComputeBuffer& buffer); + bool CreateMipmapRootSignature(); + ID3D12PipelineState* GetMipmapPipelineState(DXGI_FORMAT format); void BeginInternal(); @@ -60,6 +65,7 @@ class CommandListDX12 : public CommandList void CopyTexture(Texture* src, Texture* dst) override; void CopyTexture( Texture* src, Texture* dst, const Vec3I& srcPos, const Vec3I& dstPos, const Vec3I& size, int srcLayer, int dstLayer) override; + void GenerateMipMap(Texture* src) override; void CopyBuffer(Buffer* src, Buffer* dst) override; diff --git a/src/DX12/LLGI.TextureDX12.cpp b/src/DX12/LLGI.TextureDX12.cpp index 942cf1e1..04f6a31c 100644 --- a/src/DX12/LLGI.TextureDX12.cpp +++ b/src/DX12/LLGI.TextureDX12.cpp @@ -36,6 +36,7 @@ TextureDX12::TextureDX12(ID3D12Resource* textureResource, ID3D12Device* device, format_ = ConvertFormat(desc.Format); texture_size_ = Vec3I(static_cast(desc.Width), static_cast(desc.Height), 1); cpu_memory_size_ = GetTextureMemorySize(format_, {texture_size_.X, texture_size_.Y, 1}); + mipmapCount_ = desc.MipLevels; UINT64 size = 0; device_->GetCopyableFootprints(&desc, 0, 1, 0, &footprint_, nullptr, nullptr, &size); @@ -69,6 +70,7 @@ bool TextureDX12::Initialize(const TextureParameter& parameter) texture_size_ = parameter.Size; samplingCount_ = parameter.SampleCount; parameter_ = parameter; + mipmapCount_ = parameter.MipLevelCount; type_ = TextureType::Color; @@ -101,6 +103,11 @@ bool TextureDX12::Initialize(const TextureParameter& parameter) resourceFlag |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; } + if (parameter.MipLevelCount > 1 && type_ == TextureType::Color && parameter.Dimension == 2 && parameter.SampleCount == 1) + { + resourceFlag |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + } + if (type_ == TextureType::Render) { texture_ = CreateResourceBuffer(device_, @@ -110,7 +117,8 @@ bool TextureDX12::Initialize(const TextureParameter& parameter) D3D12_RESOURCE_STATE_GENERIC_READ, resourceFlag, parameter.Size, - parameter.SampleCount); + parameter.SampleCount, + parameter.MipLevelCount); state_ = D3D12_RESOURCE_STATE_GENERIC_READ; } else if (type_ == TextureType::Depth) @@ -122,7 +130,8 @@ bool TextureDX12::Initialize(const TextureParameter& parameter) D3D12_RESOURCE_STATE_DEPTH_READ, resourceFlag, parameter.Size, - parameter.SampleCount); + parameter.SampleCount, + parameter.MipLevelCount); state_ = D3D12_RESOURCE_STATE_DEPTH_READ; } else if (type_ == TextureType::Color) @@ -135,7 +144,8 @@ bool TextureDX12::Initialize(const TextureParameter& parameter) D3D12_RESOURCE_STATE_COPY_DEST, resourceFlag, parameter.Size, - parameter.SampleCount); + parameter.SampleCount, + parameter.MipLevelCount); state_ = D3D12_RESOURCE_STATE_COPY_DEST; } @@ -162,6 +172,7 @@ bool TextureDX12::Initialize(ID3D12Resource* textureResource) format_ = ConvertFormat(desc.Format); texture_size_ = Vec3I(static_cast(desc.Width), static_cast(desc.Height), static_cast(desc.DepthOrArraySize)); cpu_memory_size_ = GetTextureMemorySize(format_, texture_size_); + mipmapCount_ = desc.MipLevels; UINT64 size = 0; device_->GetCopyableFootprints(&desc, 0, 1, 0, &footprint_, nullptr, nullptr, &size); diff --git a/src/LLGI.Texture.h b/src/LLGI.Texture.h index 9b498d4f..2b997ecf 100644 --- a/src/LLGI.Texture.h +++ b/src/LLGI.Texture.h @@ -32,11 +32,6 @@ class Texture : public ReferenceObject virtual bool GetData(std::vector& data) { return false; } - /** - @brief Generate mipmaps based on level zero. - */ - virtual void GenerateMipMaps() {} - virtual Vec2I GetSizeAs2D() const; [[deprecated("use GetType.")]] virtual bool IsRenderTexture() const; [[deprecated("use GetType.")]] virtual bool IsDepthTexture() const; diff --git a/src/Vulkan/LLGI.PlatformVulkan.cpp b/src/Vulkan/LLGI.PlatformVulkan.cpp index 5a2a7594..603aad98 100644 --- a/src/Vulkan/LLGI.PlatformVulkan.cpp +++ b/src/Vulkan/LLGI.PlatformVulkan.cpp @@ -1,6 +1,8 @@ #include "LLGI.PlatformVulkan.h" #include "LLGI.GraphicsVulkan.h" #include "LLGI.TextureVulkan.h" +#include +#include #include #ifdef _WIN32 @@ -56,6 +58,10 @@ bool PlatformVulkan::CreateSwapChain(Vec2I windowSize, bool waitVSync) { vkDevice_.destroyFence(swapBuffers[i].fence); } + if (swapBuffers[i].renderComplete) + { + vkDevice_.destroySemaphore(swapBuffers[i].renderComplete); + } SafeRelease(swapBuffers[i].texture); } @@ -160,6 +166,7 @@ bool PlatformVulkan::CreateSwapChain(Vec2I windowSize, bool waitVSync) viewCreateInfo.image = swapChainImages[i]; swapBuffers[i].view = vkDevice_.createImageView(viewCreateInfo); swapBuffers[i].fence = vk::Fence(); + swapBuffers[i].renderComplete = vkDevice_.createSemaphore(vk::SemaphoreCreateInfo()); swapBuffers[i].texture = new TextureVulkan(); if (!swapBuffers[i].texture->InitializeAsScreen(swapBuffers[i].image, swapBuffers[i].view, surfaceFormat, swapchainSize_)) @@ -306,6 +313,10 @@ void PlatformVulkan::Reset() { vkDevice_.destroyFence(swapBuffer.fence); } + if (swapBuffer.renderComplete) + { + vkDevice_.destroySemaphore(swapBuffer.renderComplete); + } SafeRelease(swapBuffer.texture); } @@ -333,12 +344,6 @@ void PlatformVulkan::Reset() vkPresentComplete_ = nullptr; } - if (vkRenderComplete_) - { - vkDevice_.destroySemaphore(vkRenderComplete_); - vkRenderComplete_ = nullptr; - } - if (vkCmdBuffers.size() > 0) { vkDevice_.freeCommandBuffers(vkCmdPool_, vkCmdBuffers); @@ -481,9 +486,8 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) vk::ApplicationInfo appInfo; appInfo.pApplicationName = "Vulkan"; appInfo.pEngineName = "Vulkan"; - appInfo.apiVersion = 1; appInfo.engineVersion = 1; - appInfo.apiVersion = VK_API_VERSION_1_0; + appInfo.apiVersion = VK_API_VERSION_1_1; // specify extension const std::vector extensions = { @@ -626,12 +630,21 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) queueCreateInfo.pQueuePriorities = queuePriorities; queueFamilyIndex_ = queueCreateInfo.queueFamilyIndex; - const std::vector enabledExtensions = { + std::vector enabledExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, #if !defined(NDEBUG) // VK_EXT_DEBUG_MARKER_EXTENSION_NAME, #endif }; +#if defined(VK_KHR_MAINTENANCE1_EXTENSION_NAME) + const auto deviceExtensions = vkPhysicalDevice.enumerateDeviceExtensionProperties(); + if (std::any_of(deviceExtensions.begin(), deviceExtensions.end(), [](const vk::ExtensionProperties& extension) { + return std::strcmp(extension.extensionName, VK_KHR_MAINTENANCE1_EXTENSION_NAME) == 0; + })) + { + enabledExtensions.push_back(VK_KHR_MAINTENANCE1_EXTENSION_NAME); + } +#endif vk::DeviceCreateInfo deviceCreateInfo; deviceCreateInfo.queueCreateInfoCount = 1; deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo; @@ -639,19 +652,6 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) deviceCreateInfo.enabledExtensionCount = static_cast(enabledExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = enabledExtensions.data(); -#if !defined(NDEBUG) - if (optimalLayers.size() > 0) - { - deviceCreateInfo.enabledLayerCount = static_cast(optimalLayers.size()); - deviceCreateInfo.ppEnabledLayerNames = optimalLayers.data(); - } - else - { - deviceCreateInfo.enabledLayerCount = 0; - } -#else - deviceCreateInfo.enabledLayerCount = 0; -#endif vkDevice_ = vkPhysicalDevice.createDevice(deviceCreateInfo); #if !defined(NDEBUG) @@ -712,8 +712,6 @@ bool PlatformVulkan::Initialize(Window* window, bool waitVSync) vkPresentComplete_ = vkDevice_.createSemaphore(semaphoreCreateInfo); - vkRenderComplete_ = vkDevice_.createSemaphore(semaphoreCreateInfo); - // create command buffer vk::CommandBufferAllocateInfo allocInfo; allocInfo.commandPool = vkCmdPool_; @@ -821,7 +819,7 @@ void PlatformVulkan::Present() // set a semaphore which notify to finish to execute commands submitInfo.signalSemaphoreCount = 1; - submitInfo.pSignalSemaphores = &vkRenderComplete_; + submitInfo.pSignalSemaphores = &swapBuffers[frameIndex].renderComplete; vk::Fence fence = GetSubmitFence(true); vkQueue.submit(submitInfo, fence); @@ -832,7 +830,7 @@ void PlatformVulkan::Present() } } - const auto result = Present(vkRenderComplete_); + const auto result = Present(swapBuffers[frameIndex].renderComplete); // TODO optimize it if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) diff --git a/src/Vulkan/LLGI.PlatformVulkan.h b/src/Vulkan/LLGI.PlatformVulkan.h index abfbe2aa..3f62fb40 100644 --- a/src/Vulkan/LLGI.PlatformVulkan.h +++ b/src/Vulkan/LLGI.PlatformVulkan.h @@ -36,6 +36,7 @@ class PlatformVulkan : public Platform vk::Image image = nullptr; vk::ImageView view = nullptr; vk::Fence fence = nullptr; + vk::Semaphore renderComplete = nullptr; TextureVulkan* texture = nullptr; }; @@ -63,8 +64,6 @@ class PlatformVulkan : public Platform //! to check to finish present vk::Semaphore vkPresentComplete_; - //! to check to finish render - vk::Semaphore vkRenderComplete_; std::vector vkCmdBuffers; vk::SurfaceKHR surface_ = nullptr; diff --git a/src/WebGPU/LLGI.CommandListWebGPU.cpp b/src/WebGPU/LLGI.CommandListWebGPU.cpp index ef95da36..1d98b25c 100644 --- a/src/WebGPU/LLGI.CommandListWebGPU.cpp +++ b/src/WebGPU/LLGI.CommandListWebGPU.cpp @@ -25,6 +25,73 @@ bool NeedsTextureSampler(TextureWebGPU* texture) } } // namespace +bool CommandListWebGPU::Equals(const std::vector& lhs, const std::vector& rhs) +{ + if (lhs.size() != rhs.size()) + { + return false; + } + + for (size_t i = 0; i < lhs.size(); i++) + { + if (lhs[i].binding != rhs[i].binding || lhs[i].resource != rhs[i].resource || lhs[i].offset != rhs[i].offset || + lhs[i].size != rhs[i].size || lhs[i].wrapMode != rhs[i].wrapMode || lhs[i].minMagFilter != rhs[i].minMagFilter) + { + return false; + } + } + + return true; +} + +void CommandListWebGPU::ResetRenderBindGroupCaches() +{ + for (auto& cache : renderBindGroupCaches_) + { + cache.pipeline = nullptr; + cache.entries.clear(); + cache.bindGroup = nullptr; + } +} + +void CommandListWebGPU::ResetComputeBindGroupCaches() +{ + for (auto& cache : computeBindGroupCaches_) + { + cache.pipeline = nullptr; + cache.entries.clear(); + cache.bindGroup = nullptr; + } +} + +void CommandListWebGPU::SetRenderBindGroup( + uint32_t index, const void* pipeline, const std::vector& entries, const wgpu::BindGroupDescriptor& desc) +{ + auto& cache = renderBindGroupCaches_[index]; + if (cache.bindGroup == nullptr || cache.pipeline != pipeline || !Equals(cache.entries, entries)) + { + cache.pipeline = pipeline; + cache.entries = entries; + cache.bindGroup = device_.CreateBindGroup(&desc); + } + + renderPassEncorder_.SetBindGroup(index, cache.bindGroup); +} + +void CommandListWebGPU::SetComputeBindGroup( + uint32_t index, const void* pipeline, const std::vector& entries, const wgpu::BindGroupDescriptor& desc) +{ + auto& cache = computeBindGroupCaches_[index]; + if (cache.bindGroup == nullptr || cache.pipeline != pipeline || !Equals(cache.entries, entries)) + { + cache.pipeline = pipeline; + cache.entries = entries; + cache.bindGroup = device_.CreateBindGroup(&desc); + } + + computePassEncorder_.SetBindGroup(index, cache.bindGroup); +} + CommandListWebGPU::CommandListWebGPU(wgpu::Device device) : device_(device) { wgpu::TextureDescriptor fallbackTextureDesc{}; @@ -85,6 +152,8 @@ void CommandListWebGPU::Begin() { wgpu::CommandEncoderDescriptor desc = {}; commandEncorder_ = device_.CreateCommandEncoder(&desc); + ResetRenderBindGroupCaches(); + ResetComputeBindGroupCaches(); CommandList::Begin(); } @@ -106,6 +175,7 @@ void CommandListWebGPU::BeginRenderPass(RenderPass* renderPass) const auto& desc = rp->GetDescriptor(); renderPassEncorder_ = commandEncorder_.BeginRenderPass(&desc); + ResetRenderBindGroupCaches(); renderPassEncorder_.SetViewport(0.0f, 0.0f, static_cast(rp->GetScreenSize().X), static_cast(rp->GetScreenSize().Y), 0.0f, 1.0f); CommandList::BeginRenderPass(renderPass); @@ -130,6 +200,7 @@ void CommandListWebGPU::BeginComputePass() wgpu::ComputePassDescriptor desc{}; computePassEncorder_ = commandEncorder_.BeginComputePass(&desc); + ResetComputeBindGroupCaches(); } void CommandListWebGPU::EndComputePass() @@ -163,24 +234,25 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) auto ib = static_cast(bib.indexBuffer); auto pip = static_cast(bpip); - if (vb != nullptr) + if (vb != nullptr && isVBDirtied) { renderPassEncorder_.SetVertexBuffer(0, vb->GetBuffer(), bvb.offset, bvb.vertexBuffer->GetSize() - bvb.offset); } - if (ib != nullptr) + if (ib != nullptr && isIBDirtied) { const auto format = bib.stride == 2 ? wgpu::IndexFormat::Uint16 : wgpu::IndexFormat::Uint32; renderPassEncorder_.SetIndexBuffer(ib->GetBuffer(), format, bib.offset, ib->GetSize() - bib.offset); } - if (pip != nullptr) + if (pip != nullptr && isPipDirtied) { renderPassEncorder_.SetPipeline(pip->GetRenderPipeline()); renderPassEncorder_.SetStencilReference(pip->StencilRef); } std::vector constantBindGroupEntries; + std::vector constantBindGroupEntryKeys; for (size_t unit_ind = 0; unit_ind < constantBuffers_.size(); unit_ind++) { @@ -200,6 +272,8 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) entry.size = cb->GetAllocatedSize() - cb->GetOffset(); entry.offset = cb->GetOffset(); constantBindGroupEntries.push_back(entry); + constantBindGroupEntryKeys.push_back( + {static_cast(unit_ind), cb, static_cast(entry.offset), static_cast(entry.size), 0, 0}); } if (!constantBindGroupEntries.empty()) @@ -208,12 +282,13 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) constantBindGroupDesc.layout = pip->GetRenderPipeline().GetBindGroupLayout(0); constantBindGroupDesc.entries = constantBindGroupEntries.data(); constantBindGroupDesc.entryCount = constantBindGroupEntries.size(); - auto constantBindGroup = device_.CreateBindGroup(&constantBindGroupDesc); - renderPassEncorder_.SetBindGroup(0, constantBindGroup); + SetRenderBindGroup(0, pip, constantBindGroupEntryKeys, constantBindGroupDesc); } std::vector textureGroupEntries; std::vector samplerGroupEntries; + std::vector textureGroupEntryKeys; + std::vector samplerGroupEntryKeys; for (int unit_ind = 0; unit_ind < static_cast(currentTextures_.size()); unit_ind++) { @@ -228,6 +303,8 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) textureEntry.binding = unit_ind; textureEntry.textureView = texture != nullptr ? texture->GetTextureView() : fallbackTextureView_; textureGroupEntries.push_back(textureEntry); + textureGroupEntryKeys.push_back( + {static_cast(unit_ind), texture, 0, 0, static_cast(wm), static_cast(mm)}); wgpu::BindGroupEntry samplerEntry = {}; if (NeedsTextureSampler(texture)) @@ -239,6 +316,7 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) samplerEntry.binding = unit_ind; samplerEntry.sampler = samplers_[wm][mm]; samplerGroupEntries.push_back(samplerEntry); + samplerGroupEntryKeys.push_back({static_cast(unit_ind), nullptr, 0, 0, wm, mm}); } } @@ -260,6 +338,12 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) bufferEntry.offset = buffer->GetOffset(); bufferEntry.size = buffer->GetSize(); textureGroupEntries.push_back(bufferEntry); + textureGroupEntryKeys.push_back({static_cast(unit_ind), + buffer, + static_cast(bufferEntry.offset), + static_cast(bufferEntry.size), + 0, + 0}); } if (!textureGroupEntries.empty()) @@ -268,8 +352,7 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) textureBindGroupDesc.layout = pip->GetRenderPipeline().GetBindGroupLayout(1); textureBindGroupDesc.entries = textureGroupEntries.data(); textureBindGroupDesc.entryCount = textureGroupEntries.size(); - auto textureBindGroup = device_.CreateBindGroup(&textureBindGroupDesc); - renderPassEncorder_.SetBindGroup(1, textureBindGroup); + SetRenderBindGroup(1, pip, textureGroupEntryKeys, textureBindGroupDesc); } if (!samplerGroupEntries.empty()) @@ -278,8 +361,7 @@ void CommandListWebGPU::Draw(int32_t primitiveCount, int32_t instanceCount) samplerBindGroupDesc.layout = pip->GetRenderPipeline().GetBindGroupLayout(2); samplerBindGroupDesc.entries = samplerGroupEntries.data(); samplerBindGroupDesc.entryCount = samplerGroupEntries.size(); - auto samplerBindGroup = device_.CreateBindGroup(&samplerBindGroupDesc); - renderPassEncorder_.SetBindGroup(2, samplerBindGroup); + SetRenderBindGroup(2, pip, samplerGroupEntryKeys, samplerBindGroupDesc); } int indexPerPrim = 0; @@ -316,14 +398,20 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, return; } + bool isComputePassStarted = false; if (computePassEncorder_ == nullptr) { BeginComputePass(); + isComputePassStarted = true; } - computePassEncorder_.SetPipeline(pip->GetComputePipeline()); + if (isComputePassStarted || isPipDirtied) + { + computePassEncorder_.SetPipeline(pip->GetComputePipeline()); + } std::vector constantBindGroupEntries; + std::vector constantBindGroupEntryKeys; for (size_t unit_ind = 0; unit_ind < constantBuffers_.size(); unit_ind++) { auto cb = static_cast(constantBuffers_[unit_ind]); @@ -342,6 +430,8 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, entry.size = cb->GetAllocatedSize() - cb->GetOffset(); entry.offset = cb->GetOffset(); constantBindGroupEntries.push_back(entry); + constantBindGroupEntryKeys.push_back( + {static_cast(unit_ind), cb, static_cast(entry.offset), static_cast(entry.size), 0, 0}); } if (!constantBindGroupEntries.empty()) @@ -350,12 +440,13 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, desc.layout = pip->GetComputePipeline().GetBindGroupLayout(0); desc.entries = constantBindGroupEntries.data(); desc.entryCount = constantBindGroupEntries.size(); - auto bindGroup = device_.CreateBindGroup(&desc); - computePassEncorder_.SetBindGroup(0, bindGroup); + SetComputeBindGroup(0, pip, constantBindGroupEntryKeys, desc); } std::vector textureGroupEntries; std::vector samplerAndBufferGroupEntries; + std::vector textureGroupEntryKeys; + std::vector samplerAndBufferGroupEntryKeys; for (int unit_ind = 0; unit_ind < static_cast(currentTextures_.size()); unit_ind++) { @@ -373,6 +464,8 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, textureEntry.binding = unit_ind; textureEntry.textureView = texture != nullptr ? texture->GetTextureView() : fallbackTextureView_; textureGroupEntries.push_back(textureEntry); + textureGroupEntryKeys.push_back( + {static_cast(unit_ind), texture, 0, 0, static_cast(wm), static_cast(mm)}); if (NeedsTextureSampler(texture)) { @@ -384,6 +477,7 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, samplerEntry.binding = unit_ind; samplerEntry.sampler = samplers_[wm][mm]; samplerAndBufferGroupEntries.push_back(samplerEntry); + samplerAndBufferGroupEntryKeys.push_back({static_cast(unit_ind), nullptr, 0, 0, wm, mm}); } } @@ -405,6 +499,12 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, entry.offset = buffer->GetOffset(); entry.size = buffer->GetSize(); samplerAndBufferGroupEntries.push_back(entry); + samplerAndBufferGroupEntryKeys.push_back({static_cast(unit_ind), + buffer, + static_cast(entry.offset), + static_cast(entry.size), + 0, + 0}); } if (!textureGroupEntries.empty()) @@ -413,8 +513,7 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, desc.layout = pip->GetComputePipeline().GetBindGroupLayout(1); desc.entries = textureGroupEntries.data(); desc.entryCount = textureGroupEntries.size(); - auto bindGroup = device_.CreateBindGroup(&desc); - computePassEncorder_.SetBindGroup(1, bindGroup); + SetComputeBindGroup(1, pip, textureGroupEntryKeys, desc); } if (!samplerAndBufferGroupEntries.empty()) @@ -423,8 +522,7 @@ void CommandListWebGPU::Dispatch(int32_t groupX, int32_t groupY, int32_t groupZ, desc.layout = pip->GetComputePipeline().GetBindGroupLayout(2); desc.entries = samplerAndBufferGroupEntries.data(); desc.entryCount = samplerAndBufferGroupEntries.size(); - auto bindGroup = device_.CreateBindGroup(&desc); - computePassEncorder_.SetBindGroup(2, bindGroup); + SetComputeBindGroup(2, pip, samplerAndBufferGroupEntryKeys, desc); } computePassEncorder_.DispatchWorkgroups(groupX, groupY, groupZ); @@ -473,6 +571,25 @@ void CommandListWebGPU::CopyTexture( commandEncorder_.CopyTextureToTexture(&srcTexCopy, &dstTexCopy, &extend3d); } +void CommandListWebGPU::GenerateMipMap(Texture* src) +{ + if (isInRenderPass_) + { + Log(LogType::Error, "Please call GenerateMipMap outside of RenderPass"); + return; + } + + EndComputePass(); + + auto srcTex = static_cast(src); + if (srcTex == nullptr) + { + return; + } + + srcTex->GenerateMipMaps(commandEncorder_); +} + void CommandListWebGPU::CopyBuffer(Buffer* src, Buffer* dst) { auto srcBuffer = static_cast(src); diff --git a/src/WebGPU/LLGI.CommandListWebGPU.h b/src/WebGPU/LLGI.CommandListWebGPU.h index d443e3cc..c76065ec 100644 --- a/src/WebGPU/LLGI.CommandListWebGPU.h +++ b/src/WebGPU/LLGI.CommandListWebGPU.h @@ -2,12 +2,31 @@ #include "../LLGI.CommandList.h" #include "LLGI.BaseWebGPU.h" +#include +#include namespace LLGI { class CommandListWebGPU : public CommandList { + struct BindGroupEntryKey + { + uint32_t binding = 0; + const void* resource = nullptr; + uint64_t offset = 0; + uint64_t size = 0; + int32_t wrapMode = 0; + int32_t minMagFilter = 0; + }; + + struct BindGroupCache + { + const void* pipeline = nullptr; + std::vector entries; + wgpu::BindGroup bindGroup = nullptr; + }; + wgpu::Device device_; wgpu::CommandBuffer commandBuffer_; wgpu::CommandEncoder commandEncorder_; @@ -16,6 +35,16 @@ class CommandListWebGPU : public CommandList wgpu::Sampler samplers_[3][2]; wgpu::Texture fallbackTexture_; wgpu::TextureView fallbackTextureView_; + std::array renderBindGroupCaches_; + std::array computeBindGroupCaches_; + + static bool Equals(const std::vector& lhs, const std::vector& rhs); + void ResetRenderBindGroupCaches(); + void ResetComputeBindGroupCaches(); + void SetRenderBindGroup( + uint32_t index, const void* pipeline, const std::vector& entries, const wgpu::BindGroupDescriptor& desc); + void SetComputeBindGroup( + uint32_t index, const void* pipeline, const std::vector& entries, const wgpu::BindGroupDescriptor& desc); public: CommandListWebGPU(wgpu::Device device); @@ -43,6 +72,8 @@ class CommandListWebGPU : public CommandList void CopyTexture( Texture* src, Texture* dst, const Vec3I& srcPos, const Vec3I& dstPos, const Vec3I& size, int srcLayer, int dstLayer) override; + void GenerateMipMap(Texture* src) override; + void CopyBuffer(Buffer* src, Buffer* dst) override; void WaitUntilCompleted() override; diff --git a/src/WebGPU/LLGI.GraphicsWebGPU.cpp b/src/WebGPU/LLGI.GraphicsWebGPU.cpp index 5efbf08d..f8289a51 100644 --- a/src/WebGPU/LLGI.GraphicsWebGPU.cpp +++ b/src/WebGPU/LLGI.GraphicsWebGPU.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #if defined(__EMSCRIPTEN__) @@ -66,6 +67,26 @@ void WaitForQueue(wgpu::Queue& queue) Log(LogType::Warning, "Timed out or failed while waiting for WebGPU queue completion."); } } +#else +void WaitForQueue(wgpu::Instance& instance, wgpu::Queue& queue) +{ + if (instance == nullptr) + { + return; + } + + bool succeeded = false; + auto future = queue.OnSubmittedWorkDone(wgpu::CallbackMode::WaitAnyOnly, + [&succeeded](wgpu::QueueWorkDoneStatus status, wgpu::StringView) { + succeeded = status == wgpu::QueueWorkDoneStatus::Success; + }); + instance.WaitAny(future, std::numeric_limits::max()); + + if (!succeeded) + { + Log(LogType::Warning, "Failed while waiting for WebGPU queue completion."); + } +} #endif } // namespace @@ -124,7 +145,11 @@ void GraphicsWebGPU::WaitFinish() WaitForQueue(queue_); } #else - if (device_ != nullptr) + if (queue_ != nullptr && instance_ != nullptr) + { + WaitForQueue(instance_, queue_); + } + else if (device_ != nullptr) { device_.Tick(); } @@ -312,7 +337,6 @@ std::vector GraphicsWebGPU::CaptureRenderTarget(Texture* renderTarget) auto commandBuffer = encoder.Finish(); queue_.Submit(1, &commandBuffer); - WaitFinish(); bool completed = false; bool succeeded = false; diff --git a/src/WebGPU/LLGI.TextureWebGPU.cpp b/src/WebGPU/LLGI.TextureWebGPU.cpp index 88af6e61..d79f0ba0 100644 --- a/src/WebGPU/LLGI.TextureWebGPU.cpp +++ b/src/WebGPU/LLGI.TextureWebGPU.cpp @@ -19,57 +19,184 @@ uint32_t AlignTo(uint32_t value, uint32_t alignment) return (value + alignment - 1) / alignment * alignment; } -int32_t GetMipmapPixelSize(TextureFormatType format) +const char* MipmapShaderWGSL = R"( +struct VSOutput { + @builtin(position) position : vec4f, + @location(0) uv : vec2f, +}; + +@vertex +fn VSMain(@builtin(vertex_index) vertexId : u32) -> VSOutput { + var positions = array( + vec2f(-1.0, -1.0), + vec2f(-1.0, 3.0), + vec2f(3.0, -1.0)); + var uvs = array( + vec2f(0.0, 1.0), + vec2f(0.0, -1.0), + vec2f(2.0, 1.0)); + + var output : VSOutput; + output.position = vec4f(positions[vertexId], 0.0, 1.0); + output.uv = uvs[vertexId]; + return output; +} + +@group(0) @binding(0) var srcSampler : sampler; +@group(0) @binding(1) var srcTexture : texture_2d; + +@fragment +fn PSMain(input : VSOutput) -> @location(0) vec4f { + return textureSample(srcTexture, srcSampler, input.uv); +} +)"; + +struct MipmapSize +{ + uint32_t Width = 1; + uint32_t Height = 1; +}; + +bool CanGenerateMipMaps(const TextureParameter& parameter) +{ + return parameter.MipLevelCount > 1 && parameter.Dimension == 2 && parameter.Size.Z == 1 && !IsDepthFormat(parameter.Format) && + parameter.SampleCount == 1; +} + +MipmapSize GetMipmapSize(const TextureParameter& parameter, uint32_t mipLevel) +{ + MipmapSize size; + size.Width = std::max(static_cast(parameter.Size.X) >> mipLevel, 1); + size.Height = std::max(static_cast(parameter.Size.Y) >> mipLevel, 1); + return size; +} + +wgpu::TextureViewDescriptor CreateMipmapViewDesc(wgpu::TextureFormat format, uint32_t mipLevel) { - switch (format) + wgpu::TextureViewDescriptor desc{}; + desc.format = format; + desc.dimension = wgpu::TextureViewDimension::e2D; + desc.baseMipLevel = mipLevel; + desc.mipLevelCount = 1; + desc.baseArrayLayer = 0; + desc.arrayLayerCount = 1; + desc.aspect = wgpu::TextureAspect::All; + return desc; +} + +wgpu::RenderPassColorAttachment CreateMipmapColorAttachment(wgpu::TextureView view) +{ + wgpu::RenderPassColorAttachment attachment{}; + attachment.view = view; + attachment.loadOp = wgpu::LoadOp::Clear; + attachment.storeOp = wgpu::StoreOp::Store; + attachment.clearValue = {0, 0, 0, 0}; + return attachment; +} + +} // namespace + +bool TextureWebGPU::CreateMipmapResources() +{ + if (mipmapShaderModule_ != nullptr) + { + return true; + } + + wgpu::BindGroupLayoutEntry entries[2]{}; + entries[0].binding = 0; + entries[0].visibility = wgpu::ShaderStage::Fragment; + entries[0].sampler.type = wgpu::SamplerBindingType::Filtering; + entries[1].binding = 1; + entries[1].visibility = wgpu::ShaderStage::Fragment; + entries[1].texture.sampleType = wgpu::TextureSampleType::Float; + entries[1].texture.viewDimension = wgpu::TextureViewDimension::e2D; + entries[1].texture.multisampled = false; + + wgpu::BindGroupLayoutDescriptor bindGroupLayoutDesc{}; + bindGroupLayoutDesc.entryCount = 2; + bindGroupLayoutDesc.entries = entries; + mipmapBindGroupLayout_ = device_.CreateBindGroupLayout(&bindGroupLayoutDesc); + if (mipmapBindGroupLayout_ == nullptr) + { + return false; + } + + wgpu::PipelineLayoutDescriptor pipelineLayoutDesc{}; + pipelineLayoutDesc.bindGroupLayoutCount = 1; + pipelineLayoutDesc.bindGroupLayouts = &mipmapBindGroupLayout_; + mipmapPipelineLayout_ = device_.CreatePipelineLayout(&pipelineLayoutDesc); + if (mipmapPipelineLayout_ == nullptr) { - case TextureFormatType::R8_UNORM: - return 1; - case TextureFormatType::R8G8B8A8_UNORM: - case TextureFormatType::B8G8R8A8_UNORM: - case TextureFormatType::R8G8B8A8_UNORM_SRGB: - case TextureFormatType::B8G8R8A8_UNORM_SRGB: - return 4; - default: - return 0; + return false; + } + + wgpu::SamplerDescriptor samplerDesc{}; + samplerDesc.addressModeU = wgpu::AddressMode::ClampToEdge; + samplerDesc.addressModeV = wgpu::AddressMode::ClampToEdge; + samplerDesc.addressModeW = wgpu::AddressMode::ClampToEdge; + samplerDesc.magFilter = wgpu::FilterMode::Linear; + samplerDesc.minFilter = wgpu::FilterMode::Linear; + samplerDesc.mipmapFilter = wgpu::MipmapFilterMode::Nearest; + samplerDesc.lodMinClamp = 0.0f; + samplerDesc.lodMaxClamp = 32.0f; + samplerDesc.maxAnisotropy = 1; + mipmapSampler_ = device_.CreateSampler(&samplerDesc); + if (mipmapSampler_ == nullptr) + { + return false; } + + wgpu::ShaderSourceWGSL wgslDesc{}; + wgslDesc.code = wgpu::StringView(MipmapShaderWGSL, strlen(MipmapShaderWGSL)); + wgpu::ShaderModuleDescriptor shaderDesc{}; + shaderDesc.nextInChain = reinterpret_cast(&wgslDesc); + mipmapShaderModule_ = device_.CreateShaderModule(&shaderDesc); + return mipmapShaderModule_ != nullptr; } -std::vector GenerateNextMipmap(const std::vector& src, int32_t srcWidth, int32_t srcHeight, int32_t pixelSize) +wgpu::RenderPipeline TextureWebGPU::GetMipmapPipeline(wgpu::TextureFormat format) { - const auto dstWidth = std::max(srcWidth / 2, 1); - const auto dstHeight = std::max(srcHeight / 2, 1); - std::vector dst(static_cast(dstWidth) * static_cast(dstHeight) * pixelSize); + auto found = mipmapPipelines_.find(format); + if (found != mipmapPipelines_.end()) + { + return found->second; + } - for (int32_t y = 0; y < dstHeight; y++) + if (!CreateMipmapResources()) { - for (int32_t x = 0; x < dstWidth; x++) - { - for (int32_t c = 0; c < pixelSize; c++) - { - int32_t sum = 0; - int32_t count = 0; - for (int32_t oy = 0; oy < 2; oy++) - { - const auto sy = std::min(y * 2 + oy, srcHeight - 1); - for (int32_t ox = 0; ox < 2; ox++) - { - const auto sx = std::min(x * 2 + ox, srcWidth - 1); - const auto srcIndex = (static_cast(sy) * srcWidth + sx) * pixelSize + c; - sum += src[srcIndex]; - count++; - } - } - - const auto dstIndex = (static_cast(y) * dstWidth + x) * pixelSize + c; - dst[dstIndex] = static_cast((sum + count / 2) / count); - } - } + return nullptr; } - return dst; + wgpu::ColorTargetState colorTargetState{}; + colorTargetState.format = format; + colorTargetState.writeMask = wgpu::ColorWriteMask::All; + + wgpu::FragmentState fragmentState{}; + fragmentState.module = mipmapShaderModule_; + fragmentState.entryPoint = "PSMain"; + fragmentState.targetCount = 1; + fragmentState.targets = &colorTargetState; + + wgpu::RenderPipelineDescriptor desc{}; + desc.layout = mipmapPipelineLayout_; + desc.vertex.module = mipmapShaderModule_; + desc.vertex.entryPoint = "VSMain"; + desc.primitive.topology = wgpu::PrimitiveTopology::TriangleList; + desc.primitive.frontFace = wgpu::FrontFace::CW; + desc.primitive.cullMode = wgpu::CullMode::None; + desc.fragment = &fragmentState; + desc.multisample.count = 1; + desc.multisample.mask = UINT32_MAX; + desc.multisample.alphaToCoverageEnabled = false; + + auto pipeline = device_.CreateRenderPipeline(&desc); + if (pipeline != nullptr) + { + mipmapPipelines_[format] = pipeline; + } + return pipeline; } -} // namespace bool TextureWebGPU::Initialize(wgpu::Device& device, const TextureParameter& parameter, wgpu::Instance instance) { @@ -114,6 +241,11 @@ bool TextureWebGPU::Initialize(wgpu::Device& device, const TextureParameter& par texDesc.usage |= wgpu::TextureUsage::RenderAttachment; } + if (CanGenerateMipMaps(parameter)) + { + texDesc.usage |= wgpu::TextureUsage::RenderAttachment; + } + if (BitwiseContains(parameter.Usage, TextureUsageType::Storage)) { texDesc.usage |= wgpu::TextureUsage::StorageBinding; @@ -226,44 +358,53 @@ void TextureWebGPU::Unlock() device_.GetQueue().WriteTexture(&imageCopyTexture, temp_buffer_.data(), temp_buffer_.size(), &textureDataLayout, &extent); } -void TextureWebGPU::GenerateMipMaps() +void TextureWebGPU::GenerateMipMaps(wgpu::CommandEncoder& commandEncoder) { - if (parameter_.MipLevelCount <= 1 || parameter_.Dimension != 2 || parameter_.Size.Z != 1) + if (!CanGenerateMipMaps(parameter_)) { return; } - const auto pixelSize = GetMipmapPixelSize(format_); - if (pixelSize == 0 || temp_buffer_.empty()) + const auto format = ConvertFormat(parameter_.Format); + auto pipeline = GetMipmapPipeline(format); + if (pipeline == nullptr) { return; } - auto srcData = temp_buffer_; - auto srcWidth = parameter_.Size.X; - auto srcHeight = parameter_.Size.Y; - for (uint32_t mipLevel = 1; mipLevel < static_cast(parameter_.MipLevelCount); mipLevel++) { - auto mipData = GenerateNextMipmap(srcData, srcWidth, srcHeight, pixelSize); - srcWidth = std::max(srcWidth / 2, 1); - srcHeight = std::max(srcHeight / 2, 1); - - wgpu::TexelCopyTextureInfo imageCopyTexture{}; - imageCopyTexture.texture = texture_; - imageCopyTexture.mipLevel = mipLevel; - imageCopyTexture.aspect = wgpu::TextureAspect::All; - - wgpu::TexelCopyBufferLayout textureDataLayout{}; - textureDataLayout.bytesPerRow = static_cast(srcWidth * pixelSize); - - wgpu::Extent3D extent{}; - extent.width = static_cast(srcWidth); - extent.height = static_cast(srcHeight); - extent.depthOrArrayLayers = 1; - device_.GetQueue().WriteTexture(&imageCopyTexture, mipData.data(), mipData.size(), &textureDataLayout, &extent); - - srcData = std::move(mipData); + const auto srcViewDesc = CreateMipmapViewDesc(format, mipLevel - 1); + auto srcView = texture_.CreateView(&srcViewDesc); + + const auto dstViewDesc = CreateMipmapViewDesc(format, mipLevel); + auto dstView = texture_.CreateView(&dstViewDesc); + + wgpu::BindGroupEntry bindGroupEntries[2]{}; + bindGroupEntries[0].binding = 0; + bindGroupEntries[0].sampler = mipmapSampler_; + bindGroupEntries[1].binding = 1; + bindGroupEntries[1].textureView = srcView; + + wgpu::BindGroupDescriptor bindGroupDesc{}; + bindGroupDesc.layout = mipmapBindGroupLayout_; + bindGroupDesc.entryCount = 2; + bindGroupDesc.entries = bindGroupEntries; + auto bindGroup = device_.CreateBindGroup(&bindGroupDesc); + + auto colorAttachment = CreateMipmapColorAttachment(dstView); + + wgpu::RenderPassDescriptor renderPassDesc{}; + renderPassDesc.colorAttachmentCount = 1; + renderPassDesc.colorAttachments = &colorAttachment; + + auto passEncoder = commandEncoder.BeginRenderPass(&renderPassDesc); + const auto dstSize = GetMipmapSize(parameter_, mipLevel); + passEncoder.SetViewport(0.0f, 0.0f, static_cast(dstSize.Width), static_cast(dstSize.Height), 0.0f, 1.0f); + passEncoder.SetPipeline(pipeline); + passEncoder.SetBindGroup(0, bindGroup); + passEncoder.Draw(3); + passEncoder.End(); } } diff --git a/src/WebGPU/LLGI.TextureWebGPU.h b/src/WebGPU/LLGI.TextureWebGPU.h index 54f19c9a..6f5fda3a 100644 --- a/src/WebGPU/LLGI.TextureWebGPU.h +++ b/src/WebGPU/LLGI.TextureWebGPU.h @@ -3,6 +3,7 @@ #include "../LLGI.Graphics.h" #include "../LLGI.Texture.h" #include "LLGI.BaseWebGPU.h" +#include namespace LLGI { @@ -14,15 +15,23 @@ class TextureWebGPU : public Texture wgpu::Texture texture_; wgpu::TextureView textureView_; + wgpu::BindGroupLayout mipmapBindGroupLayout_; + wgpu::PipelineLayout mipmapPipelineLayout_; + wgpu::Sampler mipmapSampler_; + wgpu::ShaderModule mipmapShaderModule_; + std::unordered_map mipmapPipelines_; TextureParameter parameter_; std::vector temp_buffer_; + bool CreateMipmapResources(); + wgpu::RenderPipeline GetMipmapPipeline(wgpu::TextureFormat format); + public: bool Initialize(wgpu::Device& device, const TextureParameter& parameter, wgpu::Instance instance = nullptr); bool InitializeFromSurfaceTexture(wgpu::Device& device, wgpu::Texture texture, const TextureParameter& parameter); void* Lock() override; void Unlock() override; - void GenerateMipMaps() override; + void GenerateMipMaps(wgpu::CommandEncoder& commandEncoder); bool GetData(std::vector& data) override; Vec2I GetSizeAs2D() const override; bool IsRenderTexture() const override; diff --git a/src_test/test_compute_shader.cpp b/src_test/test_compute_shader.cpp index 31a8a019..7f12432f 100644 --- a/src_test/test_compute_shader.cpp +++ b/src_test/test_compute_shader.cpp @@ -158,7 +158,7 @@ void test_compute_shader_texture(LLGI::DeviceType deviceType) pip->SetShader(LLGI::ShaderStageType::Compute, shader_cs.get()); if (!pip->Compile()) { - std::cout << "Failed : Compile" << std::endl; + std::cout << "Failed : Compile" << std::endl; abort(); } @@ -196,10 +196,7 @@ void test_compute_shader_texture(LLGI::DeviceType deviceType) LLGI::TextureParameter texParamWrite; texParamWrite.Size = {1, 1, 1}; texParamWrite.Usage = LLGI::TextureUsageType::Storage; - if (deviceType == LLGI::DeviceType::WebGPU) - { - texParamWrite.Format = LLGI::TextureFormatType::R32G32B32A32_FLOAT; - } + texParamWrite.Format = LLGI::TextureFormatType::R32G32B32A32_FLOAT; auto texWrite = LLGI::CreateSharedPtr(graphics->CreateTexture(texParamWrite)); if (!platform->NewFrame()) @@ -224,21 +221,10 @@ void test_compute_shader_texture(LLGI::DeviceType deviceType) std::vector result; if (texWrite->GetData(result)) { - if (deviceType == LLGI::DeviceType::WebGPU) - { - const auto p = reinterpret_cast(result.data()); - if (!(p[0] == 0.5f && p[1] == 0.25f && p[2] == 0.25f && p[3] == 0.5f)) - { - std::cout << "Failed : Mismatch" << p[0] << "," << p[1] << "," << p[2] << "," << p[3] << std::endl; - abort(); - } - } - else if (!(result[0] == 128 && result[1] == 64 && result[2] == 64 && result[3] == 128)) + const auto p = reinterpret_cast(result.data()); + if (!(p[0] == 0.5f && p[1] == 0.25f && p[2] == 0.25f && p[3] == 0.5f)) { - std::cout << "Failed : Mismatch" << static_cast(result[0]) << "," - << static_cast(result[1]) << "," - << static_cast(result[2]) << "," - << static_cast(result[3]) << std::endl; + std::cout << "Failed : Mismatch" << p[0] << "," << p[1] << "," << p[2] << "," << p[3] << std::endl; abort(); } } diff --git a/tools/DX12MipmapShaderCompiler/CMakeLists.txt b/tools/DX12MipmapShaderCompiler/CMakeLists.txt new file mode 100644 index 00000000..e76c8c1e --- /dev/null +++ b/tools/DX12MipmapShaderCompiler/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(LLGI_DX12MipmapShaderCompiler main.cpp) +target_link_libraries(LLGI_DX12MipmapShaderCompiler PRIVATE d3dcompiler) + +if(MSVC) + target_compile_options(LLGI_DX12MipmapShaderCompiler PRIVATE /W4 /WX /wd4100) +endif() diff --git a/tools/DX12MipmapShaderCompiler/main.cpp b/tools/DX12MipmapShaderCompiler/main.cpp new file mode 100644 index 00000000..dd117b35 --- /dev/null +++ b/tools/DX12MipmapShaderCompiler/main.cpp @@ -0,0 +1,132 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +const char* shader = R"( +Texture2D SrcTexture : register(t0); +SamplerState SrcSampler : register(s0); + +struct VSOutput +{ + float4 Position : SV_Position; + float2 UV : TEXCOORD0; +}; + +VSOutput VSMain(uint vertexId : SV_VertexID) +{ + float2 positions[3] = { float2(-1.0, -1.0), float2(-1.0, 3.0), float2(3.0, -1.0) }; + float2 uvs[3] = { float2(0.0, 1.0), float2(0.0, -1.0), float2(2.0, 1.0) }; + VSOutput output; + output.Position = float4(positions[vertexId], 0.0, 1.0); + output.UV = uvs[vertexId]; + return output; +} + +float4 PSMain(VSOutput input) : SV_Target +{ + return SrcTexture.Sample(SrcSampler, input.UV); +} +)"; + +void SafeRelease(IUnknown* object) +{ + if (object != nullptr) + { + object->Release(); + } +} + +bool Compile(std::vector& binary, const char* entryPoint, const char* target) +{ + ID3DBlob* shaderBlob = nullptr; + ID3DBlob* errorBlob = nullptr; + const auto hr = D3DCompile(shader, strlen(shader), nullptr, nullptr, nullptr, entryPoint, target, 0, 0, &shaderBlob, &errorBlob); + if (FAILED(hr)) + { + if (errorBlob != nullptr) + { + std::fprintf(stderr, "%s\n", static_cast(errorBlob->GetBufferPointer())); + } + SafeRelease(shaderBlob); + SafeRelease(errorBlob); + return false; + } + + const auto* data = static_cast(shaderBlob->GetBufferPointer()); + binary.assign(data, data + shaderBlob->GetBufferSize()); + SafeRelease(shaderBlob); + SafeRelease(errorBlob); + return true; +} + +void WriteArray(std::ofstream& ofs, const char* name, const std::vector& binary) +{ + ofs << "static const uint8_t " << name << "[] = {\n"; + for (size_t i = 0; i < binary.size(); i++) + { + if (i % 12 == 0) + { + ofs << "\t"; + } + + ofs << "0x" << std::hex << std::setw(2) << std::setfill('0') << static_cast(binary[i]) << std::dec; + if (i + 1 < binary.size()) + { + ofs << ", "; + } + + if (i % 12 == 11 || i + 1 == binary.size()) + { + ofs << "\n"; + } + } + ofs << "};\n"; + ofs << "static const size_t " << name << "Size = sizeof(" << name << ");\n\n"; +} + +} // namespace + +int main(int argc, char* argv[]) +{ + if (argc != 2) + { + std::fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + std::vector vertexShader; + std::vector pixelShader; + if (!Compile(vertexShader, "VSMain", "vs_5_0") || !Compile(pixelShader, "PSMain", "ps_5_0")) + { + return 1; + } + + std::ofstream ofs(argv[1], std::ios::binary); + if (!ofs) + { + std::fprintf(stderr, "Failed to open %s\n", argv[1]); + return 1; + } + + ofs << "#pragma once\n\n"; + ofs << "#include \n"; + ofs << "#include \n\n"; + ofs << "namespace LLGI\n"; + ofs << "{\n"; + ofs << "namespace DX12MipmapShader\n"; + ofs << "{\n\n"; + WriteArray(ofs, "VertexShader", vertexShader); + WriteArray(ofs, "PixelShader", pixelShader); + ofs << "} // namespace DX12MipmapShader\n"; + ofs << "} // namespace LLGI\n"; + + return 0; +} From 1460c9032b85b399634cb0bc9c3204662369bfb7 Mon Sep 17 00:00:00 2001 From: durswd Date: Thu, 7 May 2026 21:12:09 +0900 Subject: [PATCH 16/16] fix --- src/Vulkan/LLGI.BaseVulkan.cpp | 24 ++++++- src/Vulkan/LLGI.GraphicsVulkan.h | 2 + src/Vulkan/LLGI.PipelineStateVulkan.cpp | 12 ++-- ...LGI.RenderPassPipelineStateCacheVulkan.cpp | 69 +++++++++++++------ src/Vulkan/LLGI.RenderPassVulkan.cpp | 6 ++ src/Vulkan/LLGI.TextureVulkan.cpp | 3 +- src/WebGPU/LLGI.GraphicsWebGPU.h | 2 + src/WebGPU/LLGI.PipelineStateWebGPU.cpp | 17 +++-- src/WebGPU/LLGI.RenderPassWebGPU.cpp | 6 ++ src/WebGPU/LLGI.ShaderWebGPU.cpp | 7 ++ src/WebGPU/LLGI.ShaderWebGPU.h | 1 + .../simple_depth_texture_rectangle.frag | 11 +++ src_test/test_depth_stencil.cpp | 13 +++- 13 files changed, 138 insertions(+), 35 deletions(-) create mode 100644 src_test/Shaders/WebGPU/simple_depth_texture_rectangle.frag diff --git a/src/Vulkan/LLGI.BaseVulkan.cpp b/src/Vulkan/LLGI.BaseVulkan.cpp index 8e888592..9dda96bb 100644 --- a/src/Vulkan/LLGI.BaseVulkan.cpp +++ b/src/Vulkan/LLGI.BaseVulkan.cpp @@ -205,6 +205,15 @@ static vk::PipelineStageFlags GetStageFlag(vk::ImageLayout layout) { return vk::PipelineStageFlagBits::eColorAttachmentOutput; } + else if (layout == vk::ImageLayout::eDepthStencilAttachmentOptimal) + { + return vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests; + } + else if (layout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) + { + return vk::PipelineStageFlagBits::eFragmentShader | vk::PipelineStageFlagBits::eEarlyFragmentTests | + vk::PipelineStageFlagBits::eLateFragmentTests; + } else if (layout == vk::ImageLayout::eShaderReadOnlyOptimal) { return vk::PipelineStageFlagBits::eVertexShader | vk::PipelineStageFlagBits::eFragmentShader; @@ -242,6 +251,10 @@ void SetImageLayout(vk::CommandBuffer cmdbuffer, { imageMemoryBarrier.srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentWrite; } + else if (oldImageLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) + { + imageMemoryBarrier.srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eShaderRead; + } else if (oldImageLayout == vk::ImageLayout::eTransferSrcOptimal) { imageMemoryBarrier.srcAccessMask = vk::AccessFlagBits::eTransferRead; @@ -271,7 +284,12 @@ void SetImageLayout(vk::CommandBuffer cmdbuffer, } else if (newImageLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { - imageMemoryBarrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentWrite; + imageMemoryBarrier.dstAccessMask = + vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; + } + else if (newImageLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) + { + imageMemoryBarrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eShaderRead; } else if (newImageLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { @@ -293,6 +311,10 @@ void SetImageLayout(vk::CommandBuffer cmdbuffer, imageMemoryBarrier.dstAccessMask == vk::AccessFlagBits::eShaderRead || imageMemoryBarrier.srcAccessMask == vk::AccessFlagBits::eColorAttachmentWrite || imageMemoryBarrier.srcAccessMask == vk::AccessFlagBits::eTransferRead || + imageMemoryBarrier.srcAccessMask == vk::AccessFlagBits::eDepthStencilAttachmentWrite || + imageMemoryBarrier.dstAccessMask == (vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eShaderRead) || + imageMemoryBarrier.dstAccessMask == + (vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite) || imageMemoryBarrier.dstAccessMask == (vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite)) { cmdbuffer.pipelineBarrier( diff --git a/src/Vulkan/LLGI.GraphicsVulkan.h b/src/Vulkan/LLGI.GraphicsVulkan.h index 83e078f9..cdacbaf0 100644 --- a/src/Vulkan/LLGI.GraphicsVulkan.h +++ b/src/Vulkan/LLGI.GraphicsVulkan.h @@ -82,6 +82,8 @@ class GraphicsVulkan : public Graphics Query* CreateQuery(QueryType queryType, int32_t queryCount) override; uint64_t TimestampToMicroseconds(uint64_t timestamp) const override; + + bool IsResolvedDepthSupported() const override { return false; } }; } // namespace LLGI diff --git a/src/Vulkan/LLGI.PipelineStateVulkan.cpp b/src/Vulkan/LLGI.PipelineStateVulkan.cpp index 1ce59beb..26729e96 100644 --- a/src/Vulkan/LLGI.PipelineStateVulkan.cpp +++ b/src/Vulkan/LLGI.PipelineStateVulkan.cpp @@ -290,10 +290,12 @@ bool PipelineStateVulkan::CreateGraphicsPipeline() // setup a depthstencil vk::PipelineDepthStencilStateCreateInfo depthStencilInfo; - // DepthTest flag must be enabled because DepthWrite and Stencil are depended on DepthTestFlag - depthStencilInfo.depthTestEnable = true; + const bool hasDepth = renderPassPipelineState->Key.DepthFormat != TextureFormatType::Unknown; + const bool hasStencil = HasStencil(renderPassPipelineState->Key.DepthFormat); - depthStencilInfo.depthWriteEnable = IsDepthWriteEnabled; + depthStencilInfo.depthTestEnable = hasDepth && (IsDepthTestEnabled || IsDepthWriteEnabled); + + depthStencilInfo.depthWriteEnable = hasDepth && IsDepthWriteEnabled; std::array compareOps; compareOps[static_cast(DepthFuncType::Never)] = vk::CompareOp::eNever; @@ -313,7 +315,7 @@ bool PipelineStateVulkan::CreateGraphicsPipeline() } vk::StencilOpState stencil; - depthStencilInfo.stencilTestEnable = true; + depthStencilInfo.stencilTestEnable = hasStencil && IsStencilTestEnabled; /* enum class StencilOperatorType @@ -339,7 +341,7 @@ bool PipelineStateVulkan::CreateGraphicsPipeline() stencilOps[static_cast(StencilOperatorType::IncRepeat)] = vk::StencilOp::eIncrementAndWrap; stencilOps[static_cast(StencilOperatorType::DecRepeat)] = vk::StencilOp::eDecrementAndWrap; - if (IsStencilTestEnabled) + if (depthStencilInfo.stencilTestEnable) { stencil.depthFailOp = stencilOps[static_cast(StencilDepthFailOp)]; stencil.failOp = stencilOps[static_cast(StencilFailOp)]; diff --git a/src/Vulkan/LLGI.RenderPassPipelineStateCacheVulkan.cpp b/src/Vulkan/LLGI.RenderPassPipelineStateCacheVulkan.cpp index 12754c80..7cacdd24 100644 --- a/src/Vulkan/LLGI.RenderPassPipelineStateCacheVulkan.cpp +++ b/src/Vulkan/LLGI.RenderPassPipelineStateCacheVulkan.cpp @@ -97,14 +97,13 @@ RenderPassPipelineStateVulkan* RenderPassPipelineStateCacheVulkan::Create(const attachmentDescs.at(colorCount).stencilLoadOp = vk::AttachmentLoadOp::eDontCare; } - attachmentDescs.at(colorCount).storeOp = vk::AttachmentStoreOp::eDontCare; - attachmentDescs.at(colorCount).stencilStoreOp = vk::AttachmentStoreOp::eDontCare; + attachmentDescs.at(colorCount).storeOp = vk::AttachmentStoreOp::eStore; + attachmentDescs.at(colorCount).stencilStoreOp = HasStencil(key.DepthFormat) ? vk::AttachmentStoreOp::eStore : vk::AttachmentStoreOp::eDontCare; // When clearing, the initialLayout does not matter. attachmentDescs.at(colorCount).initialLayout = (key.IsDepthCleared) ? vk::ImageLayout::eUndefined : vk::ImageLayout::eDepthStencilAttachmentOptimal; - attachmentDescs.at(colorCount).finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; - // attachmentDescs.at(colorCount).finalLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; + attachmentDescs.at(colorCount).finalLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; } // resolve @@ -204,27 +203,55 @@ RenderPassPipelineStateVulkan* RenderPassPipelineStateCacheVulkan::Create(const } } - std::array dependencies; + std::array dependencies; + int32_t dependencyCount = 0; if (!key.IsPresent) { for (int i = 0; i < colorCount; i++) { - dependencies[i * 2 + 0].srcSubpass = VK_SUBPASS_EXTERNAL; - dependencies[i * 2 + 0].dstSubpass = 0; - dependencies[i * 2 + 0].srcStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - dependencies[i * 2 + 0].dstStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dependencies[i * 2 + 0].srcAccessMask = (vk::AccessFlags)VK_ACCESS_SHADER_READ_BIT; - dependencies[i * 2 + 0].dstAccessMask = (vk::AccessFlags)VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dependencies[i * 2 + 0].dependencyFlags = (vk::DependencyFlags)VK_DEPENDENCY_BY_REGION_BIT; - - dependencies[i * 2 + 1].srcSubpass = 0; - dependencies[i * 2 + 1].dstSubpass = VK_SUBPASS_EXTERNAL; - dependencies[i * 2 + 1].srcStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dependencies[i * 2 + 1].dstStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - dependencies[i * 2 + 1].srcAccessMask = (vk::AccessFlags)VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dependencies[i * 2 + 1].dstAccessMask = (vk::AccessFlags)VK_ACCESS_SHADER_READ_BIT; - dependencies[i * 2 + 1].dependencyFlags = (vk::DependencyFlags)VK_DEPENDENCY_BY_REGION_BIT; + auto& before = dependencies[dependencyCount++]; + before.srcSubpass = VK_SUBPASS_EXTERNAL; + before.dstSubpass = 0; + before.srcStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + before.dstStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + before.srcAccessMask = (vk::AccessFlags)VK_ACCESS_SHADER_READ_BIT; + before.dstAccessMask = (vk::AccessFlags)VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + before.dependencyFlags = (vk::DependencyFlags)VK_DEPENDENCY_BY_REGION_BIT; + + auto& after = dependencies[dependencyCount++]; + after.srcSubpass = 0; + after.dstSubpass = VK_SUBPASS_EXTERNAL; + after.srcStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + after.dstStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + after.srcAccessMask = (vk::AccessFlags)VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + after.dstAccessMask = (vk::AccessFlags)VK_ACCESS_SHADER_READ_BIT; + after.dependencyFlags = (vk::DependencyFlags)VK_DEPENDENCY_BY_REGION_BIT; + } + + if (hasDepth) + { + auto& before = dependencies[dependencyCount++]; + before.srcSubpass = VK_SUBPASS_EXTERNAL; + before.dstSubpass = 0; + before.srcStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + before.dstStageMask = + (vk::PipelineStageFlags)(VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT); + before.srcAccessMask = (vk::AccessFlags)VK_ACCESS_SHADER_READ_BIT; + before.dstAccessMask = + (vk::AccessFlags)(VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + before.dependencyFlags = (vk::DependencyFlags)VK_DEPENDENCY_BY_REGION_BIT; + + auto& after = dependencies[dependencyCount++]; + after.srcSubpass = 0; + after.dstSubpass = VK_SUBPASS_EXTERNAL; + after.srcStageMask = + (vk::PipelineStageFlags)(VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT); + after.dstStageMask = (vk::PipelineStageFlags)VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + after.srcAccessMask = + (vk::AccessFlags)(VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + after.dstAccessMask = (vk::AccessFlags)VK_ACCESS_SHADER_READ_BIT; + after.dependencyFlags = (vk::DependencyFlags)VK_DEPENDENCY_BY_REGION_BIT; } } @@ -238,7 +265,7 @@ RenderPassPipelineStateVulkan* RenderPassPipelineStateCacheVulkan::Create(const if (!key.IsPresent) { - renderPassInfo.dependencyCount = static_cast(colorCount * 2); + renderPassInfo.dependencyCount = static_cast(dependencyCount); renderPassInfo.pDependencies = dependencies.data(); } else diff --git a/src/Vulkan/LLGI.RenderPassVulkan.cpp b/src/Vulkan/LLGI.RenderPassVulkan.cpp index 1e668b7d..cc398fdd 100644 --- a/src/Vulkan/LLGI.RenderPassVulkan.cpp +++ b/src/Vulkan/LLGI.RenderPassVulkan.cpp @@ -40,6 +40,12 @@ bool RenderPassVulkan::Initialize(const TextureVulkan** textures, if (textureCount == 0) return false; + if (resolvedDepthTexture != nullptr) + { + Log(LogType::Error, "RenderPassVulkan : Resolved depth is not supported."); + return false; + } + if (!assignRenderTextures((Texture**)(textures), textureCount)) { return false; diff --git a/src/Vulkan/LLGI.TextureVulkan.cpp b/src/Vulkan/LLGI.TextureVulkan.cpp index 92cdf790..18d4d110 100644 --- a/src/Vulkan/LLGI.TextureVulkan.cpp +++ b/src/Vulkan/LLGI.TextureVulkan.cpp @@ -76,7 +76,8 @@ bool TextureVulkan::Initialize(GraphicsVulkan* graphics, if (IsDepthFormat(parameter.Format)) { - resourceUsage = resourceUsage | vk::ImageUsageFlagBits::eDepthStencilAttachment; + resourceUsage = + resourceUsage | vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; aspect = vk::ImageAspectFlagBits::eDepth; diff --git a/src/WebGPU/LLGI.GraphicsWebGPU.h b/src/WebGPU/LLGI.GraphicsWebGPU.h index 8f9b6403..42f3609e 100644 --- a/src/WebGPU/LLGI.GraphicsWebGPU.h +++ b/src/WebGPU/LLGI.GraphicsWebGPU.h @@ -64,6 +64,8 @@ class GraphicsWebGPU : public Graphics wgpu::Device& GetDevice() { return device_; } wgpu::Queue& GetQueue() { return queue_; } + + bool IsResolvedDepthSupported() const override { return false; } }; } // namespace LLGI diff --git a/src/WebGPU/LLGI.PipelineStateWebGPU.cpp b/src/WebGPU/LLGI.PipelineStateWebGPU.cpp index b7fe76df..f1b3f085 100644 --- a/src/WebGPU/LLGI.PipelineStateWebGPU.cpp +++ b/src/WebGPU/LLGI.PipelineStateWebGPU.cpp @@ -32,7 +32,7 @@ bool BuildBindGroupLayoutEntry(const ShaderBindingWebGPU& binding, wgpu::ShaderS entry.buffer.minBindingSize = 0; return true; case ShaderBindingResourceTypeWebGPU::Texture: - entry.texture.sampleType = wgpu::TextureSampleType::Float; + entry.texture.sampleType = binding.TextureSampleType; entry.texture.viewDimension = binding.TextureViewDimension; entry.texture.multisampled = false; return true; @@ -67,6 +67,12 @@ bool ContainsBinding(const std::vector& bindings, const Sha continue; } + if (binding.ResourceType == ShaderBindingResourceTypeWebGPU::Texture && + existingBinding.TextureSampleType != binding.TextureSampleType) + { + continue; + } + if (binding.ResourceType == ShaderBindingResourceTypeWebGPU::StorageTexture && (existingBinding.StorageTextureFormat != binding.StorageTextureFormat || existingBinding.StorageTextureAccess != binding.StorageTextureAccess)) @@ -283,8 +289,11 @@ bool PipelineStateWebGPU::Compile() desc.fragment = &fragmentState; + const bool hasDepth = renderPassPipelineState_->Key.DepthFormat != TextureFormatType::Unknown; + const bool hasStencil = HasStencil(renderPassPipelineState_->Key.DepthFormat); + wgpu::DepthStencilState depthStencilState = {}; - depthStencilState.depthWriteEnabled = IsDepthWriteEnabled; + depthStencilState.depthWriteEnabled = hasDepth && IsDepthWriteEnabled; if (IsDepthTestEnabled) { @@ -295,7 +304,7 @@ bool PipelineStateWebGPU::Compile() depthStencilState.depthCompare = wgpu::CompareFunction::Always; } - if (IsStencilTestEnabled) + if (hasStencil && IsStencilTestEnabled) { wgpu::StencilFaceState fs; @@ -326,7 +335,7 @@ bool PipelineStateWebGPU::Compile() depthStencilState.stencilReadMask = 0xff; } - if (renderPassPipelineState_->Key.DepthFormat != TextureFormatType::Unknown) + if (hasDepth) { depthStencilState.format = ConvertFormat(renderPassPipelineState_->Key.DepthFormat); desc.depthStencil = &depthStencilState; diff --git a/src/WebGPU/LLGI.RenderPassWebGPU.cpp b/src/WebGPU/LLGI.RenderPassWebGPU.cpp index 08b73e7a..1d1cc2ab 100644 --- a/src/WebGPU/LLGI.RenderPassWebGPU.cpp +++ b/src/WebGPU/LLGI.RenderPassWebGPU.cpp @@ -43,6 +43,12 @@ void RenderPassWebGPU::RefreshDescriptor() bool RenderPassWebGPU::Initialize( Texture** textures, int textureCount, Texture* depthTexture, Texture* resolvedRenderTexture, Texture* resolvedDepthTexture) { + if (resolvedDepthTexture != nullptr) + { + Log(LogType::Error, "RenderPassWebGPU : Resolved depth is not supported."); + return false; + } + if (!assignRenderTextures(textures, textureCount)) { return false; diff --git a/src/WebGPU/LLGI.ShaderWebGPU.cpp b/src/WebGPU/LLGI.ShaderWebGPU.cpp index 344f74f0..f21fcf92 100644 --- a/src/WebGPU/LLGI.ShaderWebGPU.cpp +++ b/src/WebGPU/LLGI.ShaderWebGPU.cpp @@ -144,6 +144,11 @@ bool IsSameBinding(const ShaderBindingWebGPU& lhs, const ShaderBindingWebGPU& rh return false; } + if (lhs.ResourceType == ShaderBindingResourceTypeWebGPU::Texture) + { + return lhs.TextureSampleType == rhs.TextureSampleType; + } + if (lhs.ResourceType == ShaderBindingResourceTypeWebGPU::StorageTexture) { return lhs.StorageTextureFormat == rhs.StorageTextureFormat && @@ -203,6 +208,8 @@ std::vector ReflectBindings(const std::string& code) { reflected.ResourceType = ShaderBindingResourceTypeWebGPU::Texture; reflected.TextureViewDimension = ParseTextureViewDimension(statement); + reflected.TextureSampleType = + statement.find(": texture_depth_") != std::string::npos ? wgpu::TextureSampleType::Depth : wgpu::TextureSampleType::Float; } else if (statement.find(": sampler") != std::string::npos) { diff --git a/src/WebGPU/LLGI.ShaderWebGPU.h b/src/WebGPU/LLGI.ShaderWebGPU.h index 42176e14..471970e8 100644 --- a/src/WebGPU/LLGI.ShaderWebGPU.h +++ b/src/WebGPU/LLGI.ShaderWebGPU.h @@ -24,6 +24,7 @@ struct ShaderBindingWebGPU uint32_t Binding = 0; ShaderBindingResourceTypeWebGPU ResourceType = ShaderBindingResourceTypeWebGPU::Unknown; wgpu::TextureViewDimension TextureViewDimension = wgpu::TextureViewDimension::e2D; + wgpu::TextureSampleType TextureSampleType = wgpu::TextureSampleType::Float; wgpu::TextureFormat StorageTextureFormat = wgpu::TextureFormat::Undefined; wgpu::StorageTextureAccess StorageTextureAccess = wgpu::StorageTextureAccess::Undefined; }; diff --git a/src_test/Shaders/WebGPU/simple_depth_texture_rectangle.frag b/src_test/Shaders/WebGPU/simple_depth_texture_rectangle.frag new file mode 100644 index 00000000..b5eb262d --- /dev/null +++ b/src_test/Shaders/WebGPU/simple_depth_texture_rectangle.frag @@ -0,0 +1,11 @@ +diagnostic(off, derivative_uniformity); + +@group(1) @binding(0) var txt : texture_depth_2d; + +@fragment +fn main(@builtin(position) position : vec4, @location(0u) uv : vec2, @location(1u) color : vec4) -> @location(0u) vec4 { + let size = textureDimensions(txt); + let coord = vec2(clamp(vec2(uv * vec2(size)), vec2(0u), size - vec2(1u))); + let depth = textureLoad(txt, coord, 0); + return vec4(depth, 0.0, 0.0, 1.0); +} diff --git a/src_test/test_depth_stencil.cpp b/src_test/test_depth_stencil.cpp index cd84158f..b559da3c 100644 --- a/src_test/test_depth_stencil.cpp +++ b/src_test/test_depth_stencil.cpp @@ -16,7 +16,8 @@ void test_depth_stencil(LLGI::DeviceType deviceType, DepthStencilTestMode mode) { if (mode == DepthStencilTestMode::DepthAsTexture) { - if (deviceType != LLGI::DeviceType::DirectX12 && deviceType != LLGI::DeviceType::Default) + if (deviceType != LLGI::DeviceType::DirectX12 && deviceType != LLGI::DeviceType::Vulkan && deviceType != LLGI::DeviceType::WebGPU && + deviceType != LLGI::DeviceType::Default) return; } @@ -42,8 +43,14 @@ void test_depth_stencil(LLGI::DeviceType deviceType, DepthStencilTestMode mode) std::shared_ptr shader_screen_vs = nullptr; std::shared_ptr shader_screen_ps = nullptr; - TestHelper::CreateShader( - graphics.get(), deviceType, "simple_texture_rectangle.vert", "simple_texture_rectangle.frag", shader_screen_vs, shader_screen_ps); + TestHelper::CreateShader(graphics.get(), + deviceType, + "simple_texture_rectangle.vert", + mode == DepthStencilTestMode::DepthAsTexture && deviceType == LLGI::DeviceType::WebGPU + ? "simple_depth_texture_rectangle.frag" + : "simple_texture_rectangle.frag", + shader_screen_vs, + shader_screen_ps); // Green: near std::shared_ptr vb1;