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