From 33ec54347df497c3f8c237e5c2b65590fb1807df Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 2 Apr 2026 23:01:14 -0600 Subject: [PATCH] Add offscreen rendering support & GXSetTexCopyDst (#89) * Add offscreen rendering support & GXSetTexCopyDst * Try to fix alignment for MSVC --- include/dolphin/gx/GXAurora.h | 15 ++ lib/dolphin/gx/GXAurora.cpp | 13 ++ lib/dolphin/gx/GXFrameBuffer.cpp | 54 +++--- lib/gfx/clear.cpp | 14 +- lib/gfx/clear.hpp | 17 +- lib/gfx/common.cpp | 288 ++++++++++++++++++++++++++----- lib/gfx/common.hpp | 13 +- lib/gfx/tex_copy_conv.cpp | 54 +++++- lib/gfx/tex_copy_conv.hpp | 6 +- lib/gx/gx.cpp | 5 +- lib/gx/gx.hpp | 3 +- lib/gx/gx_fmt.hpp | 2 + lib/gx/pipeline.hpp | 11 +- lib/logging.cpp | 3 +- lib/webgpu/gpu.cpp | 6 +- tests/gx_test_stubs.cpp | 5 +- 16 files changed, 386 insertions(+), 123 deletions(-) diff --git a/include/dolphin/gx/GXAurora.h b/include/dolphin/gx/GXAurora.h index 3069c52..c71ba7e 100644 --- a/include/dolphin/gx/GXAurora.h +++ b/include/dolphin/gx/GXAurora.h @@ -1,6 +1,8 @@ #ifndef DOLPHIN_GXAURORA_H #define DOLPHIN_GXAURORA_H +#include + #if __cplusplus extern "C" { #endif @@ -58,6 +60,19 @@ void GXPopDebugGroup(); */ void GXInsertDebugMarker(const char* label); +/** + * Create an offscreen framebuffer and switch rendering to it. + * All subsequent GX rendering will target this framebuffer until GXRestoreFrameBuffer() is called. + * Use GXCopyTex to resolve the offscreen content into a texture. + */ +void GXCreateFrameBuffer(u32 width, u32 height); + +/** + * Restore rendering to the main EFB framebuffer. + * Must be called after GXCreateFrameBuffer() to resume normal rendering. + */ +void GXRestoreFrameBuffer(void); + #if __cplusplus } #endif diff --git a/lib/dolphin/gx/GXAurora.cpp b/lib/dolphin/gx/GXAurora.cpp index 47501ef..e58f7f9 100644 --- a/lib/dolphin/gx/GXAurora.cpp +++ b/lib/dolphin/gx/GXAurora.cpp @@ -5,6 +5,9 @@ #include "__gx.h" #include "gx.hpp" +#include "../../gfx/common.hpp" +#include "../../gx/fifo.hpp" + static void GXWriteString(const char* label) { auto length = strlen(label); @@ -30,3 +33,13 @@ void GXInsertDebugMarker(const char* label) { GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_MARKER_INSERT); GXWriteString(label); } + +void GXCreateFrameBuffer(u32 width, u32 height) { + aurora::gx::fifo::drain(); + aurora::gfx::begin_offscreen(width, height); +} + +void GXRestoreFrameBuffer() { + aurora::gx::fifo::drain(); + aurora::gfx::end_offscreen(); +} diff --git a/lib/dolphin/gx/GXFrameBuffer.cpp b/lib/dolphin/gx/GXFrameBuffer.cpp index 0657574..0166572 100644 --- a/lib/dolphin/gx/GXFrameBuffer.cpp +++ b/lib/dolphin/gx/GXFrameBuffer.cpp @@ -85,8 +85,9 @@ void GXSetTexCopySrc(u16 left, u16 top, u16 wd, u16 ht) { g_gxState.texCopySrc = void GXSetDispCopyDst(u16 wd, u16 ht) {} void GXSetTexCopyDst(u16 wd, u16 ht, GXTexFmt fmt, GXBool mipmap) { - // TODO texture copy scaling (mipmap) g_gxState.texCopyFmt = fmt; + g_gxState.texCopyDstWidth = wd; + g_gxState.texCopyDstHeight = ht; } // TODO GXSetDispCopyFrame2Field @@ -125,25 +126,29 @@ void GXCopyDisp(void* dest, GXBool clear) {} void GXCopyTex(void* dest, GXBool clear) { const auto& rect = g_gxState.texCopySrc; - const wgpu::Extent3D size{ - .width = static_cast(rect.width), - .height = static_cast(rect.height), - .depthOrArrayLayers = 1, - }; + const u32 dstWidth = g_gxState.texCopyDstWidth; + const u32 dstHeight = g_gxState.texCopyDstHeight; + const auto texCopyFmt = g_gxState.texCopyFmt; + const aurora::gx::GXState::CopyTextureKey key{ .dest = dest, - .width = size.width, - .height = size.height, - .format = g_gxState.texCopyFmt, + .width = dstWidth, + .height = dstHeight, + .format = texCopyFmt, }; auto it = g_gxState.copyTextureCache.find(key); if (it == g_gxState.copyTextureCache.end()) { - // Configure the texture swizzle to use alpha 1.0 if targeting RGB565 or EFB doesn't have alpha - const auto fmt = g_gxState.texCopyFmt == GX_TF_RGB565 || g_gxState.pixelFmt == GX_PF_RGB8_Z24 || - g_gxState.pixelFmt == GX_PF_RGB565_Z16 - ? GX_TF_RGB565 - : GX_TF_RGBA8; - auto handle = aurora::gfx::new_render_texture(rect.width, rect.height, fmt, "Resolved Texture"); + aurora::gfx::TextureHandle handle; + if (aurora::gfx::tex_copy_conv::needs_conversion(texCopyFmt)) { + handle = aurora::gfx::new_conv_texture(dstWidth, dstHeight, texCopyFmt, "Copy Conv Texture"); + } else { + // Configure the texture swizzle to use alpha 1.0 if targeting RGB565 or EFB doesn't have alpha + const auto fmt = texCopyFmt == GX_TF_RGB565 || g_gxState.pixelFmt == GX_PF_RGB8_Z24 || + g_gxState.pixelFmt == GX_PF_RGB565_Z16 + ? GX_TF_RGB565 + : GX_TF_RGBA8; + handle = aurora::gfx::new_render_texture(dstWidth, dstHeight, fmt, "Resolved Texture"); + } it = g_gxState.copyTextureCache.emplace(key, handle).first; } const auto& handle = it->second; @@ -167,24 +172,9 @@ void GXCopyTex(void* dest, GXBool clear) { const auto clearAlpha = clear && g_gxState.alphaUpdate; const auto clearDepth = clear && g_gxState.depthUpdate; aurora::gfx::resolve_pass(handle, rect, clearColor, clearAlpha, clearDepth, g_gxState.clearColor, - aurora::gx::clear_depth_value()); + aurora::gx::clear_depth_value(), texCopyFmt); - if (aurora::gfx::tex_copy_conv::needs_conversion(g_gxState.texCopyFmt)) { - auto convIt = g_gxState.convTextureCache.find(key); - if (convIt == g_gxState.convTextureCache.end()) { - auto convHandle = - aurora::gfx::new_conv_texture(rect.width, rect.height, g_gxState.texCopyFmt, "Copy Conv Texture"); - convIt = g_gxState.convTextureCache.emplace(key, convHandle).first; - } - aurora::gfx::queue_copy_conv({ - .fmt = g_gxState.texCopyFmt, - .src = handle, - .dst = convIt->second, - }); - g_gxState.copyTextures[dest] = convIt->second; - } else { - g_gxState.copyTextures[dest] = handle; - } + g_gxState.copyTextures[dest] = handle; } // TODO GXGetYScaleFactor diff --git a/lib/gfx/clear.cpp b/lib/gfx/clear.cpp index 8c57cdb..2799a5c 100644 --- a/lib/gfx/clear.cpp +++ b/lib/gfx/clear.cpp @@ -18,7 +18,6 @@ wgpu::ColorWriteMask clear_write_mask(bool clearColor, bool clearAlpha) { namespace aurora::gfx::clear { using webgpu::g_device; -using webgpu::g_frameBuffer; using webgpu::g_graphicsConfig; wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) { @@ -103,23 +102,22 @@ fn fs_main() -> @location(0) vec4 { .depthStencil = &depthStencil, .multisample = wgpu::MultisampleState{ - .count = g_graphicsConfig.msaaSamples, - .mask = UINT32_MAX, + .count = config.msaaSamples, }, .fragment = &fragmentState, }; return g_device.CreateRenderPipeline(&pipelineDescriptor); } -void render(const DrawData& data, const wgpu::RenderPassEncoder& pass) { +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, const wgpu::Extent3D& targetSize) { if (!bind_pipeline(data.pipeline, pass)) { return; } - const auto& size = g_frameBuffer.size; pass.SetBlendConstant(&data.color); - pass.SetViewport(0.f, 0.f, static_cast(size.width), static_cast(size.height), data.depth, data.depth); - pass.SetScissorRect(0, 0, size.width, size.height); + pass.SetViewport(0.f, 0.f, static_cast(targetSize.width), static_cast(targetSize.height), data.depth, + data.depth); + pass.SetScissorRect(0, 0, targetSize.width, targetSize.height); pass.Draw(3); } -} // namespace aurora::gfx::clear \ No newline at end of file +} // namespace aurora::gfx::clear diff --git a/lib/gfx/clear.hpp b/lib/gfx/clear.hpp index b064f2b..2f87117 100644 --- a/lib/gfx/clear.hpp +++ b/lib/gfx/clear.hpp @@ -11,16 +11,17 @@ struct DrawData { float depth = 0.f; }; -constexpr uint8_t ClearPipelineConfigVersion = 1; +constexpr uint32_t ClearPipelineConfigVersion = 2; struct PipelineConfig { - uint8_t version = ClearPipelineConfigVersion; - bool clearColor : 1 = true; - bool clearAlpha : 1 = true; - bool clearDepth : 1 = true; - uint8_t _pad : 5 = 0; + uint32_t version = ClearPipelineConfigVersion; + uint32_t msaaSamples = 1; + bool clearColor = true; + bool clearAlpha = true; + bool clearDepth = true; + uint8_t _pad = 0; }; static_assert(std::has_unique_object_representations_v); wgpu::RenderPipeline create_pipeline(const PipelineConfig& config); -void render(const DrawData& data, const wgpu::RenderPassEncoder& pass); -} // namespace aurora::gfx::clear \ No newline at end of file +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, const wgpu::Extent3D& targetSize); +} // namespace aurora::gfx::clear diff --git a/lib/gfx/common.cpp b/lib/gfx/common.cpp index ddc8e6f..98954d7 100644 --- a/lib/gfx/common.cpp +++ b/lib/gfx/common.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -137,18 +138,55 @@ std::atomic_ref createdPipelines{g_stats.createdPipelines}; using CommandList = std::vector; struct RenderPass { + wgpu::TextureView colorView; + wgpu::TextureView resolveView; // MSAA resolve target; null if msaaSamples == 1 + wgpu::TextureView depthView; + wgpu::Texture copySourceTexture; + wgpu::TextureView copySourceView; + wgpu::Extent3D targetSize; + uint32_t msaaSamples = 1; + TextureHandle resolveTarget; + GXTexFmt resolveFormat = GX_TF_RGBA8; ClipRect resolveRect; + Range resolveUniformRange; Vec4 clearColorValue{0.f, 0.f, 0.f, 0.f}; float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f; CommandList commands; bool clearColor = true; bool clearDepth = true; - std::vector copyConvs; std::vector paletteConvs; }; static std::vector g_renderPasses; static u32 g_currentRenderPass = UINT32_MAX; +static bool g_inOffscreen = false; +static std::optional g_suspendedEfbPass; +static webgpu::TextureWithSampler g_offscreenColor; +static webgpu::TextureWithSampler g_offscreenDepth; + +static void set_efb_targets(RenderPass& pass) { + pass.colorView = webgpu::g_frameBuffer.view; + pass.resolveView = webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.view : nullptr; + pass.depthView = webgpu::g_depthBuffer.view; + pass.copySourceTexture = + webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.texture : webgpu::g_frameBuffer.texture; + pass.copySourceView = + webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.view : webgpu::g_frameBuffer.view; + pass.targetSize = webgpu::g_frameBuffer.size; + pass.msaaSamples = webgpu::g_graphicsConfig.msaaSamples; +} + +struct OffscreenDepthKey { + uint32_t width; + uint32_t height; + + bool operator==(const OffscreenDepthKey& rhs) const { return width == rhs.width && height == rhs.height; } + template + friend H AbslHashValue(H h, const OffscreenDepthKey& key) { + return H::combine(std::move(h), key.width, key.height); + } +}; +static absl::flat_hash_map g_offscreenDepthCache; std::vector g_textureUploads; static ByteBuffer g_serializedPipelines{}; @@ -273,20 +311,46 @@ PipelineRef pipeline_ref(clear::PipelineConfig config) { } void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, - Vec4 clearColorValue, float clearDepthValue) { - auto& currentPass = g_renderPasses[g_currentRenderPass]; - currentPass.resolveTarget = std::move(texture); - currentPass.resolveRect = rect; - auto& newPass = g_renderPasses.emplace_back(); - newPass.clearColorValue = clearColorValue; - newPass.clearDepthValue = clearDepthValue; - newPass.clearColor = clearColor && clearAlpha; - newPass.clearDepth = clearDepth; + Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat) { + // Resolve current render pass + auto& prevPass = g_renderPasses[g_currentRenderPass]; + prevPass.resolveTarget = std::move(texture); + prevPass.resolveRect = rect; + prevPass.resolveFormat = resolveFormat; + // Push UV transform uniform for tex_copy_conv (crop region in UV space) + const auto srcW = static_cast(prevPass.targetSize.width); + const auto srcH = static_cast(prevPass.targetSize.height); + const std::array uvTransform{ + static_cast(rect.x) / srcW, + static_cast(rect.y) / srcH, + static_cast(rect.width) / srcW, + static_cast(rect.height) / srcH, + }; + prevPass.resolveUniformRange = push_uniform(uvTransform); + + // Populate new render pass from previous + const auto msaaSamples = prevPass.msaaSamples; + RenderPass newPass{ + .colorView = prevPass.colorView, + .resolveView = prevPass.resolveView, + .depthView = prevPass.depthView, + .copySourceTexture = prevPass.copySourceTexture, + .copySourceView = prevPass.copySourceView, + .targetSize = prevPass.targetSize, + .msaaSamples = msaaSamples, + .clearColorValue = clearColorValue, + .clearDepthValue = clearDepthValue, + .clearColor = clearColor && clearAlpha, + .clearDepth = clearDepth, + }; + g_renderPasses.emplace_back(std::move(newPass)); ++g_currentRenderPass; + if (!newPass.clearColor && (clearColor || clearAlpha)) { // If we're only clearing color _or_ alpha, perform a clear draw push_draw_command(clear::DrawData{ .pipeline = pipeline_ref(clear::PipelineConfig{ + .msaaSamples = msaaSamples, .clearColor = clearColor, .clearAlpha = clearAlpha, .clearDepth = false, // Depth cleared via render attachment @@ -304,15 +368,131 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); } -void queue_copy_conv(tex_copy_conv::ConvRequest req) { - CHECK(g_currentRenderPass > 0, "queue_copy_conv called without a prior resolve_pass"); - g_renderPasses[g_currentRenderPass - 1].copyConvs.push_back(std::move(req)); -} - void queue_palette_conv(tex_palette_conv::ConvRequest req) { g_renderPasses[g_currentRenderPass].paletteConvs.push_back(std::move(req)); } +bool is_offscreen() noexcept { return g_inOffscreen; } + +uint32_t get_sample_count() noexcept { + CHECK(g_currentRenderPass != UINT32_MAX, "get_sample_count called outside of a frame"); + return g_renderPasses[g_currentRenderPass].msaaSamples; +} + +void clear_offscreen_cache() { g_offscreenDepthCache.clear(); } + +static webgpu::TextureWithSampler get_offscreen_depth(uint32_t width, uint32_t height) { + OffscreenDepthKey key{width, height}; + auto it = g_offscreenDepthCache.find(key); + if (it != g_offscreenDepthCache.end()) { + return it->second; + } + const auto format = webgpu::g_graphicsConfig.depthFormat; + const wgpu::Extent3D size{width, height, 1}; + const wgpu::TextureDescriptor desc{ + .label = "Offscreen Depth", + .usage = wgpu::TextureUsage::RenderAttachment, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = format, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&desc); + auto view = texture.CreateView(); + webgpu::TextureWithSampler result{ + .texture = std::move(texture), + .view = std::move(view), + .size = size, + .format = format, + }; + auto [insertIt, _] = g_offscreenDepthCache.emplace(key, result); + return insertIt->second; +} + +void begin_offscreen(uint32_t width, uint32_t height) { + CHECK(g_currentRenderPass != UINT32_MAX, "begin_offscreen called outside of a frame"); + + // If the current EFB pass has no resolve target, its output is unobservable. + // Suspend it so that we can resume it after the offscreen pass. + if (!g_inOffscreen) { + auto& currentPass = g_renderPasses[g_currentRenderPass]; + if (!currentPass.resolveTarget) { + g_suspendedEfbPass = std::move(currentPass); + g_renderPasses.pop_back(); + --g_currentRenderPass; + } + } + + // Create offscreen color target + const wgpu::Extent3D size{width, height, 1}; + const auto colorFormat = webgpu::g_graphicsConfig.surfaceConfiguration.format; + const wgpu::TextureDescriptor colorDesc{ + .label = "Offscreen Color", + .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | + wgpu::TextureUsage::CopyDst, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = colorFormat, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto colorTexture = g_device.CreateTexture(&colorDesc); + auto colorView = colorTexture.CreateView(); + g_offscreenColor = { + .texture = std::move(colorTexture), + .view = std::move(colorView), + .size = size, + .format = colorFormat, + }; + g_offscreenDepth = get_offscreen_depth(width, height); + + // Start a new pass with offscreen targets + RenderPass newPass{ + .colorView = g_offscreenColor.view, + .depthView = g_offscreenDepth.view, + .copySourceTexture = g_offscreenColor.texture, + .copySourceView = g_offscreenColor.view, + .targetSize = size, + .msaaSamples = 1, + .clearColorValue = {0.f, 0.f, 0.f, 0.f}, + .clearDepthValue = gx::UseReversedZ ? 0.f : 1.f, + .clearColor = true, + .clearDepth = true, + }; + g_renderPasses.emplace_back(std::move(newPass)); + ++g_currentRenderPass; + + g_inOffscreen = true; + + push_command(CommandType::SetViewport, Command::Data{.setViewport = {0.f, 0.f, static_cast(width), + static_cast(height), 0.f, 1.f}}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = {0, 0, width, height}}); +} + +void end_offscreen() { + CHECK(g_inOffscreen, "end_offscreen called without begin_offscreen"); + + g_inOffscreen = false; + g_offscreenColor = {}; + g_offscreenDepth = {}; + + // Resume suspended EFB pass, or start a new one (load existing content) + if (g_suspendedEfbPass) { + g_renderPasses.emplace_back(std::move(*g_suspendedEfbPass)); + g_suspendedEfbPass.reset(); + } else { + auto& pass = g_renderPasses.emplace_back(); + pass.clearColor = false; + pass.clearDepth = false; + } + ++g_currentRenderPass; + set_efb_targets(g_renderPasses[g_currentRenderPass]); + + push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); +} + template <> void push_draw_command(gx::DrawData data) { push_draw_command(ShaderDrawCommand{.type = ShaderType::GX, .gx = data}); @@ -506,6 +686,10 @@ void shutdown() { g_stagingBuffers.fill({}); g_renderPasses.clear(); g_currentRenderPass = UINT32_MAX; + g_offscreenDepthCache.clear(); + g_offscreenColor = {}; + g_offscreenDepth = {}; + g_inOffscreen = false; queuedPipelines = 0; createdPipelines = 0; @@ -549,8 +733,10 @@ void begin_frame() { g_stats.drawCallCount = 0; g_stats.mergedDrawCallCount = 0; + g_suspendedEfbPass.reset(); g_renderPasses.emplace_back(); + set_efb_targets(g_renderPasses[0]); g_renderPasses[0].clearColorValue = gx::g_gxState.clearColor; g_renderPasses[0].clearDepthValue = gx::clear_depth_value(); g_currentRenderPass = 0; @@ -563,6 +749,7 @@ void begin_frame() { } void end_frame(const wgpu::CommandEncoder& cmd) { + ASSERT(!g_inOffscreen, "end_frame called while offscreen rendering is active"); uint64_t bufferOffset = 0; const auto writeBuffer = [&](ByteBuffer& buf, wgpu::Buffer& out, uint64_t size, std::string_view label) { const auto writeSize = buf.size(); // Only need to copy this many bytes @@ -611,13 +798,20 @@ void end_frame(const wgpu::CommandEncoder& cmd) { void render(wgpu::CommandEncoder& cmd) { for (u32 i = 0; i < g_renderPasses.size(); ++i) { const auto& passInfo = g_renderPasses[i]; + for (const auto& conv : passInfo.paletteConvs) { + tex_palette_conv::run(cmd, conv); + } if (i == g_renderPasses.size() - 1) { ASSERT(!passInfo.resolveTarget, "Final render pass must not have resolve target"); + } else if (!passInfo.resolveTarget) { + // Skip intermediate render passes without resolve target + continue; } + const std::array attachments{ wgpu::RenderPassColorAttachment{ - .view = webgpu::g_frameBuffer.view, - .resolveTarget = webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.view : nullptr, + .view = passInfo.colorView, + .resolveTarget = passInfo.resolveView, .loadOp = passInfo.clearColor ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, .storeOp = wgpu::StoreOp::Store, .clearValue = @@ -630,7 +824,7 @@ void render(wgpu::CommandEncoder& cmd) { }, }; const wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{ - .view = webgpu::g_depthBuffer.view, + .view = passInfo.depthView, .depthLoadOp = passInfo.clearDepth ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, .depthStoreOp = wgpu::StoreOp::Store, .depthClearValue = passInfo.clearDepthValue, @@ -643,37 +837,43 @@ void render(wgpu::CommandEncoder& cmd) { .depthStencilAttachment = &depthStencilAttachment, }; - for (const auto& conv : passInfo.paletteConvs) { - tex_palette_conv::run(cmd, conv); - } auto pass = cmd.BeginRenderPass(&renderPassDescriptor); render_pass(pass, i); pass.End(); if (passInfo.resolveTarget) { - wgpu::TexelCopyTextureInfo src{ - .origin = - wgpu::Origin3D{ - .x = static_cast(passInfo.resolveRect.x), - .y = static_cast(passInfo.resolveRect.y), - }, + const auto& dstSize = passInfo.resolveTarget->size; + const bool needsConversion = tex_copy_conv::needs_conversion(passInfo.resolveFormat); + const bool needsScaling = dstSize.width != static_cast(passInfo.resolveRect.width) || + dstSize.height != static_cast(passInfo.resolveRect.height); + const tex_copy_conv::ConvRequest convReq{ + .fmt = passInfo.resolveFormat, + .srcView = passInfo.copySourceView, + .uniformRange = passInfo.resolveUniformRange, + .dst = passInfo.resolveTarget, }; - if (webgpu::g_graphicsConfig.msaaSamples > 1) { - src.texture = webgpu::g_frameBufferResolved.texture; + if (needsConversion) { + tex_copy_conv::run(cmd, convReq); + } else if (needsScaling) { + tex_copy_conv::blit(cmd, convReq); } else { - src.texture = webgpu::g_frameBuffer.texture; - } - const wgpu::TexelCopyTextureInfo dst{ - .texture = passInfo.resolveTarget->texture, - }; - const wgpu::Extent3D size{ - .width = static_cast(passInfo.resolveRect.width), - .height = static_cast(passInfo.resolveRect.height), - .depthOrArrayLayers = 1, - }; - cmd.CopyTextureToTexture(&src, &dst, &size); - for (const auto& conv : passInfo.copyConvs) { - tex_copy_conv::run(cmd, conv); + const wgpu::TexelCopyTextureInfo src{ + .texture = passInfo.copySourceTexture, + .origin = + wgpu::Origin3D{ + .x = static_cast(passInfo.resolveRect.x), + .y = static_cast(passInfo.resolveRect.y), + }, + }; + const wgpu::TexelCopyTextureInfo dst{ + .texture = passInfo.resolveTarget->texture, + }; + const wgpu::Extent3D size{ + .width = static_cast(passInfo.resolveRect.width), + .height = static_cast(passInfo.resolveRect.height), + .depthOrArrayLayers = 1, + }; + cmd.CopyTextureToTexture(&src, &dst, &size); } } } @@ -728,7 +928,7 @@ void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { } break; case CommandType::SetScissor: { const auto& sc = cmd.data.setScissor; - const auto& size = webgpu::g_frameBuffer.size; + const auto& size = g_renderPasses[idx].targetSize; const auto x = std::clamp(sc.x, 0u, size.width); const auto y = std::clamp(sc.y, 0u, size.height); const auto w = std::clamp(sc.w, 0u, size.width - x); @@ -739,7 +939,7 @@ void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { const auto& draw = cmd.data.draw; switch (draw.type) { case ShaderType::Clear: - clear::render(draw.clear, pass); + clear::render(draw.clear, pass, g_renderPasses[idx].targetSize); break; case ShaderType::GX: gx::render(draw.gx, pass); diff --git a/lib/gfx/common.hpp b/lib/gfx/common.hpp index e723208..590a7b3 100644 --- a/lib/gfx/common.hpp +++ b/lib/gfx/common.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #define XXH_STATIC_LINKING_ONLY #include @@ -213,15 +214,17 @@ void render(wgpu::CommandEncoder& cmd); void render_pass(const wgpu::RenderPassEncoder& pass, uint32_t idx); void map_staging_buffer(); void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, - Vec4 clearColorValue, float clearDepthValue); + Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat = GX_TF_RGBA8); + +void begin_offscreen(uint32_t width, uint32_t height); +void end_offscreen(); +bool is_offscreen() noexcept; +uint32_t get_sample_count() noexcept; +void clear_offscreen_cache(); -namespace tex_copy_conv { -struct ConvRequest; -} // namespace tex_copy_conv namespace tex_palette_conv { struct ConvRequest; } // namespace tex_palette_conv -void queue_copy_conv(tex_copy_conv::ConvRequest req); void queue_palette_conv(tex_palette_conv::ConvRequest req); Range push_verts(const uint8_t* data, size_t length); diff --git a/lib/gfx/tex_copy_conv.cpp b/lib/gfx/tex_copy_conv.cpp index 667c53f..1dffb32 100644 --- a/lib/gfx/tex_copy_conv.cpp +++ b/lib/gfx/tex_copy_conv.cpp @@ -20,6 +20,12 @@ static constexpr std::string_view ShaderPreamble = R"( @group(0) @binding(0) var src_samp: sampler; @group(0) @binding(1) var src: texture_2d; +struct UVTransform { + offset: vec2f, + scale: vec2f, +}; +@group(0) @binding(2) var uv_xf: UVTransform; + struct VertexOutput { @builtin(position) pos: vec4f, @location(0) uv: vec2f, @@ -39,7 +45,7 @@ var uvs: array = array( @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { var out: VertexOutput; out.pos = vec4f(positions[vi], 0.0, 1.0); - out.uv = uvs[vi]; + out.uv = uvs[vi] * uv_xf.scale + uv_xf.offset; return out; } @@ -53,6 +59,13 @@ fn quantize4(v: f32) -> f32 { } )"sv; +// Passthrough blit (for scaling) +static constexpr std::string_view FragPassthrough = R"( +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + return textureSample(src, src_samp, in.uv); +} +)"sv; + // GX_TF_I4: 4-bit intensity -> R8Unorm (quantized) static constexpr std::string_view FragI4 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { @@ -197,6 +210,7 @@ static constexpr std::array ConvPipelines{ static wgpu::BindGroupLayout g_bindGroupLayout; static wgpu::Sampler g_sampler; static absl::flat_hash_map g_pipelines; +static wgpu::RenderPipeline g_blitPipeline; static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv) { std::string shaderSource; @@ -267,6 +281,14 @@ void initialize() { .viewDimension = wgpu::TextureViewDimension::e2D, }, }, + wgpu::BindGroupLayoutEntry{ + .binding = 2, + .visibility = wgpu::ShaderStage::Vertex, + .buffer = + wgpu::BufferBindingLayout{ + .type = wgpu::BufferBindingType::Uniform, + }, + }, }; const wgpu::BindGroupLayoutDescriptor bindGroupLayoutDescriptor{ .label = "TexCopyConv Bind Group Layout", @@ -275,6 +297,8 @@ void initialize() { }; g_bindGroupLayout = g_device.CreateBindGroupLayout(&bindGroupLayoutDescriptor); + g_blitPipeline = create_pipeline( + {GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"}); for (const auto& conv : ConvPipelines) { g_pipelines[conv.fmt] = create_pipeline(conv); if (conv.outputFormat != to_wgpu(conv.fmt)) { @@ -292,16 +316,12 @@ void initialize() { void shutdown() { g_pipelines.clear(); + g_blitPipeline = {}; g_bindGroupLayout = {}; g_sampler = {}; } -void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { - const auto it = g_pipelines.find(req.fmt); - if (it == g_pipelines.end()) { - Log.fatal("No copy conversion pipeline for format {}", static_cast(req.fmt)); - } - +static void execute(const wgpu::CommandEncoder& cmd, const ConvRequest& req, const wgpu::RenderPipeline& pipeline) { const std::array bindGroupEntries{ wgpu::BindGroupEntry{ .binding = 0, @@ -309,7 +329,13 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { }, wgpu::BindGroupEntry{ .binding = 1, - .textureView = req.src->sampleTextureView, + .textureView = req.srcView, + }, + wgpu::BindGroupEntry{ + .binding = 2, + .buffer = g_uniformBuffer, + .offset = req.uniformRange.offset, + .size = req.uniformRange.size, }, }; const wgpu::BindGroupDescriptor bindGroupDescriptor{ @@ -333,10 +359,20 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { .colorAttachments = colorAttachments.data(), }; const auto pass = cmd.BeginRenderPass(&renderPassDescriptor); - pass.SetPipeline(it->second); + pass.SetPipeline(pipeline); pass.SetBindGroup(0, bindGroup); pass.Draw(3); pass.End(); } +void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { + const auto it = g_pipelines.find(req.fmt); + if (it == g_pipelines.end()) { + Log.fatal("No copy conversion pipeline for format {}", static_cast(req.fmt)); + } + execute(cmd, req, it->second); +} + +void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); } + } // namespace aurora::gfx::tex_copy_conv diff --git a/lib/gfx/tex_copy_conv.hpp b/lib/gfx/tex_copy_conv.hpp index 55a699b..4e7dc47 100644 --- a/lib/gfx/tex_copy_conv.hpp +++ b/lib/gfx/tex_copy_conv.hpp @@ -8,8 +8,9 @@ namespace aurora::gfx::tex_copy_conv { struct ConvRequest { GXTexFmt fmt; - TextureHandle src; - TextureHandle dst; + wgpu::TextureView srcView; // View of resolved EFB / offscreen color + Range uniformRange; // UV transform uniform (offset + scale) + TextureHandle dst; // Destination texture }; bool needs_conversion(GXTexFmt fmt); @@ -17,5 +18,6 @@ bool needs_conversion(GXTexFmt fmt); void initialize(); void shutdown(); void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req); +void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req); } // namespace aurora::gfx::tex_copy_conv diff --git a/lib/gx/gx.cpp b/lib/gx/gx.cpp index db5df0c..46c94f4 100644 --- a/lib/gx/gx.cpp +++ b/lib/gx/gx.cpp @@ -29,7 +29,6 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept { return g_gxState.t void clear_copy_texture_cache() noexcept { g_gxState.copyTextures.clear(); g_gxState.copyTextureCache.clear(); - g_gxState.convTextureCache.clear(); } static inline wgpu::BlendFactor to_blend_factor(GXBlendFactor fac, bool isDst) { @@ -231,8 +230,7 @@ wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef arrays; gfx::ClipRect texCopySrc; GXTexFmt texCopyFmt; + u16 texCopyDstWidth = 0; + u16 texCopyDstHeight = 0; struct CopyTextureKey { const void* dest = nullptr; u32 width = 0; @@ -345,7 +347,6 @@ struct GXState { }; absl::flat_hash_map copyTextures; absl::flat_hash_map copyTextureCache; - absl::flat_hash_map convTextureCache; bool depthCompare = true; bool depthUpdate = true; bool colorUpdate = true; diff --git a/lib/gx/gx_fmt.hpp b/lib/gx/gx_fmt.hpp index 0cca1a1..26500ff 100644 --- a/lib/gx/gx_fmt.hpp +++ b/lib/gx/gx_fmt.hpp @@ -933,5 +933,7 @@ inline std::string format_as(const GXTexFmt& fmt) { return "GX_TF_R8_PC"; case GX_TF_RGBA8_PC: return "GX_TF_RGBA8_PC"; + default: + return fmt::format("GXTexFmt({})", underlying(fmt)); } } \ No newline at end of file diff --git a/lib/gx/pipeline.hpp b/lib/gx/pipeline.hpp index 6196ca8..ae7d0e6 100644 --- a/lib/gx/pipeline.hpp +++ b/lib/gx/pipeline.hpp @@ -14,19 +14,20 @@ struct DrawData { uint32_t indexCount; uint32_t instanceCount; GXBindGroups bindGroups; - u32 dstAlpha; + uint32_t dstAlpha; }; -constexpr u32 GXPipelineConfigVersion = 10; +constexpr uint32_t GXPipelineConfigVersion = 11; struct PipelineConfig { - u32 version = GXPipelineConfigVersion; + uint32_t version = GXPipelineConfigVersion; + uint32_t msaaSamples = 1; ShaderConfig shaderConfig; GXCompare depthFunc; GXCullMode cullMode; GXBlendMode blendMode; GXBlendFactor blendFacSrc, blendFacDst; GXLogicOp blendOp; - u32 dstAlpha; + uint32_t dstAlpha; bool depthCompare, depthUpdate, alphaUpdate, colorUpdate; }; static_assert(std::has_unique_object_representations_v); @@ -34,5 +35,5 @@ static_assert(std::has_unique_object_representations_v); wgpu::RenderPipeline create_pipeline([[maybe_unused]] const PipelineConfig& config); void render(const DrawData& data, const wgpu::RenderPassEncoder& pass); -void queue_surface(const u8* dlStart, u32 dlSize, bool bigEndian) noexcept; +void queue_surface(const u8* dlStart, uint32_t dlSize, bool bigEndian) noexcept; } // namespace aurora::gx diff --git a/lib/logging.cpp b/lib/logging.cpp index 4887292..8f80ab0 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -23,7 +23,8 @@ void log_internal(const AuroraLogLevel level, const char* module, const char* me } } // namespace aurora -auto fmt::formatter::format(const AuroraLogLevel level, format_context& ctx) const -> format_context::iterator { +auto fmt::formatter::format(const AuroraLogLevel level, format_context& ctx) const + -> format_context::iterator { std::string_view name = "unknown"; switch (level) { case LOG_DEBUG: diff --git a/lib/webgpu/gpu.cpp b/lib/webgpu/gpu.cpp index a22bd33..9491df1 100644 --- a/lib/webgpu/gpu.cpp +++ b/lib/webgpu/gpu.cpp @@ -21,7 +21,10 @@ namespace aurora::gx { void clear_copy_texture_cache() noexcept; -} +} // namespace aurora::gx +namespace aurora::gfx { +void clear_offscreen_cache(); +} // namespace aurora::gfx namespace aurora::webgpu { static Module Log("aurora::gpu"); @@ -604,6 +607,7 @@ void resize_swapchain(uint32_t width, uint32_t height, bool force) { } if (sizeChanged) { gx::clear_copy_texture_cache(); + gfx::clear_offscreen_cache(); } g_graphicsConfig.surfaceConfiguration.width = width; g_graphicsConfig.surfaceConfiguration.height = height; diff --git a/tests/gx_test_stubs.cpp b/tests/gx_test_stubs.cpp index e05e30c..244390c 100644 --- a/tests/gx_test_stubs.cpp +++ b/tests/gx_test_stubs.cpp @@ -139,15 +139,12 @@ TextureHandle new_render_texture(uint32_t width, uint32_t height, u32 gxFormat, TextureHandle new_conv_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept { return {}; } void write_texture(const TextureRef& ref, ArrayRef data) noexcept {} void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, - Vec4 clearColorValue, float clearDepthValue) {} -void queue_copy_conv(tex_copy_conv::ConvRequest req) {} + Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat) {} void queue_palette_conv(tex_palette_conv::ConvRequest req) {} } // namespace aurora::gfx namespace aurora::gfx::tex_copy_conv { bool needs_conversion(GXTexFmt fmt) { return false; } -wgpu::TextureFormat output_format(GXTexFmt fmt) { return wgpu::TextureFormat::Undefined; } -void queue(ConvRequest req) {} } // namespace aurora::gfx::tex_copy_conv namespace aurora::gfx::tex_palette_conv {