Files
aurora/lib/gfx/common.cpp
T

1041 lines
36 KiB
C++
Raw Normal View History

2022-07-27 11:25:25 -04:00
#include "common.hpp"
2026-04-01 00:49:28 -06:00
#include "clear.hpp"
2022-07-27 11:25:25 -04:00
#include "../internal.hpp"
#include "../webgpu/gpu.hpp"
#include "../gx/pipeline.hpp"
2026-04-08 20:03:25 -06:00
#include "pipeline_cache.hpp"
2026-04-01 00:49:28 -06:00
#include "tex_copy_conv.hpp"
#include "tex_palette_conv.hpp"
#include "texture_replacement.hpp"
2022-07-27 11:25:25 -04:00
#include "texture.hpp"
#include "../window.hpp"
2022-07-27 11:25:25 -04:00
#include <optional>
2026-03-11 01:38:35 +01:00
#include <ranges>
2025-04-04 01:59:30 -06:00
#include <absl/container/flat_hash_map.h>
2022-07-27 11:25:25 -04:00
#include <magic_enum.hpp>
2026-04-09 03:19:58 +02:00
#include "tracy/Tracy.hpp"
2022-07-27 11:25:25 -04:00
namespace aurora::gfx {
static Module Log("aurora::gfx");
using webgpu::g_device;
using webgpu::g_instance;
2022-07-27 11:25:25 -04:00
using webgpu::g_queue;
#ifdef AURORA_GFX_DEBUG_GROUPS
std::vector<std::string> g_debugGroupStack;
2026-03-11 01:38:35 +01:00
std::vector<std::string> g_debugMarkers;
2022-07-27 11:25:25 -04:00
#endif
2026-04-10 18:18:29 -06:00
constexpr uint64_t StagingBufferSize = UniformBufferSize + VertexBufferSize + IndexBufferSize + StorageBufferSize +
(UseTextureBuffer ? TextureUploadSize : 0);
2022-07-27 11:25:25 -04:00
struct ShaderDrawCommand {
ShaderType type;
union {
2026-04-01 00:49:28 -06:00
clear::DrawData clear;
gx::DrawData gx;
2022-07-27 11:25:25 -04:00
};
};
enum class CommandType {
SetViewport,
SetScissor,
Draw,
2026-03-11 01:38:35 +01:00
DebugMarker,
2022-07-27 11:25:25 -04:00
};
struct Command {
CommandType type;
#ifdef AURORA_GFX_DEBUG_GROUPS
std::vector<std::string> debugGroupStack;
#endif
union Data {
Viewport setViewport;
ClipRect setScissor;
2022-07-27 11:25:25 -04:00
ShaderDrawCommand draw;
2026-03-11 01:38:35 +01:00
size_t debugMarkerIndex;
2022-07-27 11:25:25 -04:00
} data;
};
} // namespace aurora::gfx
namespace aurora {
// For types that we can't ensure are safe to hash with has_unique_object_representations,
// 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 <>
2026-04-09 03:51:06 +02:00
inline HashType xxh3_hash(const WGPUBindGroupDescriptor& input, HashType seed) {
constexpr auto offset = offsetof(WGPUBindGroupDescriptor, layout); // skip nextInChain, label
2022-07-27 11:25:25 -04:00
const auto hash = xxh3_hash_s(reinterpret_cast<const u8*>(&input) + offset,
2026-04-09 03:51:06 +02:00
sizeof(WGPUBindGroupDescriptor) - offset - sizeof(void*) /* skip entries */, seed);
return xxh3_hash_s(input.entries, sizeof(WGPUBindGroupEntry) * input.entryCount, hash);
2022-07-27 11:25:25 -04:00
}
template <>
inline HashType xxh3_hash(const wgpu::SamplerDescriptor& input, HashType seed) {
2025-04-04 01:59:30 -06:00
constexpr auto offset = offsetof(wgpu::SamplerDescriptor, addressModeU); // skip nextInChain, label
2022-07-27 11:25:25 -04:00
return xxh3_hash_s(reinterpret_cast<const u8*>(&input) + offset,
sizeof(wgpu::SamplerDescriptor) - offset - 2 /* skip padding */, seed);
2022-07-27 11:25:25 -04:00
}
} // namespace aurora
namespace aurora::gfx {
2026-04-12 18:55:35 -06:00
namespace {
struct CachedBindGroup {
wgpu::BindGroup bindGroup;
uint32_t lastUsedFrame = 0;
};
constexpr uint32_t BindGroupCacheRetainFrames = 120;
constexpr uint32_t BindGroupCacheSweepPeriod = 32;
} // namespace
static absl::flat_hash_map<BindGroupRef, CachedBindGroup> g_cachedBindGroups;
static absl::flat_hash_map<SamplerRef, wgpu::Sampler> g_cachedSamplers;
2022-07-27 11:25:25 -04:00
static ByteBuffer g_verts;
static ByteBuffer g_uniforms;
static ByteBuffer g_indices;
static ByteBuffer g_storage;
static ByteBuffer g_textureUpload;
wgpu::Buffer g_vertexBuffer;
wgpu::Buffer g_uniformBuffer;
wgpu::Buffer g_indexBuffer;
wgpu::Buffer g_storageBuffer;
static std::array<wgpu::Buffer, 3> g_stagingBuffers;
2025-04-03 00:12:22 -06:00
static wgpu::Limits g_cachedLimits;
2026-04-08 20:03:25 -06:00
static uint32_t g_frameIndex = UINT32_MAX;
2022-07-27 11:25:25 -04:00
static PipelineRef g_currentPipeline;
2026-04-12 18:55:35 -06:00
wgpu::BindGroupLayout g_staticBindGroupLayout;
wgpu::BindGroup g_staticBindGroup;
wgpu::BindGroupLayout g_uniformBindGroupLayout;
wgpu::BindGroup g_uniformBindGroup;
2022-07-27 11:25:25 -04:00
// for imgui debug
AuroraStats g_stats{};
2022-07-27 11:25:25 -04:00
using CommandList = std::vector<Command>;
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;
2022-08-09 02:05:33 -04:00
TextureHandle resolveTarget;
GXTexFmt resolveFormat = GX_TF_RGBA8;
2022-07-27 11:25:25 -04:00
ClipRect resolveRect;
Range resolveUniformRange;
2026-04-01 00:49:28 -06:00
Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f};
float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f;
2022-07-27 11:25:25 -04:00
CommandList commands;
2026-04-01 00:49:28 -06:00
bool clearColor = true;
bool clearDepth = true;
std::vector<tex_palette_conv::ConvRequest> paletteConvs;
2022-07-27 11:25:25 -04:00
};
static std::vector<RenderPass> g_renderPasses;
static u32 g_currentRenderPass = UINT32_MAX;
static bool g_inOffscreen = false;
static std::optional<RenderPass> g_suspendedEfbPass;
static Viewport g_suspendedEfbViewport;
static ClipRect g_suspendedEfbScissor;
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;
}
2026-04-07 19:01:41 -06:00
struct OffscreenCacheKey {
uint32_t width;
uint32_t height;
2026-04-07 19:01:41 -06:00
bool operator==(const OffscreenCacheKey& rhs) const { return width == rhs.width && height == rhs.height; }
template <typename H>
2026-04-07 19:01:41 -06:00
friend H AbslHashValue(H h, const OffscreenCacheKey& key) {
return H::combine(std::move(h), key.width, key.height);
}
};
2026-04-07 19:01:41 -06:00
struct OffscreenCacheEntry {
webgpu::TextureWithSampler color;
webgpu::TextureWithSampler depth;
};
static absl::flat_hash_map<OffscreenCacheKey, OffscreenCacheEntry> g_offscreenCache;
2022-07-27 11:25:25 -04:00
std::vector<TextureUpload> g_textureUploads;
static inline void push_command(CommandType type, const Command::Data& data) {
2022-08-09 02:05:33 -04:00
if (g_currentRenderPass == UINT32_MAX)
UNLIKELY {
2025-04-06 16:37:05 -06:00
Log.warn("Dropping command {}", magic_enum::enum_name(type));
2022-08-09 02:05:33 -04:00
return;
}
2022-07-27 11:25:25 -04:00
g_renderPasses[g_currentRenderPass].commands.push_back({
.type = type,
#ifdef AURORA_GFX_DEBUG_GROUPS
.debugGroupStack = g_debugGroupStack,
#endif
.data = data,
});
}
2025-04-04 01:59:30 -06:00
template <>
gx::DrawData* get_last_draw_command() {
if (g_currentRenderPass >= g_renderPasses.size()) {
return nullptr;
}
auto& last = g_renderPasses[g_currentRenderPass].commands.back();
if (last.type != CommandType::Draw || last.data.draw.type != ShaderType::GX) {
return nullptr;
}
return &last.data.draw.gx;
}
2022-07-27 11:25:25 -04:00
static void push_draw_command(ShaderDrawCommand data) {
push_command(CommandType::Draw, Command::Data{.draw = data});
++g_stats.drawCallCount;
}
2022-07-27 11:25:25 -04:00
Vec2<uint32_t> get_render_target_size() noexcept {
if (g_currentRenderPass < g_renderPasses.size()) {
const auto& size = g_renderPasses[g_currentRenderPass].targetSize;
return {size.width, size.height};
}
const auto windowSize = window::get_window_size();
return {windowSize.fb_width, windowSize.fb_height};
}
2026-03-05 18:09:26 +01:00
static Viewport g_cachedViewport;
void set_viewport(const Viewport& cmd) noexcept {
2022-07-27 11:25:25 -04:00
if (cmd != g_cachedViewport) {
push_command(CommandType::SetViewport, Command::Data{.setViewport = cmd});
g_cachedViewport = cmd;
}
}
2025-04-04 01:59:30 -06:00
static ClipRect g_cachedScissor;
void set_scissor(const ClipRect& cmd) noexcept {
2022-07-27 11:25:25 -04:00
if (cmd != g_cachedScissor) {
push_command(CommandType::SetScissor, Command::Data{.setScissor = cmd});
g_cachedScissor = cmd;
}
}
2026-04-01 00:49:28 -06:00
template <>
void push_draw_command(clear::DrawData data) {
push_draw_command(ShaderDrawCommand{.type = ShaderType::Clear, .clear = data});
}
template <>
2026-04-09 03:51:06 +02:00
PipelineRef pipeline_ref(const clear::PipelineConfig& config) {
2026-04-01 00:49:28 -06:00
return find_pipeline(ShaderType::Clear, config, [=] { return create_pipeline(config); });
}
void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth,
Vec4<float> 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<float>(prevPass.targetSize.width);
const auto srcH = static_cast<float>(prevPass.targetSize.height);
const std::array uvTransform{
static_cast<float>(rect.x) / srcW,
static_cast<float>(rect.y) / srcH,
static_cast<float>(rect.width) / srcW,
static_cast<float>(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));
2022-07-27 11:25:25 -04:00
++g_currentRenderPass;
2026-04-01 00:49:28 -06:00
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,
2026-04-01 00:49:28 -06:00
.clearColor = clearColor,
.clearAlpha = clearAlpha,
.clearDepth = false, // Depth cleared via render attachment
}),
.color =
wgpu::Color{
.r = clearColorValue.x(),
.g = clearColorValue.y(),
.b = clearColorValue.z(),
.a = clearColorValue.w(),
},
});
}
2026-03-07 20:35:33 -08:00
push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport});
push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor});
2022-07-27 11:25:25 -04:00
}
2026-04-01 00:49:28 -06:00
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;
}
2026-04-07 19:01:41 -06:00
void clear_offscreen_cache() { g_offscreenCache.clear(); }
2026-04-07 19:01:41 -06:00
static OffscreenCacheEntry get_offscreen_textures(uint32_t width, uint32_t height) {
OffscreenCacheKey key{width, height};
if (const auto it = g_offscreenCache.find(key); it != g_offscreenCache.end()) {
return it->second;
}
const auto colorFormat = webgpu::g_graphicsConfig.surfaceConfiguration.format;
const wgpu::Extent3D size{width, height, 1};
2026-04-07 19:01:41 -06:00
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,
2026-04-07 19:01:41 -06:00
.mipLevelCount = 1,
.sampleCount = 1,
};
auto colorTexture = g_device.CreateTexture(&colorDesc);
auto colorView = colorTexture.CreateView();
webgpu::TextureWithSampler color{
.texture = std::move(colorTexture),
.view = std::move(colorView),
.size = size,
.format = colorFormat,
2026-04-07 19:01:41 -06:00
};
const auto depthFormat = webgpu::g_graphicsConfig.depthFormat;
2026-04-07 19:01:41 -06:00
const wgpu::TextureDescriptor depthDesc{
.label = "Offscreen Depth",
.usage = wgpu::TextureUsage::RenderAttachment,
.dimension = wgpu::TextureDimension::e2D,
.size = size,
.format = depthFormat,
.mipLevelCount = 1,
.sampleCount = 1,
};
2026-04-07 19:01:41 -06:00
auto depthTexture = g_device.CreateTexture(&depthDesc);
auto depthView = depthTexture.CreateView();
webgpu::TextureWithSampler depth{
.texture = std::move(depthTexture),
.view = std::move(depthView),
.size = size,
.format = depthFormat,
};
2026-04-07 19:01:41 -06:00
OffscreenCacheEntry entry{
.color = std::move(color),
.depth = std::move(depth),
};
auto [insertIt, _] = g_offscreenCache.emplace(key, std::move(entry));
return insertIt->second;
}
void begin_offscreen(uint32_t width, uint32_t height) {
2026-04-09 03:19:58 +02:00
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.
// 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;
}
g_suspendedEfbViewport = g_cachedViewport;
g_suspendedEfbScissor = g_cachedScissor;
}
2026-04-07 19:01:41 -06:00
// Create offscreen textures
auto offscreenEntry = get_offscreen_textures(width, height);
g_offscreenColor = std::move(offscreenEntry.color);
g_offscreenDepth = std::move(offscreenEntry.depth);
// 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,
2026-04-07 19:01:41 -06:00
.targetSize = {width, height, 1},
.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;
g_cachedViewport = {0.f, 0.f, static_cast<float>(width), static_cast<float>(height), 0.f, 1.f};
g_cachedScissor = {0, 0, static_cast<int32_t>(width), static_cast<int32_t>(height)};
push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport});
push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor});
}
void end_offscreen() {
2026-04-09 03:19:58 +02:00
ZoneScoped;
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]);
g_cachedViewport = g_suspendedEfbViewport;
g_cachedScissor = g_suspendedEfbScissor;
push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport});
push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor});
}
2022-07-27 11:25:25 -04:00
template <>
void push_draw_command(gx::DrawData data) {
push_draw_command(ShaderDrawCommand{.type = ShaderType::GX, .gx = data});
2022-07-27 11:25:25 -04:00
}
2025-04-04 01:59:30 -06:00
2022-07-27 11:25:25 -04:00
template <>
2026-04-09 03:51:06 +02:00
PipelineRef pipeline_ref(const gx::PipelineConfig& config) {
2026-04-01 00:49:28 -06:00
return find_pipeline(ShaderType::GX, config, [=] { return create_pipeline(config); });
2022-07-27 11:25:25 -04:00
}
void initialize() {
2026-04-08 20:03:25 -06:00
g_frameIndex = 0;
2026-04-01 00:49:28 -06:00
tex_copy_conv::initialize();
tex_palette_conv::initialize();
texture_replacement::initialize();
2026-04-01 00:49:28 -06:00
2022-07-27 11:25:25 -04:00
// For uniform & storage buffer offset alignments
g_device.GetLimits(&g_cachedLimits);
2022-07-27 11:25:25 -04:00
const auto createBuffer = [](wgpu::Buffer& out, wgpu::BufferUsage usage, uint64_t size, const char* label) {
2022-07-27 11:25:25 -04:00
if (size <= 0) {
return;
}
const wgpu::BufferDescriptor descriptor{
2022-07-27 11:25:25 -04:00
.label = label,
.usage = usage,
.size = size,
};
out = g_device.CreateBuffer(&descriptor);
2022-07-27 11:25:25 -04:00
};
createBuffer(g_uniformBuffer, wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst, UniformBufferSize,
2022-07-27 11:25:25 -04:00
"Shared Uniform Buffer");
2026-03-18 23:58:00 -06:00
createBuffer(g_vertexBuffer, wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst, VertexBufferSize,
2022-07-27 11:25:25 -04:00
"Shared Vertex Buffer");
createBuffer(g_indexBuffer, wgpu::BufferUsage::Index | wgpu::BufferUsage::CopyDst, IndexBufferSize,
"Shared Index Buffer");
createBuffer(g_storageBuffer, wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst, StorageBufferSize,
2022-07-27 11:25:25 -04:00
"Shared Storage Buffer");
for (int i = 0; i < g_stagingBuffers.size(); ++i) {
2025-04-03 21:03:08 -06:00
const auto label = fmt::format("Staging Buffer {}", i);
createBuffer(g_stagingBuffers[i], wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc, StagingBufferSize,
2022-07-27 11:25:25 -04:00
label.c_str());
}
map_staging_buffer();
2026-04-12 18:55:35 -06:00
{
constexpr std::array layoutEntries{
// Vertex data buffer
wgpu::BindGroupLayoutEntry{
.binding = 0,
.visibility = wgpu::ShaderStage::Vertex,
.buffer =
wgpu::BufferBindingLayout{
.type = wgpu::BufferBindingType::ReadOnlyStorage,
},
},
// Storage data buffer
wgpu::BindGroupLayoutEntry{
.binding = 1,
.visibility = wgpu::ShaderStage::Vertex,
.buffer =
wgpu::BufferBindingLayout{
.type = wgpu::BufferBindingType::ReadOnlyStorage,
},
},
};
const wgpu::BindGroupLayoutDescriptor layoutDesc{
.label = "Static bind group layout",
.entryCount = layoutEntries.size(),
.entries = layoutEntries.data(),
};
g_staticBindGroupLayout = g_device.CreateBindGroupLayout(&layoutDesc);
const std::array entries{
wgpu::BindGroupEntry{
.binding = 0,
.buffer = g_vertexBuffer,
},
wgpu::BindGroupEntry{
.binding = 1,
.buffer = g_storageBuffer,
},
};
const wgpu::BindGroupDescriptor bindGroupDescriptor{
.label = "Static bind group",
.layout = g_staticBindGroupLayout,
.entryCount = entries.size(),
.entries = entries.data(),
};
g_staticBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor);
}
{
constexpr std::array layoutEntries{
// Uniform buffer (dynamic offset)
wgpu::BindGroupLayoutEntry{
.binding = 0,
.visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment,
.buffer =
wgpu::BufferBindingLayout{
.type = wgpu::BufferBindingType::Uniform,
.hasDynamicOffset = true,
},
},
};
const wgpu::BindGroupLayoutDescriptor layoutDesc{
.label = "Uniform bind group layout",
.entryCount = layoutEntries.size(),
.entries = layoutEntries.data(),
};
g_uniformBindGroupLayout = g_device.CreateBindGroupLayout(&layoutDesc);
const std::array entries{
wgpu::BindGroupEntry{
.binding = 0,
.buffer = g_uniformBuffer,
.size = gx::MaxUniformSize,
},
};
const wgpu::BindGroupDescriptor bindGroupDescriptor{
.label = "Uniform bind group",
.layout = g_uniformBindGroupLayout,
.entryCount = entries.size(),
.entries = entries.data(),
};
g_uniformBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor);
}
2026-04-01 00:49:28 -06:00
gx::initialize();
2026-04-08 20:03:25 -06:00
initialize_pipeline_cache();
2022-07-27 11:25:25 -04:00
}
void shutdown() {
2026-04-08 20:03:25 -06:00
shutdown_pipeline_cache();
2026-04-01 00:49:28 -06:00
tex_copy_conv::shutdown();
tex_palette_conv::shutdown();
texture_replacement::shutdown();
2022-07-27 11:25:25 -04:00
gx::shutdown();
g_textureUploads.clear();
g_cachedBindGroups.clear();
g_cachedSamplers.clear();
g_vertexBuffer = {};
g_uniformBuffer = {};
g_indexBuffer = {};
g_storageBuffer = {};
g_stagingBuffers.fill({});
2022-07-27 11:25:25 -04:00
g_renderPasses.clear();
g_currentRenderPass = UINT32_MAX;
2026-04-07 19:01:41 -06:00
g_offscreenCache.clear();
g_offscreenColor = {};
g_offscreenDepth = {};
2026-04-12 18:55:35 -06:00
g_staticBindGroup = {};
g_staticBindGroupLayout = {};
g_uniformBindGroup = {};
g_uniformBindGroupLayout = {};
g_inOffscreen = false;
2026-04-08 20:03:25 -06:00
g_frameIndex = UINT32_MAX;
2022-07-27 11:25:25 -04:00
}
static size_t currentStagingBuffer = 0;
static bool bufferMapped = false;
void map_staging_buffer() {
bufferMapped = false;
g_stagingBuffers[currentStagingBuffer].MapAsync(
2025-04-03 00:12:22 -06:00
wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous,
[](wgpu::MapAsyncStatus status, wgpu::StringView message) {
if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) {
Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message);
2022-07-27 11:25:25 -04:00
return;
}
2025-04-03 00:12:22 -06:00
ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status),
message);
bufferMapped = true;
});
2022-07-27 11:25:25 -04:00
}
void begin_frame() {
2026-04-09 03:19:58 +02:00
ZoneScoped;
{
ZoneScopedN("Wait for buffer map");
while (!bufferMapped) {
g_instance.ProcessEvents();
}
2022-07-27 11:25:25 -04:00
}
size_t bufferOffset = 0;
2025-04-07 20:12:31 -06:00
const auto& stagingBuf = g_stagingBuffers[currentStagingBuffer];
2022-07-27 11:25:25 -04:00
const auto mapBuffer = [&](ByteBuffer& buf, uint64_t size) {
if (size <= 0) {
return;
}
buf = ByteBuffer{static_cast<u8*>(stagingBuf.GetMappedRange(bufferOffset, size)), static_cast<size_t>(size)};
2022-07-27 11:25:25 -04:00
bufferOffset += size;
};
mapBuffer(g_verts, VertexBufferSize);
mapBuffer(g_uniforms, UniformBufferSize);
mapBuffer(g_indices, IndexBufferSize);
mapBuffer(g_storage, StorageBufferSize);
2026-04-10 18:18:29 -06:00
if constexpr (UseTextureBuffer) {
mapBuffer(g_textureUpload, TextureUploadSize);
}
2022-07-27 11:25:25 -04:00
g_stats.drawCallCount = 0;
g_stats.mergedDrawCallCount = 0;
g_suspendedEfbPass.reset();
2022-07-27 11:25:25 -04:00
g_renderPasses.emplace_back();
set_efb_targets(g_renderPasses[0]);
2026-04-01 00:49:28 -06:00
g_renderPasses[0].clearColorValue = gx::g_gxState.clearColor;
g_renderPasses[0].clearDepthValue = gx::clear_depth_value();
2022-07-27 11:25:25 -04:00
g_currentRenderPass = 0;
// Refresh render viewport/scissor from logical in case FB size changed
g_cachedViewport = gx::map_logical_viewport(gx::g_gxState.logicalViewport);
g_cachedScissor = gx::map_logical_scissor(gx::g_gxState.logicalScissor);
2026-03-07 20:35:33 -08:00
push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport});
push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor});
2026-04-08 20:03:25 -06:00
begin_pipeline_frame();
2022-07-27 11:25:25 -04:00
}
void end_frame(const wgpu::CommandEncoder& cmd) {
2026-04-09 03:19:58 +02:00
ZoneScoped;
ASSERT(!g_inOffscreen, "end_frame called while offscreen rendering is active");
2026-04-12 18:55:35 -06:00
g_uniforms.append_zeroes(gx::MaxUniformSize); // Pad the end of the buffer
2022-07-27 11:25:25 -04:00
uint64_t bufferOffset = 0;
const auto writeBuffer = [&](ByteBuffer& buf, wgpu::Buffer& out, uint64_t size, std::string_view label) {
2022-07-27 11:25:25 -04:00
const auto writeSize = buf.size(); // Only need to copy this many bytes
if (writeSize > 0) {
2026-04-09 03:19:58 +02:00
cmd.CopyBufferToBuffer(g_stagingBuffers[currentStagingBuffer], bufferOffset, out, 0, AURORA_ALIGN(writeSize, 4));
2026-04-09 03:51:06 +02:00
buf.release();
2022-07-27 11:25:25 -04:00
}
bufferOffset += size;
return writeSize;
};
g_stagingBuffers[currentStagingBuffer].Unmap();
g_stats.lastVertSize = writeBuffer(g_verts, g_vertexBuffer, VertexBufferSize, "Vertex");
g_stats.lastUniformSize = writeBuffer(g_uniforms, g_uniformBuffer, UniformBufferSize, "Uniform");
g_stats.lastIndexSize = writeBuffer(g_indices, g_indexBuffer, IndexBufferSize, "Index");
g_stats.lastStorageSize = writeBuffer(g_storage, g_storageBuffer, StorageBufferSize, "Storage");
2026-04-10 18:18:29 -06:00
if constexpr (UseTextureBuffer) {
g_stats.lastTextureUploadSize = g_textureUpload.size();
{
// Perform texture copies
for (const auto& item : g_textureUploads) {
const wgpu::TexelCopyBufferInfo buf{
.layout =
wgpu::TexelCopyBufferLayout{
.offset = item.layout.offset + bufferOffset,
.bytesPerRow = AURORA_ALIGN(item.layout.bytesPerRow, 256),
.rowsPerImage = item.layout.rowsPerImage,
},
.buffer = g_stagingBuffers[currentStagingBuffer],
};
cmd.CopyBufferToTexture(&buf, &item.tex, &item.size);
}
g_textureUploads.clear();
g_textureUpload.release();
2022-07-27 11:25:25 -04:00
}
}
currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size();
map_staging_buffer();
g_currentRenderPass = UINT32_MAX;
for (auto& array : gx::g_gxState.arrays) {
array.cachedRange = {};
}
2026-04-08 20:03:25 -06:00
end_pipeline_frame();
++g_frameIndex;
2022-07-27 11:25:25 -04:00
}
2026-04-08 20:03:25 -06:00
uint32_t current_frame() noexcept { return g_frameIndex; }
2026-04-12 18:55:35 -06:00
static void expire_cached_bind_groups() {
if (g_cachedBindGroups.empty() || g_frameIndex == UINT32_MAX || g_frameIndex % BindGroupCacheSweepPeriod != 0) {
return;
}
for (auto it = g_cachedBindGroups.begin(); it != g_cachedBindGroups.end();) {
if (g_frameIndex - it->second.lastUsedFrame > BindGroupCacheRetainFrames) {
g_cachedBindGroups.erase(it++);
} else {
++it;
}
}
}
void render(wgpu::CommandEncoder& cmd) {
2026-04-09 03:19:58 +02:00
ZoneScoped;
2022-07-27 11:25:25 -04:00
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);
}
2022-08-09 02:05:33 -04:00
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;
2022-07-27 11:25:25 -04:00
}
2022-07-27 11:25:25 -04:00
const std::array attachments{
wgpu::RenderPassColorAttachment{
.view = passInfo.colorView,
.resolveTarget = passInfo.resolveView,
2026-04-01 00:49:28 -06:00
.loadOp = passInfo.clearColor ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load,
.storeOp = wgpu::StoreOp::Store,
2022-07-27 11:25:25 -04:00
.clearValue =
{
2026-04-01 00:49:28 -06:00
.r = passInfo.clearColorValue.x(),
.g = passInfo.clearColorValue.y(),
.b = passInfo.clearColorValue.z(),
.a = passInfo.clearColorValue.w(),
2022-07-27 11:25:25 -04:00
},
},
};
const wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{
.view = passInfo.depthView,
2026-04-01 00:49:28 -06:00
.depthLoadOp = passInfo.clearDepth ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load,
.depthStoreOp = wgpu::StoreOp::Store,
2026-04-01 00:49:28 -06:00
.depthClearValue = passInfo.clearDepthValue,
2022-07-27 11:25:25 -04:00
};
2025-04-03 21:03:08 -06:00
const auto label = fmt::format("Render pass {}", i);
const wgpu::RenderPassDescriptor renderPassDescriptor{
2022-07-27 11:25:25 -04:00
.label = label.c_str(),
.colorAttachmentCount = attachments.size(),
.colorAttachments = attachments.data(),
.depthStencilAttachment = &depthStencilAttachment,
};
2026-04-01 00:49:28 -06:00
auto pass = cmd.BeginRenderPass(&renderPassDescriptor);
2022-07-27 11:25:25 -04:00
render_pass(pass, i);
pass.End();
2022-07-27 11:25:25 -04:00
2022-08-09 02:05:33 -04:00
if (passInfo.resolveTarget) {
const auto& dstSize = passInfo.resolveTarget->size;
const bool needsConversion = tex_copy_conv::needs_conversion(passInfo.resolveFormat);
const bool needsScaling = dstSize.width != static_cast<uint32_t>(passInfo.resolveRect.width) ||
dstSize.height != static_cast<uint32_t>(passInfo.resolveRect.height);
const tex_copy_conv::ConvRequest convReq{
.fmt = passInfo.resolveFormat,
.srcView = passInfo.copySourceView,
.uniformRange = passInfo.resolveUniformRange,
.dst = passInfo.resolveTarget,
.sampleFilter = needsScaling ? tex_copy_conv::SampleFilter::Linear : tex_copy_conv::SampleFilter::Nearest,
2022-07-27 11:25:25 -04:00
};
if (needsConversion) {
tex_copy_conv::run(cmd, convReq);
} else if (needsScaling) {
tex_copy_conv::blit(cmd, convReq);
2022-07-27 11:25:25 -04:00
} else {
const wgpu::TexelCopyTextureInfo src{
.texture = passInfo.copySourceTexture,
.origin =
wgpu::Origin3D{
.x = static_cast<uint32_t>(passInfo.resolveRect.x),
.y = static_cast<uint32_t>(passInfo.resolveRect.y),
},
};
const wgpu::TexelCopyTextureInfo dst{
.texture = passInfo.resolveTarget->texture,
};
const wgpu::Extent3D size{
.width = static_cast<uint32_t>(passInfo.resolveRect.width),
.height = static_cast<uint32_t>(passInfo.resolveRect.height),
.depthOrArrayLayers = 1,
};
cmd.CopyTextureToTexture(&src, &dst, &size);
2026-04-01 00:49:28 -06:00
}
2022-07-27 11:25:25 -04:00
}
}
g_renderPasses.clear();
2026-04-12 18:55:35 -06:00
expire_cached_bind_groups();
2026-03-11 01:38:35 +01:00
#if defined(AURORA_GFX_DEBUG_GROUPS)
if (!g_debugGroupStack.empty()) {
2026-03-29 22:33:28 -06:00
for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) {
2026-03-11 01:38:35 +01:00
Log.warn("Debug group was not popped at end of frame: {}", it);
}
g_debugGroupStack.clear();
}
if (g_debugMarkers.size() > 0) {
g_debugMarkers.clear();
}
#endif
2022-07-27 11:25:25 -04:00
}
void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) {
2022-07-27 11:25:25 -04:00
g_currentPipeline = UINTPTR_MAX;
#ifdef AURORA_GFX_DEBUG_GROUPS
std::vector<std::string> lastDebugGroupStack;
#endif
2026-04-12 18:55:35 -06:00
// Bind static bind group for the whole pass
pass.SetBindGroup(0, g_staticBindGroup);
pass.SetBindGroup(2, gx::g_emptyTextureBindGroup);
2022-07-27 11:25:25 -04:00
for (const auto& cmd : g_renderPasses[idx].commands) {
#ifdef AURORA_GFX_DEBUG_GROUPS
{
size_t firstDiff = lastDebugGroupStack.size();
for (size_t i = 0; i < lastDebugGroupStack.size(); ++i) {
if (i >= cmd.debugGroupStack.size() || cmd.debugGroupStack[i] != lastDebugGroupStack[i]) {
firstDiff = i;
break;
}
}
for (size_t i = firstDiff; i < lastDebugGroupStack.size(); ++i) {
pass.PopDebugGroup();
2022-07-27 11:25:25 -04:00
}
for (size_t i = firstDiff; i < cmd.debugGroupStack.size(); ++i) {
pass.PushDebugGroup(cmd.debugGroupStack[i].c_str());
2022-07-27 11:25:25 -04:00
}
lastDebugGroupStack = cmd.debugGroupStack;
}
#endif
switch (cmd.type) {
case CommandType::SetViewport: {
const auto& vp = cmd.data.setViewport;
const float minDepth = gx::UseReversedZ ? 1.f - vp.zfar : vp.znear;
const float maxDepth = gx::UseReversedZ ? 1.f - vp.znear : vp.zfar;
pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth);
2022-07-27 11:25:25 -04:00
} break;
case CommandType::SetScissor: {
const auto& sc = cmd.data.setScissor;
const auto& size = g_renderPasses[idx].targetSize;
const auto x = std::clamp(static_cast<uint32_t>(sc.x), 0u, size.width);
const auto y = std::clamp(static_cast<uint32_t>(sc.y), 0u, size.height);
const auto w = std::clamp(static_cast<uint32_t>(sc.width), 0u, size.width - x);
const auto h = std::clamp(static_cast<uint32_t>(sc.height), 0u, size.height - y);
2025-04-07 20:12:31 -06:00
pass.SetScissorRect(x, y, w, h);
2022-07-27 11:25:25 -04:00
} break;
case CommandType::Draw: {
const auto& draw = cmd.data.draw;
switch (draw.type) {
2026-04-01 00:49:28 -06:00
case ShaderType::Clear:
clear::render(draw.clear, pass, g_renderPasses[idx].targetSize);
2026-04-01 00:49:28 -06:00
break;
case ShaderType::GX:
2026-04-01 00:49:28 -06:00
gx::render(draw.gx, pass);
2022-07-27 11:25:25 -04:00
break;
}
} break;
2026-03-11 01:38:35 +01:00
case CommandType::DebugMarker: {
#if defined(AURORA_GFX_DEBUG_GROUPS)
pass.InsertDebugMarker(wgpu::StringView(g_debugMarkers[cmd.data.debugMarkerIndex]));
#endif
} break;
2022-07-27 11:25:25 -04:00
}
}
#ifdef AURORA_GFX_DEBUG_GROUPS
for (size_t i = 0; i < lastDebugGroupStack.size(); ++i) {
pass.PopDebugGroup();
2022-07-27 11:25:25 -04:00
}
#endif
}
bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass) {
2022-07-27 11:25:25 -04:00
if (ref == g_currentPipeline) {
return true;
}
2026-04-08 20:03:25 -06:00
wgpu::RenderPipeline pipeline;
if (!get_pipeline(ref, pipeline)) {
2022-07-27 11:25:25 -04:00
return false;
}
2026-04-08 20:03:25 -06:00
pass.SetPipeline(pipeline);
2022-07-27 11:25:25 -04:00
g_currentPipeline = ref;
return true;
}
static inline Range push(ByteBuffer& target, const uint8_t* data, size_t length, size_t alignment) {
size_t padding = 0;
if (alignment != 0) {
const size_t remainder = length % alignment;
if (remainder != 0) {
padding = alignment - remainder;
}
2022-07-27 11:25:25 -04:00
}
auto begin = target.size();
if (length == 0) {
length = alignment;
target.append_zeroes(alignment);
} else {
target.append(data, length);
if (padding > 0) {
target.append_zeroes(padding);
}
}
return {static_cast<uint32_t>(begin), static_cast<uint32_t>(length + padding)};
}
static inline Range map(ByteBuffer& target, size_t length, size_t alignment) {
size_t padding = 0;
if (alignment != 0) {
const size_t remainder = length % alignment;
if (remainder != 0) {
padding = alignment - remainder;
}
2022-07-27 11:25:25 -04:00
}
if (length == 0) {
length = alignment;
}
auto begin = target.size();
target.append_zeroes(length + padding);
return {static_cast<uint32_t>(begin), static_cast<uint32_t>(length + padding)};
}
Range push_verts(const uint8_t* data, size_t length) { return push(g_verts, data, length, 0); }
Range push_indices(const uint8_t* data, size_t length) { return push(g_indices, data, length, 0); }
2022-07-27 11:25:25 -04:00
Range push_uniform(const uint8_t* data, size_t length) {
2025-04-03 00:12:22 -06:00
return push(g_uniforms, data, length, g_cachedLimits.minUniformBufferOffsetAlignment);
2022-07-27 11:25:25 -04:00
}
Range push_storage(const uint8_t* data, size_t length) {
2025-04-03 00:12:22 -06:00
return push(g_storage, data, length, g_cachedLimits.minStorageBufferOffsetAlignment);
2022-07-27 11:25:25 -04:00
}
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)
2026-04-09 03:19:58 +02:00
const auto copyBytesPerRow = AURORA_ALIGN(bytesPerRow, 256);
2022-07-27 11:25:25 -04:00
const auto range = map(g_textureUpload, copyBytesPerRow * rowsPerImage, 0);
u8* dst = g_textureUpload.data() + range.offset;
for (u32 i = 0; i < rowsPerImage; ++i) {
memcpy(dst, data, bytesPerRow);
data += bytesPerRow;
dst += copyBytesPerRow;
}
return range;
}
std::pair<ByteBuffer, Range> map_verts(size_t length) {
const auto range = map(g_verts, length, 4);
return {ByteBuffer{g_verts.data() + range.offset, range.size}, range};
}
std::pair<ByteBuffer, Range> map_indices(size_t length) {
const auto range = map(g_indices, length, 4);
return {ByteBuffer{g_indices.data() + range.offset, range.size}, range};
}
std::pair<ByteBuffer, Range> map_uniform(size_t length) {
2025-04-03 00:12:22 -06:00
const auto range = map(g_uniforms, length, g_cachedLimits.minUniformBufferOffsetAlignment);
2022-07-27 11:25:25 -04:00
return {ByteBuffer{g_uniforms.data() + range.offset, range.size}, range};
}
std::pair<ByteBuffer, Range> map_storage(size_t length) {
2025-04-03 00:12:22 -06:00
const auto range = map(g_storage, length, g_cachedLimits.minStorageBufferOffsetAlignment);
2022-07-27 11:25:25 -04:00
return {ByteBuffer{g_storage.data() + range.offset, range.size}, range};
}
2026-04-09 03:51:06 +02:00
BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor) {
2022-07-27 11:25:25 -04:00
const auto id = xxh3_hash(descriptor);
2026-04-12 18:55:35 -06:00
const auto it = g_cachedBindGroups.find(id);
if (it == g_cachedBindGroups.end()) {
auto bg = wgpu::BindGroup::Acquire(wgpuDeviceCreateBindGroup(g_device.Get(), &descriptor));
g_cachedBindGroups.emplace(id, CachedBindGroup{
.bindGroup = std::move(bg),
.lastUsedFrame = g_frameIndex,
});
} else {
it->second.lastUsedFrame = g_frameIndex;
2022-07-27 11:25:25 -04:00
}
return id;
}
2025-04-04 01:59:30 -06:00
2026-04-09 03:51:06 +02:00
wgpu::BindGroup& find_bind_group(BindGroupRef id) {
2022-07-27 11:25:25 -04:00
const auto it = g_cachedBindGroups.find(id);
2022-08-09 02:05:33 -04:00
CHECK(it != g_cachedBindGroups.end(), "get_bind_group: failed to locate {:x}", id);
2026-04-12 18:55:35 -06:00
return it->second.bindGroup;
2022-07-27 11:25:25 -04:00
}
2026-04-09 03:51:06 +02:00
wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor) {
2022-07-27 11:25:25 -04:00
const auto id = xxh3_hash(descriptor);
auto it = g_cachedSamplers.find(id);
if (it == g_cachedSamplers.end()) {
it = g_cachedSamplers.try_emplace(id, g_device.CreateSampler(&descriptor)).first;
2022-07-27 11:25:25 -04:00
}
return it->second;
}
2026-04-09 03:19:58 +02:00
uint32_t align_uniform(uint32_t value) { return AURORA_ALIGN(value, g_cachedLimits.minUniformBufferOffsetAlignment); }
2026-03-11 01:38:35 +01:00
void insert_debug_marker(std::string label) {
#if defined(AURORA_GFX_DEBUG_GROUPS)
auto idx = g_debugMarkers.size();
g_debugMarkers.emplace_back(std::move(label));
2026-03-29 22:33:28 -06:00
push_command(CommandType::DebugMarker, {.debugMarkerIndex = idx});
2026-03-11 01:38:35 +01:00
#endif
}
2022-07-27 11:25:25 -04:00
} // namespace aurora::gfx
2026-03-11 01:38:35 +01:00
void aurora::gfx::push_debug_group(std::string label) {
#if defined(AURORA_GFX_DEBUG_GROUPS)
g_debugGroupStack.push_back(std::move(label));
#endif
}
2022-07-27 11:25:25 -04:00
void push_debug_group(const char* label) {
#ifdef AURORA_GFX_DEBUG_GROUPS
aurora::gfx::g_debugGroupStack.emplace_back(label);
#endif
}
void pop_debug_group() {
#ifdef AURORA_GFX_DEBUG_GROUPS
2026-03-11 01:38:35 +01:00
if (aurora::gfx::g_debugGroupStack.empty()) {
aurora::gfx::Log.error("Debug group stack underflowed!");
return;
}
2022-07-27 11:25:25 -04:00
aurora::gfx::g_debugGroupStack.pop_back();
#endif
}
const AuroraStats* aurora_get_stats() { return &aurora::gfx::g_stats; }