From 212d01769d39f9bbd0eedd89167c8222b30d6ca3 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 9 Apr 2026 03:19:58 +0200 Subject: [PATCH 1/2] Tracy integration (#106) * Sprinkle Tracy all over the project * Move Tracy FetchContent to extern file, disable Tracy by default * Use older CMake syntax for setting cache variables * Fix test compile * Rename ALIGN macro to AURORA_ALIGN Needed because it conflicts with a macro in Apple's SDK headers on my macbook, and Tracy is now pulling in that header. --- CMakeLists.txt | 2 +- cmake/aurora_core.cmake | 2 +- cmake/aurora_gx.cmake | 2 +- extern/CMakeLists.txt | 20 ++++++++++++++++++++ lib/aurora.cpp | 23 +++++++++++++++++++++++ lib/dolphin/gx/GXTexture.cpp | 4 ++++ lib/gfx/clear.cpp | 2 ++ lib/gfx/common.cpp | 26 ++++++++++++++++++++------ lib/gfx/texture.cpp | 3 +++ lib/gx/command_processor.cpp | 11 +++++++++++ lib/gx/gx.cpp | 3 +++ lib/gx/pipeline.cpp | 2 ++ lib/gx/shader.cpp | 3 +++ lib/imgui.cpp | 5 +++++ lib/internal.hpp | 4 +--- tests/CMakeLists.txt | 1 + 16 files changed, 101 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f017879..e081201 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,7 +111,7 @@ add_library(aurora::shared ALIAS aurora_shared) target_compile_definitions(aurora_shared PUBLIC AURORA TARGET_PC) target_include_directories(aurora_shared PUBLIC include) target_link_libraries(aurora_shared PUBLIC ${AURORA_SDL3_TARGET} fmt::fmt xxhash) -target_link_libraries(aurora_shared PRIVATE absl::btree absl::flat_hash_map) +target_link_libraries(aurora_shared PRIVATE absl::btree absl::flat_hash_map TracyClient) if (AURORA_ENABLE_DVD) target_sources(aurora_shared PRIVATE lib/dolphin/dvd/dvd.cpp) diff --git a/cmake/aurora_core.cmake b/cmake/aurora_core.cmake index 3a9da55..9fc7bb2 100644 --- a/cmake/aurora_core.cmake +++ b/cmake/aurora_core.cmake @@ -9,7 +9,7 @@ add_library(aurora::core ALIAS aurora_core) target_compile_definitions(aurora_core PUBLIC AURORA TARGET_PC) target_include_directories(aurora_core PUBLIC include) target_link_libraries(aurora_core PUBLIC ${AURORA_SDL3_TARGET} fmt::fmt xxhash) -target_link_libraries(aurora_core PRIVATE absl::btree absl::flat_hash_map sqlite3) +target_link_libraries(aurora_core PRIVATE absl::btree absl::flat_hash_map sqlite3 TracyClient) if (AURORA_ENABLE_GX AND AURORA_CACHE_USE_ZSTD) target_compile_definitions(aurora_core PRIVATE AURORA_CACHE_USE_ZSTD) target_link_libraries(aurora_core PRIVATE libzstd_static) diff --git a/cmake/aurora_gx.cmake b/cmake/aurora_gx.cmake index 362ea5f..9d9cca6 100644 --- a/cmake/aurora_gx.cmake +++ b/cmake/aurora_gx.cmake @@ -35,7 +35,7 @@ add_library(aurora_gx STATIC add_library(aurora::gx ALIAS aurora_gx) target_link_libraries(aurora_gx PUBLIC aurora::core xxhash) -target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map) +target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map TracyClient) if (EMSCRIPTEN) target_link_options(aurora_gx PUBLIC -sUSE_WEBGPU=1 -sASYNCIFY -sEXIT_RUNTIME) target_compile_definitions(aurora_gx PRIVATE ENABLE_BACKEND_WEBGPU) diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index b713452..a59bda3 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -161,3 +161,23 @@ if (AURORA_ENABLE_GX) endif () endif () endif () + +if (NOT TARGET TracyClient) + message(STATUS "aurora: Fetching Tracy") + + # Default to off, users can enable the cache variable if they want to use it. + set(TRACY_ENABLE OFF CACHE BOOL "Enable Tracy profiling") + + include(FetchContent) + FetchContent_Declare( + tracy + URL https://github.com/wolfpld/tracy/archive/refs/tags/v0.13.1.tar.gz + URL_HASH SHA256=d4efc50ebcb0bfcfdbba148995aeb75044c0d80f5d91223aebfaa8fa9e563d2b + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ) + FetchContent_MakeAvailable(tracy) + + if (NOT TARGET TracyClient) + message(FATAL_ERROR "Failed to make TracyClient available") + endif () +endif () diff --git a/lib/aurora.cpp b/lib/aurora.cpp index fc7ea0a..adb7bd2 100644 --- a/lib/aurora.cpp +++ b/lib/aurora.cpp @@ -15,6 +15,8 @@ #include #include +#include "tracy/Tracy.hpp" + namespace aurora { AuroraConfig g_config; uint32_t g_sdlCustomEventsStart; @@ -167,6 +169,7 @@ void shutdown() noexcept { } const AuroraEvent* update() noexcept { + ZoneScoped; if (g_initialFrame) { g_initialFrame = false; input::initialize(); @@ -175,6 +178,7 @@ const AuroraEvent* update() noexcept { } bool begin_frame() noexcept { + ZoneScoped; #ifdef AURORA_ENABLE_GX if (!g_surface) { webgpu::refresh_surface(true); @@ -205,6 +209,7 @@ bool begin_frame() noexcept { Log.error("Failed to get surface texture: {}", magic_enum::enum_name(surfaceTexture.status)); return false; } + imgui::new_frame(window::get_window_size()); gfx::begin_frame(); #endif @@ -212,6 +217,7 @@ bool begin_frame() noexcept { } void end_frame() noexcept { + ZoneScoped; #ifdef AURORA_ENABLE_GX gx::fifo::drain(); const auto encoderDescriptor = wgpu::CommandEncoderDescriptor{ @@ -258,6 +264,23 @@ void end_frame() noexcept { Log.warn("Surface present failed: {}", static_cast(presentStatus)); } g_currentView = {}; + + TracyPlotConfig("aurora: lastVertSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastUniformSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastIndexSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastStorageSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastTextureUploadSize", tracy::PlotFormatType::Memory, false, true, 0); + + TracyPlot("aurora: queuedPipelines", static_cast(gfx::g_stats.queuedPipelines)); + TracyPlot("aurora: createdPipelines", static_cast(gfx::g_stats.createdPipelines)); + TracyPlot("aurora: drawCallCount", static_cast(gfx::g_stats.drawCallCount)); + TracyPlot("aurora: mergedDrawCallCount", static_cast(gfx::g_stats.mergedDrawCallCount)); + TracyPlot("aurora: lastVertSize", static_cast(gfx::g_stats.lastVertSize)); + TracyPlot("aurora: lastUniformSize", static_cast(gfx::g_stats.lastUniformSize)); + TracyPlot("aurora: lastIndexSize", static_cast(gfx::g_stats.lastIndexSize)); + TracyPlot("aurora: lastStorageSize", static_cast(gfx::g_stats.lastStorageSize)); + TracyPlot("aurora: lastTextureUploadSize", static_cast(gfx::g_stats.lastTextureUploadSize)); + #endif } } // namespace diff --git a/lib/dolphin/gx/GXTexture.cpp b/lib/dolphin/gx/GXTexture.cpp index 1c88874..c9ae65c 100644 --- a/lib/dolphin/gx/GXTexture.cpp +++ b/lib/dolphin/gx/GXTexture.cpp @@ -8,11 +8,14 @@ #include +#include "tracy/Tracy.hpp" + using namespace aurora::gfx; extern "C" { void GXInitTexObj(GXTexObj* obj_, const void* data, u16 width, u16 height, GXTexFmt format, GXTexWrapMode wrapS, GXTexWrapMode wrapT, GXBool mipmap) { + ZoneScopedS(3); if (reinterpret_cast(obj_)->ref) Log.fatal("Texture object already has a reference"); memset(obj_, 0, sizeof(GXTexObj)); @@ -40,6 +43,7 @@ void GXInitTexObj(GXTexObj* obj_, const void* data, u16 width, u16 height, GXTex void GXInitTexObjCI(GXTexObj* obj_, const void* data, u16 width, u16 height, GXCITexFmt format, GXTexWrapMode wrapS, GXTexWrapMode wrapT, GXBool mipmap, u32 tlut) { + ZoneScopedS(3); if (reinterpret_cast(obj_)->ref) Log.fatal("Texture object already has a reference"); memset(obj_, 0, sizeof(GXTexObj)); diff --git a/lib/gfx/clear.cpp b/lib/gfx/clear.cpp index 2799a5c..d0b4c78 100644 --- a/lib/gfx/clear.cpp +++ b/lib/gfx/clear.cpp @@ -1,6 +1,7 @@ #include "clear.hpp" #include "../webgpu/gpu.hpp" +#include "tracy/Tracy.hpp" namespace { wgpu::ColorWriteMask clear_write_mask(bool clearColor, bool clearAlpha) { @@ -21,6 +22,7 @@ using webgpu::g_device; using webgpu::g_graphicsConfig; wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) { + ZoneScoped; wgpu::ShaderSourceWGSL sourceDescriptor{}; sourceDescriptor.code = R"""( struct VertexOutput { diff --git a/lib/gfx/common.cpp b/lib/gfx/common.cpp index b8acc64..53bcb3b 100644 --- a/lib/gfx/common.cpp +++ b/lib/gfx/common.cpp @@ -22,6 +22,8 @@ #include #include +#include "tracy/Tracy.hpp" + namespace aurora::gfx { static Module Log("aurora::gfx"); @@ -432,6 +434,7 @@ static OffscreenCacheEntry get_offscreen_textures(uint32_t width, uint32_t heigh } void begin_offscreen(uint32_t width, uint32_t height) { + ZoneScoped; 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. @@ -474,6 +477,7 @@ void begin_offscreen(uint32_t width, uint32_t height) { } void end_offscreen() { + ZoneScoped; CHECK(g_inOffscreen, "end_offscreen called without begin_offscreen"); g_inOffscreen = false; @@ -507,6 +511,10 @@ PipelineRef pipeline_ref(gx::PipelineConfig config) { } static void pipeline_worker() { +#ifdef TRACY_ENABLE + tracy::SetThreadName("Pipeline compilation thread"); +#endif + bool hasMore = false; while (g_hasPipelineThread || g_pipelinesPerFrame < BuildPipelinesPerFrame) { std::pair cb; @@ -718,8 +726,12 @@ void map_staging_buffer() { } void begin_frame() { - while (!bufferMapped) { - g_instance.ProcessEvents(); + ZoneScoped; + { + ZoneScopedN("Wait for buffer map"); + while (!bufferMapped) { + g_instance.ProcessEvents(); + } } size_t bufferOffset = 0; const auto& stagingBuf = g_stagingBuffers[currentStagingBuffer]; @@ -754,12 +766,13 @@ void begin_frame() { } void end_frame(const wgpu::CommandEncoder& cmd) { + ZoneScoped; 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 if (writeSize > 0) { - cmd.CopyBufferToBuffer(g_stagingBuffers[currentStagingBuffer], bufferOffset, out, 0, ALIGN(writeSize, 4)); + cmd.CopyBufferToBuffer(g_stagingBuffers[currentStagingBuffer], bufferOffset, out, 0, AURORA_ALIGN(writeSize, 4)); buf.clear(); } bufferOffset += size; @@ -778,7 +791,7 @@ void end_frame(const wgpu::CommandEncoder& cmd) { .layout = wgpu::TexelCopyBufferLayout{ .offset = item.layout.offset + bufferOffset, - .bytesPerRow = ALIGN(item.layout.bytesPerRow, 256), + .bytesPerRow = AURORA_ALIGN(item.layout.bytesPerRow, 256), .rowsPerImage = item.layout.rowsPerImage, }, .buffer = g_stagingBuffers[currentStagingBuffer], @@ -801,6 +814,7 @@ void end_frame(const wgpu::CommandEncoder& cmd) { } void render(wgpu::CommandEncoder& cmd) { + ZoneScoped; for (u32 i = 0; i < g_renderPasses.size(); ++i) { const auto& passInfo = g_renderPasses[i]; for (const auto& conv : passInfo.paletteConvs) { @@ -1025,7 +1039,7 @@ Range push_storage(const uint8_t* data, size_t length) { } Range push_texture_data(const uint8_t* data, size_t length, u32 bytesPerRow, u32 rowsPerImage) { // For CopyBufferToTexture, we need an alignment of 256 per row (see Dawn kTextureBytesPerRowAlignment) - const auto copyBytesPerRow = ALIGN(bytesPerRow, 256); + const auto copyBytesPerRow = AURORA_ALIGN(bytesPerRow, 256); const auto range = map(g_textureUpload, copyBytesPerRow * rowsPerImage, 0); u8* dst = g_textureUpload.data() + range.offset; for (u32 i = 0; i < rowsPerImage; ++i) { @@ -1086,7 +1100,7 @@ wgpu::Sampler sampler_ref(const wgpu::SamplerDescriptor& descriptor) { return it->second; } -uint32_t align_uniform(uint32_t value) { return ALIGN(value, g_cachedLimits.minUniformBufferOffsetAlignment); } +uint32_t align_uniform(uint32_t value) { return AURORA_ALIGN(value, g_cachedLimits.minUniformBufferOffsetAlignment); } void insert_debug_marker(std::string label) { #if defined(AURORA_GFX_DEBUG_GROUPS) diff --git a/lib/gfx/texture.cpp b/lib/gfx/texture.cpp index c1c2d86..8336e39 100644 --- a/lib/gfx/texture.cpp +++ b/lib/gfx/texture.cpp @@ -16,6 +16,8 @@ #include #include +#include "tracy/Tracy.hpp" + namespace aurora::gfx { using webgpu::g_device; using webgpu::g_queue; @@ -141,6 +143,7 @@ static bool setup_swizzle(wgpu::TextureComponentSwizzleDescriptor& swizzle, u32 TextureHandle new_dynamic_texture_2d(uint32_t width, uint32_t height, uint32_t mips, u32 gxFormat, const char* label) noexcept { + ZoneScopedS(3); const auto wgpuFormat = to_wgpu(gxFormat); const wgpu::Extent3D size{ .width = width, diff --git a/lib/gx/command_processor.cpp b/lib/gx/command_processor.cpp index a6fc9e5..44e394b 100644 --- a/lib/gx/command_processor.cpp +++ b/lib/gx/command_processor.cpp @@ -14,6 +14,8 @@ #include "dolphin/gx/GXAurora.h" #include +#include "tracy/Tracy.hpp" + namespace aurora::gx::fifo { static Module Log("aurora::gx::fifo"); @@ -317,6 +319,7 @@ static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi static void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian); void process(const u8* data, u32 size, bool bigEndian) { + ZoneScoped; u32 pos = 0; while (pos < size) { @@ -354,6 +357,7 @@ void process(const u8* data, u32 size, bool bigEndian) { case CP_CMD_LOAD_INDX_B: case CP_CMD_LOAD_INDX_C: case CP_CMD_LOAD_INDX_D: { + ZoneScopedN("LOAD_INDX"); // Indexed XF load: 4 bytes of data CHECK(pos + 4 <= size, "indexed XF read overrun"); u32 arrayType = GX_POS_MTX_ARRAY + (opcode - (CP_CMD_LOAD_INDX_A / 0x08)); @@ -430,6 +434,7 @@ static u32 bp_get(u32 reg, u32 size, u32 shift) { return reg >> shift & (1u << s // BP register handler - decodes BP (RAS/pixel engine) register writes and updates g_gxState static void handle_bp(u32 value, bool bigEndian) { + ZoneScoped; u32 regId = (value >> 24) & 0xFF; // Mask off the register ID from the value for field extraction // (the regId is stored in bits 24-31, data is in bits 0-23) @@ -1012,6 +1017,7 @@ static void handle_bp(u32 value, bool bigEndian) { // CP register handler - decodes CP register writes and updates g_gxState static void handle_cp(u8 addr, u32 value, bool bigEndian) { + ZoneScoped; switch (addr) { // VCD low (0x50) case 0x50: { @@ -1143,6 +1149,7 @@ static void handle_cp(u8 addr, u32 value, bool bigEndian) { // XF register handler - decodes XF (transform unit) register writes and updates g_gxState static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian) { + ZoneScoped; CHECK(pos + 4 <= size, "XF header read overrun"); u32 header = read_u32(data + pos, bigEndian); pos += 4; @@ -1364,6 +1371,7 @@ static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian) { // Draw command handler - parses vertices inline and caches results static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian) { + ZoneScoped; u8 opcode = cmd & CP_OPCODE_MASK; GXVtxFmt fmt = static_cast(cmd & CP_VAT_MASK); GXPrimitive prim = static_cast(opcode); @@ -1439,6 +1447,8 @@ static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi } } + ZoneNamedN(unmerged, "Unmerged draw", true); + { ByteBuffer idxBuf; numIndices = prepare_idx_buffer(idxBuf, prim, 0, vtxCount); @@ -1501,6 +1511,7 @@ std::string read_string(const u8* data, u32& pos, u32 size, bool bigEndian) { } void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { + ZoneScoped; CHECK(pos + 2 <= size, "Aurora cmd read overrun"); u16 subCmd = read_u16(data + pos, bigEndian); pos += 2; diff --git a/lib/gx/gx.cpp b/lib/gx/gx.cpp index 46c94f4..3132c14 100644 --- a/lib/gx/gx.cpp +++ b/lib/gx/gx.cpp @@ -12,6 +12,8 @@ #include #include +#include "tracy/Tracy.hpp" + static aurora::Module Log("aurora::gx"); namespace aurora::gx { @@ -186,6 +188,7 @@ static inline wgpu::PrimitiveState to_primitive_state(GXCullMode gx_cullMode) { wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef vtxBuffers, wgpu::ShaderModule shader, const char* label) noexcept { + ZoneScoped; const wgpu::DepthStencilState depthStencil{ .format = g_graphicsConfig.depthFormat, .depthWriteEnabled = config.depthCompare && config.depthUpdate, diff --git a/lib/gx/pipeline.cpp b/lib/gx/pipeline.cpp index b851175..0ec6f36 100644 --- a/lib/gx/pipeline.cpp +++ b/lib/gx/pipeline.cpp @@ -3,11 +3,13 @@ #include "../webgpu/gpu.hpp" #include "gx_fmt.hpp" #include "shader_info.hpp" +#include "tracy/Tracy.hpp" namespace aurora::gx { static Module Log("aurora::gx"); wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) { + ZoneScoped; const auto shader = build_shader(config.shaderConfig); return build_pipeline(config, {}, shader, "GX Pipeline"); } diff --git a/lib/gx/shader.cpp b/lib/gx/shader.cpp index a7bfe43..c8fedcf 100644 --- a/lib/gx/shader.cpp +++ b/lib/gx/shader.cpp @@ -13,6 +13,8 @@ #include #include +#include "tracy/Tracy.hpp" + namespace aurora::gx { using namespace fmt::literals; using namespace std::string_literals; @@ -680,6 +682,7 @@ auto lighting_func(const ShaderConfig& config, const ColorChannelConfig& cc, u8 } wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept { + ZoneScoped; const auto hash = xxh3_hash(config); { std::lock_guard lock{g_gxCachedShadersMutex}; diff --git a/lib/imgui.cpp b/lib/imgui.cpp index 1e9ad52..34911b4 100644 --- a/lib/imgui.cpp +++ b/lib/imgui.cpp @@ -15,6 +15,7 @@ #include "backends/imgui_impl_sdl3.h" #include "backends/imgui_impl_sdlrenderer3.h" #include "backends/imgui_impl_wgpu.h" +#include "tracy/Tracy.hpp" namespace aurora::imgui { static float g_scale; @@ -36,6 +37,7 @@ void create_context() noexcept { } void initialize() noexcept { + ZoneScoped; SDL_Renderer* renderer = window::get_sdl_renderer(); ImGui_ImplSDL3_InitForSDLRenderer(window::get_sdl_window(), renderer); g_useSdlRenderer = renderer != nullptr; @@ -50,6 +52,7 @@ void initialize() noexcept { } void shutdown() noexcept { + ZoneScoped; if (g_useSdlRenderer) { ImGui_ImplSDLRenderer3_Shutdown(); } else { @@ -69,6 +72,7 @@ void process_event(const SDL_Event& event) noexcept { } void new_frame(const AuroraWindowSize& size) noexcept { + ZoneScoped; const float framebufferScaleX = size.width > 0 ? static_cast(size.native_fb_width) / static_cast(size.width) : 1.0f; const float framebufferScaleY = @@ -98,6 +102,7 @@ void new_frame(const AuroraWindowSize& size) noexcept { } void render(const wgpu::RenderPassEncoder& pass) noexcept { + ZoneScoped; ImGui::Render(); auto* data = ImGui::GetDrawData(); diff --git a/lib/internal.hpp b/lib/internal.hpp index da2a683..8c5df12 100644 --- a/lib/internal.hpp +++ b/lib/internal.hpp @@ -80,9 +80,7 @@ auto underlying(T value) -> std::underlying_type_t { return static_cast>(value); } -#ifndef ALIGN -#define ALIGN(x, a) (((x) + ((a) - 1)) & ~((a) - 1)) -#endif +#define AURORA_ALIGN(x, a) (((x) + ((a) - 1)) & ~((a) - 1)) #define POINTER_ADD_TYPE(type_, ptr_, offset_) ((type_)((uintptr_t)(ptr_) + (uintptr_t)(offset_))) #define POINTER_ADD(ptr_, offset_) POINTER_ADD_TYPE(decltype(ptr_), ptr_, offset_) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index df1fcec..988a50b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,6 +56,7 @@ if (AURORA_ENABLE_GX) absl::flat_hash_map absl::btree dawn::dawncpp_headers + TracyClient ${AURORA_SDL3_TARGET} ) From 50fda393a1754e615bd500c415f42045a1051af7 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 9 Apr 2026 03:51:06 +0200 Subject: [PATCH 2/2] GX optimizations (#107) * handle_draw optimization 1. cache vtxSize 2. remove heap allocation from draw call merging index buffer 3. move things that aren't on the "draw call merging hot path" out to other functions, to reduce stack frame size of handle_draw (it was previously using __chkstk) * Make ByteBuffer expand exponentially Fix half a second of startup time being just that. * Babysit the compiler's inlining decisions * Merged draw call idxBuf now a global static * GXFlush() doesn't write NOPs to FIFO I'm sure this made sense on actual hardware * Don't bind unused textures/samplers * Remove C++ RAII types from build_bind_groups Most of this function's code was fucking with WebGPU AddRef and ReleaseRef. Seriously. * More refs in common.hpp * Index buffer in handle_draw_unmerged now not dynamically allocated for small draws * Fix gx_test_stubs.cpp * Some cleanup & renaming --------- Co-authored-by: Luke Street --- lib/dolphin/gx/GXManage.cpp | 4 -- lib/gfx/common.cpp | 28 ++++---- lib/gfx/common.hpp | 18 +++-- lib/gx/command_processor.cpp | 134 ++++++++++++++++++++++++----------- lib/gx/gx.cpp | 48 +++++++------ lib/gx/gx.hpp | 6 ++ tests/gx_test_stubs.cpp | 4 +- 7 files changed, 153 insertions(+), 89 deletions(-) diff --git a/lib/dolphin/gx/GXManage.cpp b/lib/dolphin/gx/GXManage.cpp index f8114ba..475d6e2 100644 --- a/lib/dolphin/gx/GXManage.cpp +++ b/lib/dolphin/gx/GXManage.cpp @@ -281,10 +281,6 @@ void GXFlush() { if (__gx->dirtyState) { __GXSetDirtyState(); } - for (u32 i = 32; i > 0; i--) { - GX_WRITE_U8(0); - } - // PPCSync(); } void GXPixModeSync() { diff --git a/lib/gfx/common.cpp b/lib/gfx/common.cpp index 53bcb3b..ef94558 100644 --- a/lib/gfx/common.cpp +++ b/lib/gfx/common.cpp @@ -80,11 +80,11 @@ namespace aurora { // we create specialized methods to handle them. Note that these are highly dependent on // the structure definition, which could easily change with Dawn updates. template <> -inline HashType xxh3_hash(const wgpu::BindGroupDescriptor& input, HashType seed) { - constexpr auto offset = offsetof(wgpu::BindGroupDescriptor, layout); // skip nextInChain, label +inline HashType xxh3_hash(const WGPUBindGroupDescriptor& input, HashType seed) { + constexpr auto offset = offsetof(WGPUBindGroupDescriptor, layout); // skip nextInChain, label const auto hash = xxh3_hash_s(reinterpret_cast(&input) + offset, - sizeof(wgpu::BindGroupDescriptor) - offset - sizeof(void*) /* skip entries */, seed); - return xxh3_hash_s(input.entries, sizeof(wgpu::BindGroupEntry) * input.entryCount, hash); + sizeof(WGPUBindGroupDescriptor) - offset - sizeof(void*) /* skip entries */, seed); + return xxh3_hash_s(input.entries, sizeof(WGPUBindGroupEntry) * input.entryCount, hash); } template <> inline HashType xxh3_hash(const wgpu::SamplerDescriptor& input, HashType seed) { @@ -307,7 +307,7 @@ void push_draw_command(clear::DrawData data) { } template <> -PipelineRef pipeline_ref(clear::PipelineConfig config) { +PipelineRef pipeline_ref(const clear::PipelineConfig& config) { return find_pipeline(ShaderType::Clear, config, [=] { return create_pipeline(config); }); } @@ -506,7 +506,7 @@ void push_draw_command(gx::DrawData data) { } template <> -PipelineRef pipeline_ref(gx::PipelineConfig config) { +PipelineRef pipeline_ref(const gx::PipelineConfig& config) { return find_pipeline(ShaderType::GX, config, [=] { return create_pipeline(config); }); } @@ -618,7 +618,7 @@ void save_pipeline_cache() { file.write(reinterpret_cast(&g_serializedPipelineCount), sizeof(g_serializedPipelineCount)); file.write(reinterpret_cast(g_serializedPipelines.data()), g_serializedPipelines.size()); } - g_serializedPipelines.clear(); + g_serializedPipelines.release(); g_serializedPipelineCount = 0; } @@ -773,7 +773,7 @@ void end_frame(const wgpu::CommandEncoder& cmd) { const auto writeSize = buf.size(); // Only need to copy this many bytes if (writeSize > 0) { cmd.CopyBufferToBuffer(g_stagingBuffers[currentStagingBuffer], bufferOffset, out, 0, AURORA_ALIGN(writeSize, 4)); - buf.clear(); + buf.release(); } bufferOffset += size; return writeSize; @@ -799,7 +799,7 @@ void end_frame(const wgpu::CommandEncoder& cmd) { cmd.CopyBufferToTexture(&buf, &item.tex, &item.size); } g_textureUploads.clear(); - g_textureUpload.clear(); + g_textureUpload.release(); } currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size(); map_staging_buffer(); @@ -1067,21 +1067,21 @@ std::pair map_storage(size_t length) { } // TODO: should we avoid caching bind groups altogether? -BindGroupRef bind_group_ref(const wgpu::BindGroupDescriptor& descriptor) { +BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor) { #ifdef EMSCRIPTEN - const auto bg = g_device.CreateBindGroup(&descriptor); + const auto bg = wgpuDeviceCreateBindGroup(g_device.Get(), &descriptor); BindGroupRef id = reinterpret_cast(bg.Get()); g_cachedBindGroups.try_emplace(id, bg); #else const auto id = xxh3_hash(descriptor); if (!g_cachedBindGroups.contains(id)) { - g_cachedBindGroups.try_emplace(id, g_device.CreateBindGroup(&descriptor)); + g_cachedBindGroups.try_emplace(id, wgpuDeviceCreateBindGroup(g_device.Get(), &descriptor)); } #endif return id; } -wgpu::BindGroup find_bind_group(BindGroupRef id) { +wgpu::BindGroup& find_bind_group(BindGroupRef id) { #ifdef EMSCRIPTEN return g_cachedBindGroups[id]; #else @@ -1091,7 +1091,7 @@ wgpu::BindGroup find_bind_group(BindGroupRef id) { #endif } -wgpu::Sampler sampler_ref(const wgpu::SamplerDescriptor& descriptor) { +wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor) { const auto id = xxh3_hash(descriptor); auto it = g_cachedSamplers.find(id); if (it == g_cachedSamplers.end()) { diff --git a/lib/gfx/common.hpp b/lib/gfx/common.hpp index 3a34e6f..e22cb8d 100644 --- a/lib/gfx/common.hpp +++ b/lib/gfx/common.hpp @@ -109,7 +109,7 @@ public: m_length += size; } - void clear() { + void release() { if (m_data != nullptr && m_owned) { free(m_data); } @@ -119,6 +119,10 @@ public: m_owned = true; } + void clear() { + m_length = 0; + } + void reserve_extra(size_t size) { resize(m_length + size, true); } private: @@ -141,6 +145,10 @@ private: if (!m_owned) { abort(); } + // Exponential expansion to avoid O(n^2) time complexity. + if (size < m_capacity * 2) { + size = m_capacity * 2; + } m_data = static_cast(realloc(m_data, size)); if (zeroed) { memset(m_data + m_capacity, 0, size - m_capacity); @@ -271,13 +279,13 @@ template DrawData* get_last_draw_command(); template -PipelineRef pipeline_ref(PipelineConfig config); +PipelineRef pipeline_ref(const PipelineConfig& config); bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass); -BindGroupRef bind_group_ref(const wgpu::BindGroupDescriptor& descriptor); -wgpu::BindGroup find_bind_group(BindGroupRef id); +BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor); +wgpu::BindGroup& find_bind_group(BindGroupRef id); -wgpu::Sampler sampler_ref(const wgpu::SamplerDescriptor& descriptor); +wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor); uint32_t align_uniform(uint32_t value); diff --git a/lib/gx/command_processor.cpp b/lib/gx/command_processor.cpp index 44e394b..0a8e51d 100644 --- a/lib/gx/command_processor.cpp +++ b/lib/gx/command_processor.cpp @@ -106,20 +106,35 @@ static constexpr u8 CP_OPCODE_MASK = GX_OPCODE_MASK; static constexpr u8 CP_VAT_MASK = GX_VAT_MASK; // Read helpers for big/little endian +#if _MSC_VER +template +__forceinline // Yes, this was necessary. +inline T unaligned_load(const T* ptr) { + return *static_cast(ptr); +} +#else +template +inline T unaligned_load(const T* ptr) { + T copy; + memcpy(©, ptr, sizeof(T)); + return copy; +} +#endif + static inline u16 read_u16(const u8* ptr, bool bigEndian) { + const u16 val = unaligned_load(reinterpret_cast(ptr)); if (bigEndian) { - return static_cast(ptr[0] << 8 | ptr[1]); + return bswap(val); } - return static_cast(ptr[1] << 8 | ptr[0]); + return val; } static inline u32 read_u32(const u8* ptr, bool bigEndian) { + const u32 val = unaligned_load(reinterpret_cast(ptr)); if (bigEndian) { - return static_cast(ptr[0]) << 24 | static_cast(ptr[1]) << 16 | static_cast(ptr[2]) << 8 | - static_cast(ptr[3]); + return bswap(val); } - return static_cast(ptr[3]) << 24 | static_cast(ptr[2]) << 16 | static_cast(ptr[1]) << 8 | - static_cast(ptr[0]); + return val; } static u32 bp_get(u32 reg, u32 size, u32 shift); @@ -430,7 +445,7 @@ void process(const u8* data, u32 size, bool bigEndian) { } // Helper to extract bit fields from a 32-bit register -static u32 bp_get(u32 reg, u32 size, u32 shift) { return reg >> shift & (1u << size) - 1; } +inline static u32 bp_get(u32 reg, u32 size, u32 shift) { return reg >> shift & (1u << size) - 1; } // BP register handler - decodes BP (RAS/pixel engine) register writes and updates g_gxState static void handle_bp(u32 value, bool bigEndian) { @@ -1036,6 +1051,7 @@ static void handle_cp(u8 addr, u32 value, bool bigEndian) { vd[GX_VA_CLR0] = static_cast(bp_get(value, 2, 13)); vd[GX_VA_CLR1] = static_cast(bp_get(value, 2, 15)); g_gxState.stateDirty = true; + g_gxState.clearVtxSizeCache(); break; } @@ -1051,6 +1067,7 @@ static void handle_cp(u8 addr, u32 value, bool bigEndian) { vd[GX_VA_TEX6] = static_cast(bp_get(value, 2, 12)); vd[GX_VA_TEX7] = static_cast(bp_get(value, 2, 14)); g_gxState.stateDirty = true; + g_gxState.clearVtxSizeCache(); break; } @@ -1092,6 +1109,7 @@ static void handle_cp(u8 addr, u32 value, bool bigEndian) { vf.attrs[GX_VA_TEX0].type = static_cast(bp_get(value, 3, 22)); vf.attrs[GX_VA_TEX0].frac = static_cast(bp_get(value, 5, 25)); g_gxState.stateDirty = true; + g_gxState.clearVtxSizeCache(); } // VAT B registers (0x80-0x87) else if (addr >= 0x80 && addr <= 0x87) { @@ -1110,6 +1128,7 @@ static void handle_cp(u8 addr, u32 value, bool bigEndian) { vf.attrs[GX_VA_TEX4].type = static_cast(bp_get(value, 3, 28)); // TEX4 frac is in VAT C g_gxState.stateDirty = true; + g_gxState.clearVtxSizeCache(); } // VAT C registers (0x90-0x97) else if (addr >= 0x90 && addr <= 0x97) { @@ -1126,6 +1145,7 @@ static void handle_cp(u8 addr, u32 value, bool bigEndian) { vf.attrs[GX_VA_TEX7].type = static_cast(bp_get(value, 3, 24)); vf.attrs[GX_VA_TEX7].frac = static_cast(bp_get(value, 5, 27)); g_gxState.stateDirty = true; + g_gxState.clearVtxSizeCache(); } // Array base addresses (0xA0-0xAF) else if (addr >= 0xA0 && addr <= 0xAF) { @@ -1369,17 +1389,24 @@ static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian) { pos += dataBytes; } +static void handle_draw_overrun [[noreturn]] (u32 totalVtxBytes, const u8* data, const u32& pos, u32 size) { + // Hex dump around the draw command for debugging + u32 cmdPos = pos - 2 - 1; // opcode byte position (before vtxCount and pos++) + u32 dumpStart = (cmdPos > 16) ? cmdPos - 16 : 0; + u32 dumpEnd = (cmdPos + 32 < size) ? cmdPos + 32 : size; + std::string hex; + for (u32 i = dumpStart; i < dumpEnd; i++) { + if (i == cmdPos) + hex += fmt::format("[{:02x}]", data[i]); + else + hex += fmt::format(" {:02x}", data[i]); + } + Log.error(" hex dump around draw cmd (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex); + FATAL("draw vertex data overrun: need {} bytes at pos {}, have {}", totalVtxBytes, pos, size); +} + // Draw command handler - parses vertices inline and caches results -static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian) { - ZoneScoped; - u8 opcode = cmd & CP_OPCODE_MASK; - GXVtxFmt fmt = static_cast(cmd & CP_VAT_MASK); - GXPrimitive prim = static_cast(opcode); - - CHECK(pos + 2 <= size, "draw vtxCount read overrun"); - u16 vtxCount = read_u16(data + pos, bigEndian); - pos += 2; - +static u32 calculate_last_vtx_size(GXVtxFmt fmt) { u32 vtxSize = 0; const auto& vtxFmt = g_gxState.vtxFmts[fmt]; for (int i = GX_VA_PNMTXIDX; i <= GX_VA_TEX7; ++i) { @@ -1400,38 +1427,53 @@ static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi break; } } + + g_gxState.lastVtxFmt = fmt; + g_gxState.lastVtxSize = vtxSize; + + return vtxSize; +} + +static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange); + +// Draw command handler - parses vertices inline and caches results +static ByteBuffer handle_draw_idx_buf; + +static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian) { + ZoneScoped; + u8 opcode = cmd & CP_OPCODE_MASK; + GXVtxFmt fmt = static_cast(cmd & CP_VAT_MASK); + GXPrimitive prim = static_cast(opcode); + + CHECK(pos + 2 <= size, "draw vtxCount read overrun"); + u16 vtxCount = read_u16(data + pos, bigEndian); + pos += 2; + + u32 vtxSize; + if (g_gxState.lastVtxFmt == fmt) LIKELY { + vtxSize = g_gxState.lastVtxSize; + } else UNLIKELY { + vtxSize = calculate_last_vtx_size(fmt); + } + u32 totalVtxBytes = vtxCount * vtxSize; - if (pos + totalVtxBytes > size) { - // Hex dump around the draw command for debugging - u32 cmdPos = pos - 2 - 1; // opcode byte position (before vtxCount and pos++) - u32 dumpStart = (cmdPos > 16) ? cmdPos - 16 : 0; - u32 dumpEnd = (cmdPos + 32 < size) ? cmdPos + 32 : size; - std::string hex; - for (u32 i = dumpStart; i < dumpEnd; i++) { - if (i == cmdPos) - hex += fmt::format("[{:02x}]", data[i]); - else - hex += fmt::format(" {:02x}", data[i]); - } - Log.error(" hex dump around draw cmd (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex); - FATAL("draw vertex data overrun: need {} bytes at pos {}, have {}", totalVtxBytes, pos, size); + if (pos + totalVtxBytes > size) UNLIKELY { + handle_draw_overrun(totalVtxBytes, data, pos, size); } // Push raw vertex data to buffer gfx::Range vertRange = gfx::push_verts(data + pos, totalVtxBytes); pos += totalVtxBytes; - u32 numIndices = 0; - gfx::Range idxRange; // Try to merge with previous draw call - if (!g_gxState.stateDirty) { + if (!g_gxState.stateDirty) LIKELY { auto* lastDraw = gfx::get_last_draw_command(); // Only if the previous draw call was a single instance draw (no lines/points handling) if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS && - lastDraw->instanceCount == 1) { - ByteBuffer idxBuf; - numIndices = prepare_idx_buffer(idxBuf, prim, lastDraw->vtxCount, vtxCount); - idxRange = gfx::push_indices(idxBuf.data(), idxBuf.size()); + lastDraw->instanceCount == 1) LIKELY { + u32 numIndices = prepare_idx_buffer(handle_draw_idx_buf, prim, lastDraw->vtxCount, vtxCount); + gfx::Range idxRange = gfx::push_indices(handle_draw_idx_buf.data(), handle_draw_idx_buf.size()); + handle_draw_idx_buf.clear(); CHECK(lastDraw->vertRange.offset + lastDraw->vertRange.size == vertRange.offset, "Non-consecutive vertex ranges ({} < {})", lastDraw->vertRange.offset + lastDraw->vertRange.size, vertRange.offset); @@ -1447,12 +1489,22 @@ static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi } } - ZoneNamedN(unmerged, "Unmerged draw", true); + handle_draw_unmerged(prim, fmt, vtxCount, vertRange); +} + +static ByteBuffer handle_draw_unmerged_idxBuf; + +static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange) { + ZoneScoped; + u32 numIndices = 0; + gfx::Range idxRange; { ByteBuffer idxBuf; - numIndices = prepare_idx_buffer(idxBuf, prim, 0, vtxCount); - idxRange = gfx::push_indices(idxBuf.data(), idxBuf.size()); + auto& realBuf = vtxCount < 1000 ? handle_draw_unmerged_idxBuf : idxBuf; + numIndices = prepare_idx_buffer(realBuf, prim, 0, vtxCount); + idxRange = gfx::push_indices(realBuf.data(), realBuf.size()); + realBuf.clear(); } // Build pipeline, bind groups, and push draw command diff --git a/lib/gx/gx.cpp b/lib/gx/gx.cpp index 3132c14..bc81678 100644 --- a/lib/gx/gx.cpp +++ b/lib/gx/gx.cpp @@ -442,11 +442,13 @@ GXBindGroups build_bind_groups(const ShaderInfo& info, const ShaderConfig& confi const BindGroupRanges& ranges) noexcept { const auto layouts = build_bind_group_layouts(config); - std::array uniformEntries; + // Using C WGPU types instead of C++ wrappers to avoid destructor overhead + std::array uniformEntries{}; uniformEntries[0].binding = 0; - uniformEntries[0].buffer = gfx::g_vertexBuffer; + uniformEntries[0].buffer = gfx::g_vertexBuffer.Get(); + uniformEntries[0].size = WGPU_WHOLE_SIZE; uniformEntries[1].binding = 1; - uniformEntries[1].buffer = gfx::g_uniformBuffer; + uniformEntries[1].buffer = gfx::g_uniformBuffer.Get(); uniformEntries[1].size = info.uniformSize; u32 uniformBindIdx = 2; for (u32 i = 0; i < MaxIndexAttr; ++i) { @@ -454,46 +456,46 @@ GXBindGroups build_bind_groups(const ShaderInfo& info, const ShaderConfig& confi if (range.size <= 0) { continue; } - wgpu::BindGroupEntry& entry = uniformEntries[uniformBindIdx]; + WGPUBindGroupEntry& entry = uniformEntries[uniformBindIdx]; entry.binding = uniformBindIdx; - entry.buffer = gfx::g_storageBuffer; + entry.buffer = gfx::g_storageBuffer.Get(); entry.size = range.size; ++uniformBindIdx; } - std::array samplerEntries; - std::array textureEntries; + std::array samplerEntries{}; + std::array textureEntries{}; u32 textureCount = 0; for (u32 i = 0; i < MaxTextures; ++i) { const auto& tex = g_gxState.textures[i]; - wgpu::BindGroupEntry& samplerEntry = samplerEntries[textureCount]; - wgpu::BindGroupEntry& textureEntry = textureEntries[textureCount]; + WGPUBindGroupEntry& samplerEntry = samplerEntries[textureCount]; + WGPUBindGroupEntry& textureEntry = textureEntries[textureCount]; samplerEntry.binding = textureCount; textureEntry.binding = textureCount; - if (tex) { - samplerEntry.sampler = gfx::sampler_ref(tex.get_descriptor()); - textureEntry.textureView = tex.texObj.ref->sampleTextureView; + if (tex && (info.sampledTextures[i] || info.sampledIndTextures[i])) { + samplerEntry.sampler = gfx::sampler_ref(tex.get_descriptor()).Get(); + textureEntry.textureView = tex.texObj.ref->sampleTextureView.Get(); } else { - samplerEntry.sampler = sEmptySampler; - textureEntry.textureView = sEmptyTextureView; + samplerEntry.sampler = sEmptySampler.Get(); + textureEntry.textureView = sEmptyTextureView.Get(); } ++textureCount; } - const wgpu::BindGroupDescriptor uniformBindGroupDescriptor{ - .label = "GX Uniform Bind Group", - .layout = layouts.uniformLayout, + const WGPUBindGroupDescriptor uniformBindGroupDescriptor{ + .label = {"GX Uniform Bind Group", WGPU_STRLEN}, + .layout = layouts.uniformLayout.Get(), .entryCount = uniformBindIdx, .entries = uniformEntries.data(), }; - const wgpu::BindGroupDescriptor samplerBindGroupDescriptor{ - .label = "GX Sampler Bind Group", - .layout = layouts.samplerLayout, + const WGPUBindGroupDescriptor samplerBindGroupDescriptor{ + .label = {"GX Sampler Bind Group", WGPU_STRLEN}, + .layout = layouts.samplerLayout.Get(), .entryCount = textureCount, .entries = samplerEntries.data(), }; - const wgpu::BindGroupDescriptor textureBindGroupDescriptor{ - .label = "GX Texture Bind Group", - .layout = layouts.textureLayout, + const WGPUBindGroupDescriptor textureBindGroupDescriptor{ + .label = {"GX Texture Bind Group", WGPU_STRLEN}, + .layout = layouts.textureLayout.Get(), .entryCount = textureCount, .entries = textureEntries.data(), }; diff --git a/lib/gx/gx.hpp b/lib/gx/gx.hpp index 78d2c80..82f800f 100644 --- a/lib/gx/gx.hpp +++ b/lib/gx/gx.hpp @@ -315,6 +315,8 @@ struct GXState { std::array, MaxPTTexMtx> ptTexMtxs; std::array tcgs; std::array texCoordScales; + u16 lastVtxSize = 0; + GXVtxFmt lastVtxFmt = GX_MAX_VTXFMT; std::array vtxDesc; std::array vtxFmts; std::array tevSwapTable{ @@ -358,6 +360,10 @@ struct GXState { bool stateDirty = true; std::array bpRegCache; GXState() { bpRegCache[0xFE] = 0x00FFFFFF; } + + void clearVtxSizeCache() { + lastVtxFmt = GX_MAX_VTXFMT; + } }; extern GXState g_gxState; diff --git a/tests/gx_test_stubs.cpp b/tests/gx_test_stubs.cpp index 999edd1..bfa5bb7 100644 --- a/tests/gx_test_stubs.cpp +++ b/tests/gx_test_stubs.cpp @@ -97,7 +97,7 @@ void set_scissor(uint32_t x, uint32_t y, uint32_t w, uint32_t h) noexcept {} // --- Pipeline/draw command stubs --- namespace aurora::gfx { template <> -PipelineRef pipeline_ref(clear::PipelineConfig config) { +PipelineRef pipeline_ref(const clear::PipelineConfig& config) { return 0; } template <> @@ -105,7 +105,7 @@ void push_draw_command(clear::DrawData data) { // No-op } template <> -PipelineRef pipeline_ref(gx::PipelineConfig config) { +PipelineRef pipeline_ref(const gx::PipelineConfig& config) { return 0; } template <>