Public aurora::gfx API (custom draws & compute)

This commit is contained in:
Luke Street
2026-07-07 13:13:53 -06:00
parent 5af0cc06a0
commit b979ff6913
8 changed files with 911 additions and 42 deletions
+5 -2
View File
@@ -101,8 +101,11 @@ void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks);
/**
* \brief Specify a set of overlay files to be used by the DVD layer.
*
* Calling this function immediately applies the new files and rebuilds the FST. This is not thread safe.
* It is best you only call this once on startup, before the game's code has started.
* Calling this function immediately applies the new files and rebuilds the FST, replacing any
* previously specified set. It may be called again at runtime; FST reads and overlay opens are
* serialized against the rebuild, and previously assigned EntryNums are stable per path. Files
* removed by a re-registration fail to open from then on (or revert to the underlying DVD file);
* already-open handles are unaffected. Only call from one thread at a time.
*
* This function must be called *after* aurora_dvd_overlay_callbacks.
*
+148
View File
@@ -0,0 +1,148 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <webgpu/webgpu_cpp.h>
namespace aurora::gfx {
inline constexpr size_t InlineDrawPayloadSize = 128;
/// Generational handle: 0 is never valid, and IDs are not reused after unregister_draw_type.
using DrawTypeId = uint64_t;
inline constexpr DrawTypeId InvalidDrawType = 0;
struct Range {
uint32_t offset = 0;
uint32_t size = 0;
bool operator==(const Range& rhs) const { return offset == rhs.offset && size == rhs.size; }
bool operator!=(const Range& rhs) const { return !(*this == rhs); }
};
struct DrawContext {
wgpu::Device device;
wgpu::Queue queue;
wgpu::Buffer vertexBuffer;
wgpu::Buffer indexBuffer;
wgpu::Buffer uniformBuffer;
wgpu::Buffer storageBuffer;
wgpu::TextureFormat colorFormat;
wgpu::TextureFormat depthFormat;
uint32_t sampleCount = 1;
uint32_t targetWidth = 0;
uint32_t targetHeight = 0;
};
/// Invoked on the render worker thread while replaying the pass the draw was
/// recorded into. The encoder's pipeline/bind-group/viewport/scissor state is
/// restored after the callback returns. Handles in the context are borrowed and
/// valid only for the duration of the call. sampleCount/target dimensions are
/// those of the containing pass (offscreen passes are always single-sample).
using DrawCallback = void (*)(const DrawContext& ctx, const wgpu::RenderPassEncoder& pass,
const void* payload, size_t payloadSize, void* userdata);
struct DrawTypeDescriptor {
const char* label = nullptr;
DrawCallback draw = nullptr;
void* userdata = nullptr;
};
wgpu::Device device() noexcept;
wgpu::Queue queue() noexcept;
wgpu::TextureFormat color_format() noexcept;
wgpu::TextureFormat depth_format() noexcept;
uint32_t sample_count() noexcept;
bool uses_reversed_z() noexcept;
DrawTypeId register_draw_type(const DrawTypeDescriptor& desc);
void unregister_draw_type(DrawTypeId type) noexcept;
/// Records an inline custom draw into the currently open render pass at the
/// current position in the command stream. Payload (<= InlineDrawPayloadSize)
/// is copied. Returns false (with a warning) outside an active render pass.
bool push_custom_draw(DrawTypeId type, const void* payload, size_t payloadSize);
/// Generational handle: 0 is never valid, and IDs are not reused after unregister_encoder_task_type.
using EncoderTaskId = uint64_t;
inline constexpr EncoderTaskId InvalidEncoderTask = 0;
struct EncoderTaskContext {
wgpu::Device device;
wgpu::Queue queue;
wgpu::Buffer vertexBuffer;
wgpu::Buffer indexBuffer;
wgpu::Buffer uniformBuffer;
wgpu::Buffer storageBuffer;
};
/// Invoked on the render worker thread with the frame's command encoder,
/// positioned between two render passes. The callback may begin/end compute
/// passes and record copies on the encoder; it must leave no pass open when it
/// returns and must not Finish the encoder. Handles in the context are borrowed
/// and valid only for the duration of the call. Data appended to the streaming
/// buffers before the task was pushed is GPU-visible inside it.
using EncoderTaskCallback = void (*)(const EncoderTaskContext& ctx, const wgpu::CommandEncoder& cmd,
const void* payload, size_t payloadSize, void* userdata);
struct EncoderTaskDescriptor {
const char* label = nullptr;
EncoderTaskCallback callback = nullptr;
void* userdata = nullptr;
};
EncoderTaskId register_encoder_task_type(const EncoderTaskDescriptor& desc);
void unregister_encoder_task_type(EncoderTaskId type) noexcept;
/// Seals the current EFB pass, records an encoder task to execute on the frame
/// encoder at this point, and resumes rendering on a pass that loads the
/// existing contents. Payload semantics match push_custom_draw. Returns false
/// (with a warning) outside an active render pass or while an offscreen pass
/// is open.
bool push_encoder_task(EncoderTaskId type, const void* payload, size_t payloadSize);
/// Append transient data to the shared per-frame streaming buffers. Returned
/// ranges are valid for the current frame only. Returns an empty Range (with a
/// warning) outside an active recording frame.
Range push_verts(const uint8_t* data, size_t length, size_t alignment);
Range push_indices(const uint8_t* data, size_t length, size_t alignment);
Range push_uniform(const uint8_t* data, size_t length);
Range push_storage(const uint8_t* data, size_t length);
struct ResolveDesc {
bool color = true;
bool depth = false;
};
struct ResolvedTargets {
wgpu::TextureView color; // single-sample snapshot; null if not requested
wgpu::TextureView depth; // single-sample R32Float depth snapshot; null if not requested
wgpu::TextureFormat colorFormat = wgpu::TextureFormat::Undefined;
uint32_t width = 0;
uint32_t height = 0;
};
/// Snapshots the current pass targets into pooled textures (valid for the
/// current frame), then: on the EFB, continues rendering on a fresh EFB pass
/// (GXCopyTex semantics); in an offscreen pass created by create_pass, ends it
/// and restores the suspended EFB pass (GXRestoreFrameBuffer semantics).
/// Requesting neither color nor depth is a plain pass break (or offscreen
/// close, discarding its output). Depth is left null when unsupported by the
/// device. Returns false (with a warning) outside an active render pass.
bool resolve_pass(const ResolveDesc& desc, ResolvedTargets& out);
/// Opens an offscreen render pass (GXCreateFrameBuffer semantics): cleared
/// single-sample color+depth at (width, height) with full-target
/// viewport/scissor. Subsequent draws target it until resolve_pass restores the
/// EFB. Nesting is unsupported: returns false (with a warning) outside an
/// active render pass or while any offscreen pass is already open.
bool create_pass(uint32_t width, uint32_t height);
/// True while an offscreen pass (create_pass or GXCreateFrameBuffer) is open.
bool is_offscreen() noexcept;
/// Blocks until the render worker has drained its queue. After this returns,
/// no draw callback is executing or queued to execute; used before unloading
/// code that registered draw types. Callable from the game thread only.
void synchronize();
} // namespace aurora::gfx
+2 -2
View File
@@ -78,8 +78,8 @@ void copy_tex(const void* dest, GXBool clear) noexcept {
const auto clearColor = clear && g_gxState.colorUpdate;
const auto clearAlpha = clear && g_gxState.alphaUpdate;
const auto clearDepth = clear && g_gxState.depthUpdate;
gfx::resolve_pass(handle.handle, rect, clearColor, clearAlpha, clearDepth, g_gxState.clearColor, clear_depth_value(),
texCopyFmt);
gfx::resolve_pass_into(handle.handle, rect, clearColor, clearAlpha, clearDepth, g_gxState.clearColor,
clear_depth_value(), texCopyFmt);
++handle.revision;
g_gxState.copyTextures[dest] = handle;
}
+587 -25
View File
File diff suppressed because it is too large Load Diff
+4 -11
View File
@@ -11,6 +11,7 @@
#include <utility>
#include <aurora/gfx.h>
#include <aurora/gfx.hpp>
#include <aurora/math.hpp>
#include <dolphin/gx/GXEnum.h>
#include <webgpu/webgpu_cpp.h>
@@ -173,7 +174,7 @@ private:
namespace aurora::gfx {
inline constexpr bool UseTextureBuffer = true;
inline constexpr uint64_t UniformBufferSize = 25165824; // 24mb
inline constexpr uint64_t VertexBufferSize = 3145728; // 3mb
inline constexpr uint64_t VertexBufferSize = 5242880; // 5mb
inline constexpr uint64_t IndexBufferSize = 1048576; // 1mb
inline constexpr uint64_t StorageBufferSize = 8388608; // 8mb
inline constexpr uint64_t TextureUploadSize = 25165824; // 24mb
@@ -194,13 +195,6 @@ using BindGroupRef = HashType;
using PipelineRef = HashType;
using SamplerRef = HashType;
using ShaderRef = HashType;
struct Range {
uint32_t offset = 0;
uint32_t size = 0;
bool operator==(const Range& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; }
bool operator!=(const Range& rhs) const { return !(*this == rhs); }
};
struct ClipRect {
int32_t x;
@@ -236,8 +230,8 @@ void after_submit() noexcept;
void gpu_synchronize();
void after_present() noexcept;
float calculate_fps() noexcept;
void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth,
Vec4<float> clearColorValue, float clearDepthValue, GXTexFmt resolveFormat = GX_TF_RGBA8);
void resolve_pass_into(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth,
Vec4<float> clearColorValue, float clearDepthValue, GXTexFmt resolveFormat = GX_TF_RGBA8);
struct ColorPassDescriptor {
const char* label = nullptr;
@@ -257,7 +251,6 @@ struct ColorPassDescriptor {
wgpu::LoadOp stencilLoadOp = wgpu::LoadOp::Undefined;
wgpu::StoreOp stencilStoreOp = wgpu::StoreOp::Undefined;
uint32_t stencilClearValue = 0;
bool observable = true;
};
void begin_color_pass(const ColorPassDescriptor& desc);
+161
View File
@@ -244,6 +244,45 @@ static constexpr std::string_view FragZ16 = R"(
}
)"sv;
// Depth -> R32Float (no scaling)
static constexpr std::string_view DepthSnapshotShader = R"(
@group(0) @binding(0) var src: texture_depth_2d;
var<private> positions: array<vec2f, 3> = array(
vec2f(-1.0, 1.0),
vec2f(-1.0, -3.0),
vec2f(3.0, 1.0),
);
@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
return vec4f(positions[vi], 0.0, 1.0);
}
@fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let depth = textureLoad(src, vec2i(pos.xy), 0);
return vec4f(depth, 0.0, 0.0, 1.0);
}
)"sv;
static constexpr std::string_view DepthSnapshotShaderMS = R"(
@group(0) @binding(0) var src: texture_depth_multisampled_2d;
var<private> positions: array<vec2f, 3> = array(
vec2f(-1.0, 1.0),
vec2f(-1.0, -3.0),
vec2f(3.0, 1.0),
);
@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
return vec4f(positions[vi], 0.0, 1.0);
}
@fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let depth = textureLoad(src, vec2i(pos.xy), 0);
return vec4f(depth, 0.0, 0.0, 1.0);
}
)"sv;
struct ConvPipeline {
GXTexFmt fmt;
std::string_view fragShader;
@@ -278,6 +317,75 @@ static wgpu::Sampler g_nearestSampler;
static wgpu::Sampler g_linearSampler;
static absl::flat_hash_map<GXTexFmt, wgpu::RenderPipeline> g_pipelines;
static wgpu::RenderPipeline g_blitPipeline;
static wgpu::BindGroupLayout g_depthSnapshotBindGroupLayout;
static wgpu::BindGroupLayout g_depthSnapshotBindGroupLayoutMS;
static wgpu::RenderPipeline g_depthSnapshotPipeline;
static wgpu::RenderPipeline g_depthSnapshotPipelineMS;
static wgpu::BindGroupLayout create_depth_snapshot_layout(bool multisampled, const char* label) {
const std::array entries{
wgpu::BindGroupLayoutEntry{
.binding = 0,
.visibility = wgpu::ShaderStage::Fragment,
.texture =
wgpu::TextureBindingLayout{
.sampleType = wgpu::TextureSampleType::Depth,
.viewDimension = wgpu::TextureViewDimension::e2D,
.multisampled = multisampled,
},
},
};
const wgpu::BindGroupLayoutDescriptor descriptor{
.label = label,
.entryCount = entries.size(),
.entries = entries.data(),
};
return g_device.CreateBindGroupLayout(&descriptor);
}
static wgpu::RenderPipeline create_depth_snapshot_pipeline(const std::string_view shaderSource,
const wgpu::BindGroupLayout& bindGroupLayout,
const char* label) {
const std::string source{shaderSource};
const wgpu::ShaderSourceWGSL wgslSource{wgpu::ShaderSourceWGSL::Init{
.code = source.c_str(),
}};
const wgpu::ShaderModuleDescriptor moduleDescriptor{
.nextInChain = &wgslSource,
.label = label,
};
const auto module = g_device.CreateShaderModule(&moduleDescriptor);
const std::array colorTargets{wgpu::ColorTargetState{
.format = wgpu::TextureFormat::R32Float,
}};
const wgpu::FragmentState fragmentState{
.module = module,
.entryPoint = "fs_main",
.targetCount = colorTargets.size(),
.targets = colorTargets.data(),
};
const wgpu::PipelineLayoutDescriptor layoutDescriptor{
.bindGroupLayoutCount = 1,
.bindGroupLayouts = &bindGroupLayout,
};
const auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor);
const wgpu::RenderPipelineDescriptor pipelineDescriptor{
.label = label,
.layout = pipelineLayout,
.vertex =
wgpu::VertexState{
.module = module,
.entryPoint = "vs_main",
},
.primitive =
wgpu::PrimitiveState{
.topology = wgpu::PrimitiveTopology::TriangleList,
},
.fragment = &fragmentState,
};
return g_device.CreateRenderPipeline(&pipelineDescriptor);
}
static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble,
const wgpu::BindGroupLayout& bindGroupLayout) {
@@ -408,6 +516,12 @@ void initialize() {
Log.fatal("Output format mismatch for {}", conv.fmt);
}
}
g_depthSnapshotBindGroupLayout = create_depth_snapshot_layout(false, "Depth Snapshot Bind Group Layout");
g_depthSnapshotBindGroupLayoutMS = create_depth_snapshot_layout(true, "Depth Snapshot MS Bind Group Layout");
g_depthSnapshotPipeline =
create_depth_snapshot_pipeline(DepthSnapshotShader, g_depthSnapshotBindGroupLayout, "Depth Snapshot");
g_depthSnapshotPipelineMS =
create_depth_snapshot_pipeline(DepthSnapshotShaderMS, g_depthSnapshotBindGroupLayoutMS, "Depth Snapshot MS");
}
static constexpr wgpu::SamplerDescriptor nearestSamplerDescriptor{
@@ -432,6 +546,10 @@ void shutdown() {
g_depthBindGroupLayout = {};
g_nearestSampler = {};
g_linearSampler = {};
g_depthSnapshotBindGroupLayout = {};
g_depthSnapshotBindGroupLayoutMS = {};
g_depthSnapshotPipeline = {};
g_depthSnapshotPipelineMS = {};
}
static void execute(const wgpu::CommandEncoder& cmd, const ConvRequest& req, const wgpu::RenderPipeline& pipeline) {
@@ -519,4 +637,47 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); }
bool snapshot_depth_supported() noexcept { return static_cast<bool>(g_depthSnapshotPipeline); }
void snapshot_depth(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& srcDepth, uint32_t msaaSamples,
const wgpu::TextureView& dst) {
const bool multisampled = msaaSamples > 1;
const auto& pipeline = multisampled ? g_depthSnapshotPipelineMS : g_depthSnapshotPipeline;
if (!pipeline) {
return;
}
const std::array bindGroupEntries{
wgpu::BindGroupEntry{
.binding = 0,
.textureView = srcDepth,
},
};
const wgpu::BindGroupDescriptor bindGroupDescriptor{
.layout = multisampled ? g_depthSnapshotBindGroupLayoutMS : g_depthSnapshotBindGroupLayout,
.entryCount = bindGroupEntries.size(),
.entries = bindGroupEntries.data(),
};
const auto bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor);
const std::array colorAttachments{
wgpu::RenderPassColorAttachment{
.view = dst,
.loadOp = wgpu::LoadOp::Clear,
.storeOp = wgpu::StoreOp::Store,
.clearValue = {0.0, 0.0, 0.0, 0.0},
},
};
const wgpu::RenderPassDescriptor renderPassDescriptor{
.label = "Depth Snapshot Pass",
.colorAttachmentCount = colorAttachments.size(),
.colorAttachments = colorAttachments.data(),
.timestampWrites = webgpu::gpu_prof::pass_writes("Depth snapshot"),
};
const auto pass = cmd.BeginRenderPass(&renderPassDescriptor);
pass.SetPipeline(pipeline);
pass.SetBindGroup(0, bindGroup);
pass.Draw(3);
pass.End();
}
} // namespace aurora::gfx::tex_copy_conv
+4
View File
@@ -26,4 +26,8 @@ void shutdown();
void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req);
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req);
bool snapshot_depth_supported() noexcept;
void snapshot_depth(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& srcDepth, uint32_t msaaSamples,
const wgpu::TextureView& dst);
} // namespace aurora::gfx::tex_copy_conv
-2
View File
@@ -697,7 +697,6 @@ void WebGPURenderInterface::BeginRenderTargetPass(const wgpu::TextureView& view,
.stencilLoadOp = clearStencil ? wgpu::LoadOp::Clear : wgpu::LoadOp::Undefined,
.stencilStoreOp = clearStencil ? wgpu::StoreOp::Store : wgpu::StoreOp::Undefined,
.stencilClearValue = 0,
.observable = true,
});
m_passActive = true;
ApplyViewport();
@@ -725,7 +724,6 @@ void WebGPURenderInterface::BeginLayerPass(Rml::LayerHandle layer, wgpu::LoadOp
.stencilLoadOp = clearStencil ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load,
.stencilStoreOp = wgpu::StoreOp::Store,
.stencilClearValue = 0,
.observable = true,
});
m_passActive = true;
ApplyViewport();