Files
2026-06-12 12:19:41 -06:00

994 lines
34 KiB
C++

#include "gx.hpp"
#include "pipeline.hpp"
#include "../dolphin/vi/vi_internal.hpp"
#include "../webgpu/gpu.hpp"
#include "../internal.hpp"
#include "../gfx/common.hpp"
#include "../gfx/tex_palette_conv.hpp"
#include "../gfx/texture.hpp"
#include "../gfx/texture_convert.hpp"
#include "../gfx/texture_replacement.hpp"
#include "gx_fmt.hpp"
#include <absl/container/flat_hash_map.h>
#include <absl/container/flat_hash_set.h>
#include <tracy/Tracy.hpp>
#include <atomic>
#include <bit>
#include <cfloat>
#include <cmath>
#include <mutex>
#include <optional>
#include <utility>
static aurora::Module Log("aurora::gx");
namespace aurora::gx {
using webgpu::g_device;
using webgpu::g_graphicsConfig;
GXState g_gxState{};
static wgpu::Sampler sEmptySampler;
static wgpu::Texture sEmptyTexture;
static wgpu::TextureView sEmptyTextureView;
static std::mutex sBindGroupLayoutMutex;
static absl::flat_hash_map<u32, wgpu::BindGroupLayout> sUniformBindGroupLayouts;
static absl::flat_hash_map<u32, std::pair<wgpu::BindGroupLayout, wgpu::BindGroupLayout>> sTextureBindGroupLayouts;
static wgpu::BindGroupLayout sTextureBindGroupLayout;
static wgpu::BindGroupLayout sSamplerBindGroupLayout;
static wgpu::PipelineLayout sPipelineLayout;
wgpu::BindGroup g_emptyTextureBindGroup;
namespace {
struct DynamicPaletteKey {
const void* sourceIdentity = nullptr;
u32 width = 0;
u32 height = 0;
u32 format = 0;
bool operator==(const DynamicPaletteKey& rhs) const = default;
template <typename H>
friend H AbslHashValue(H h, const DynamicPaletteKey& key) {
return H::combine(std::move(h), key.sourceIdentity, key.width, key.height, key.format);
}
};
struct DynamicPaletteEntry {
gfx::TextureHandle handle;
u32 sourceRevision = 0;
u32 tlutDataVersion = 0;
};
struct CachedTextureEntry {
gfx::TextureHandle handle;
u32 texDataVersion = 0;
u32 tlutObjId = 0;
u32 tlutDataVersion = 0;
};
struct CachedTlutTextureEntry {
gfx::TextureHandle handle;
u32 tlutDataVersion = 0;
};
struct TlutObjectCache {
CachedTlutTextureEntry tlutTexture;
absl::flat_hash_map<DynamicPaletteKey, DynamicPaletteEntry> dynamicPaletteTextures;
absl::flat_hash_set<u32> staticTextureUsers;
};
absl::flat_hash_map<u32, CachedTextureEntry> s_textureObjectCaches;
absl::flat_hash_map<u32, TlutObjectCache> s_tlutObjectCaches;
std::atomic_bool s_staticTextureCacheClearPending = false;
void do_clear_static_texture_cache() noexcept {
s_textureObjectCaches.clear();
for (auto& [_, cache] : s_tlutObjectCaches) {
cache.staticTextureUsers.clear();
}
}
DynamicPaletteKey make_dynamic_palette_key(const GXTexObj_& obj, const GXState::CopyTextureRef& source) {
return {
.sourceIdentity = source.handle.get(),
.width = obj.width(),
.height = obj.height(),
.format = obj.format(),
};
}
void clear_texture_dependency(u32 texObjId, u32 tlutObjId) {
if (texObjId == 0 || tlutObjId == 0) {
return;
}
if (auto it = s_tlutObjectCaches.find(tlutObjId); it != s_tlutObjectCaches.end()) {
it->second.staticTextureUsers.erase(texObjId);
if (!it->second.tlutTexture.handle && it->second.dynamicPaletteTextures.empty() &&
it->second.staticTextureUsers.empty()) {
s_tlutObjectCaches.erase(it);
}
}
}
void store_cached_texture(const GXTexObj_& obj, gfx::TextureHandle handle, u32 tlutObjId = 0, u32 tlutDataVersion = 0) {
if (obj.texObjId == 0) {
return;
}
auto& entry = s_textureObjectCaches[obj.texObjId];
if (entry.tlutObjId != tlutObjId) {
clear_texture_dependency(obj.texObjId, entry.tlutObjId);
}
entry.handle = std::move(handle);
entry.texDataVersion = obj.texDataVersion;
entry.tlutObjId = tlutObjId;
entry.tlutDataVersion = tlutDataVersion;
if (tlutObjId != 0) {
s_tlutObjectCaches[tlutObjId].staticTextureUsers.insert(obj.texObjId);
}
}
gfx::TextureHandle get_tlut_texture(const GXTlutObj_& tlut) {
if (tlut.tlutObjId != 0) {
auto& cache = s_tlutObjectCaches[tlut.tlutObjId];
if (cache.tlutTexture.handle && cache.tlutTexture.tlutDataVersion == tlut.tlutDataVersion) {
return cache.tlutTexture.handle;
}
cache.dynamicPaletteTextures.clear();
for (const u32 texObjId : cache.staticTextureUsers) {
s_textureObjectCaches.erase(texObjId);
}
cache.staticTextureUsers.clear();
}
const auto handle = gfx::new_static_texture_2d(
tlut.numEntries, 1, 1, gfx::tlut_texture_format(tlut.format),
{static_cast<const u8*>(tlut.data), static_cast<size_t>(tlut.numEntries) * sizeof(u16)}, true, "Loaded TLUT");
if (tlut.tlutObjId != 0) {
auto& cache = s_tlutObjectCaches[tlut.tlutObjId];
cache.tlutTexture.handle = handle;
cache.tlutTexture.tlutDataVersion = tlut.tlutDataVersion;
}
return handle;
}
gfx::TextureHandle resolve_static_texture(const GXTexObj_& obj) {
ZoneScoped;
if (s_staticTextureCacheClearPending.exchange(false, std::memory_order_acq_rel)) {
do_clear_static_texture_cache();
}
if (obj.texObjId != 0) {
if (const auto it = s_textureObjectCaches.find(obj.texObjId); it != s_textureObjectCaches.end()) {
const auto& entry = it->second;
if (entry.handle && entry.texDataVersion == obj.texDataVersion && entry.tlutObjId == 0) {
return entry.handle;
}
}
}
gfx::TextureHandle handle;
if (const auto replacement = gfx::texture_replacement::find_replacement(obj); replacement.has_value()) {
handle = *replacement;
} else {
#if DEBUG
const auto name = gfx::texture_replacement::build_texture_replacement_name(obj);
const auto nameStr = name.c_str();
#else
const auto nameStr = "GX Static Texture";
#endif
handle = gfx::new_static_texture_2d(obj.width(), obj.height(), obj.mip_count(), obj.format(),
{static_cast<const uint8_t*>(obj.data), UINT32_MAX}, false, nameStr);
}
if (!obj.no_cache()) {
store_cached_texture(obj, handle);
}
return handle;
}
gfx::TextureHandle resolve_static_palette_texture(const GXTexObj_& obj, const GXTlutObj_& tlut) {
ZoneScoped;
if (s_staticTextureCacheClearPending.exchange(false, std::memory_order_acq_rel)) {
do_clear_static_texture_cache();
}
if (obj.texObjId != 0) {
if (const auto it = s_textureObjectCaches.find(obj.texObjId); it != s_textureObjectCaches.end()) {
const auto& entry = it->second;
if (entry.handle && entry.texDataVersion == obj.texDataVersion && entry.tlutObjId == tlut.tlutObjId &&
entry.tlutDataVersion == tlut.tlutDataVersion) {
return entry.handle;
}
}
}
gfx::TextureHandle handle;
if (const auto replacement = gfx::texture_replacement::find_replacement(obj); replacement.has_value()) {
handle = *replacement;
} else {
auto converted = gfx::convert_texture_palette(
obj.format(), obj.width(), obj.height(), obj.mip_count(), {static_cast<const u8*>(obj.data), UINT32_MAX},
tlut.format, tlut.numEntries, {static_cast<const u8*>(tlut.data), static_cast<size_t>(tlut.numEntries) * 2});
if (converted.data.empty()) {
return {};
}
handle =
gfx::new_static_texture_2d(obj.width(), obj.height(), obj.mip_count(), GX_TF_RGBA8_PC,
{converted.data.data(), converted.data.size()}, false, "GX Static Palette Texture");
handle->hasArbitraryMips = converted.hasArbitraryMips;
}
if (!obj.no_cache() && !tlut.no_cache()) {
store_cached_texture(obj, handle, tlut.tlutObjId, tlut.tlutDataVersion);
}
return handle;
}
gfx::TextureHandle resolve_dynamic_palette_texture(const GXTexObj_& obj, const GXState::CopyTextureRef& source,
const GXTlutObj_& tlut) {
ZoneScoped;
const auto tlutHandle = get_tlut_texture(tlut);
auto& tlutCache = s_tlutObjectCaches[tlut.tlutObjId];
auto& entry = tlutCache.dynamicPaletteTextures[make_dynamic_palette_key(obj, source)];
if (!entry.handle) {
// Use source size instead of target (logical) size
entry.handle = gfx::new_conv_texture(source.handle->size.width, source.handle->size.height, GX_TF_RGBA8,
"GX Dynamic Palette Texture");
}
if (entry.sourceRevision != source.revision || entry.tlutDataVersion != tlut.tlutDataVersion) {
gfx::queue_palette_conv({
.variant = obj.format() == GX_TF_C4 ? gfx::tex_palette_conv::Variant::FromFloat4
: gfx::tex_palette_conv::Variant::FromFloat8,
.src = source.handle,
.dst = entry.handle,
.tlut = tlutHandle,
});
entry.sourceRevision = source.revision;
entry.tlutDataVersion = tlut.tlutDataVersion;
}
return entry.handle;
}
u32 resolved_format_for_handle(const gfx::TextureHandle& handle) {
if (!handle) {
return GX_TF_RGBA8;
}
if (handle->gxFormat != gfx::InvalidTextureFormat) {
return handle->gxFormat;
}
return GX_TF_RGBA8_PC;
}
template <typename T>
T round_away_from_zero(float value) noexcept {
return static_cast<T>(value < 0.0f ? std::floor(value) : std::ceil(value));
}
std::pair<f32, f32> polygon_offset_for_cull_mode(GXCullMode cullMode) noexcept {
if (cullMode == GX_CULL_FRONT) {
return {g_gxState.backOffset, g_gxState.backScale};
}
return {g_gxState.frontOffset, g_gxState.frontScale};
}
} // namespace
Vec2<uint32_t> logical_fb_size() noexcept {
return gfx::is_offscreen() ? gfx::get_render_target_size() : vi::configured_fb_size();
}
gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept {
if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) {
return logicalViewport;
}
const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size();
const auto [targetWidth, targetHeight] = gfx::get_render_target_size();
if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) {
return logicalViewport;
}
const float scaleX = static_cast<float>(targetWidth) / static_cast<float>(logicalFbWidth);
const float scaleY = static_cast<float>(targetHeight) / static_cast<float>(logicalFbHeight);
return {
.left = logicalViewport.left * scaleX,
.top = logicalViewport.top * scaleY,
.width = logicalViewport.width * scaleX,
.height = logicalViewport.height * scaleY,
.znear = logicalViewport.znear,
.zfar = logicalViewport.zfar,
};
}
gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept {
if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) {
return logicalScissor;
}
const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size();
const auto [targetWidth, targetHeight] = gfx::get_render_target_size();
if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) {
return logicalScissor;
}
const float scaleX = static_cast<float>(targetWidth) / static_cast<float>(logicalFbWidth);
const float scaleY = static_cast<float>(targetHeight) / static_cast<float>(logicalFbHeight);
const float left = static_cast<float>(logicalScissor.x) * scaleX;
const float top = static_cast<float>(logicalScissor.y) * scaleY;
const float right = static_cast<float>(logicalScissor.x + logicalScissor.width) * scaleX;
const float bottom = static_cast<float>(logicalScissor.y + logicalScissor.height) * scaleY;
const auto mappedLeft = std::clamp(static_cast<int32_t>(std::floor(left)), 0, static_cast<int32_t>(targetWidth));
const auto mappedTop = std::clamp(static_cast<int32_t>(std::floor(top)), 0, static_cast<int32_t>(targetHeight));
const auto mappedRight =
std::clamp(static_cast<int32_t>(std::ceil(right)), mappedLeft, static_cast<int32_t>(targetWidth));
const auto mappedBottom =
std::clamp(static_cast<int32_t>(std::ceil(bottom)), mappedTop, static_cast<int32_t>(targetHeight));
return {
.x = mappedLeft,
.y = mappedTop,
.width = mappedRight - mappedLeft,
.height = mappedBottom - mappedTop,
};
}
void set_logical_viewport(const gfx::Viewport& viewport) noexcept {
g_gxState.logicalViewport = viewport;
set_render_viewport(map_logical_viewport(viewport));
}
void set_render_viewport(const gfx::Viewport& viewport) noexcept {
g_gxState.renderViewport = viewport;
gfx::set_viewport(viewport);
}
void set_logical_scissor(const gfx::ClipRect& scissor) noexcept {
g_gxState.logicalScissor = scissor;
set_render_scissor(map_logical_scissor(g_gxState.logicalScissor));
}
void set_render_scissor(const gfx::ClipRect& scissor) noexcept {
g_gxState.renderScissor = scissor;
gfx::set_scissor(scissor);
}
const gfx::TextureBind& get_texture(GXTexMapID id) noexcept { return g_gxState.textures[static_cast<size_t>(id)]; }
void evict_texture_object(u32 texObjId) noexcept {
if (const auto it = s_textureObjectCaches.find(texObjId); it != s_textureObjectCaches.end()) {
clear_texture_dependency(texObjId, it->second.tlutObjId);
s_textureObjectCaches.erase(it);
}
// If there is a loaded slot with this ID, mark it as no_cache to avoid inserting it when it's resolved.
// This also handles the case where the texture was created, loaded, and immediately destroyed before we resolved it.
for (auto& obj : g_gxState.loadedTextures) {
if (obj.texObjId == texObjId) {
obj.set_no_cache(true);
}
}
}
void evict_tlut_object(u32 tlutObjId) noexcept {
if (const auto it = s_tlutObjectCaches.find(tlutObjId); it != s_tlutObjectCaches.end()) {
for (const u32 texObjId : it->second.staticTextureUsers) {
s_textureObjectCaches.erase(texObjId);
}
s_tlutObjectCaches.erase(it);
}
// If there is a loaded slot with this ID, mark it as no_cache to avoid inserting it when it's resolved.
// This also handles the case where the texture was created, loaded, and immediately destroyed before we resolved it.
for (auto& obj : g_gxState.loadedTluts) {
if (obj.tlutObjId == tlutObjId) {
obj.set_no_cache(true);
}
}
}
void clear_copy_texture_cache() noexcept {
g_gxState.copyTextures.clear();
g_gxState.copyTextureCache.clear();
for (auto& [_, cache] : s_tlutObjectCaches) {
cache.dynamicPaletteTextures.clear();
}
}
void clear_static_texture_cache() noexcept { s_staticTextureCacheClearPending.store(true, std::memory_order_release); }
void evict_copy_texture(const void* dest) noexcept {
absl::flat_hash_set<const void*> sourceIdentities;
if (const auto it = g_gxState.copyTextures.find(dest); it != g_gxState.copyTextures.end()) {
if (it->second.handle) {
sourceIdentities.insert(it->second.handle.get());
}
g_gxState.copyTextures.erase(it);
}
for (auto it = g_gxState.copyTextureCache.begin(); it != g_gxState.copyTextureCache.end();) {
if (it->first.dest == dest) {
if (it->second.handle) {
sourceIdentities.insert(it->second.handle.get());
}
g_gxState.copyTextureCache.erase(it++);
} else {
++it;
}
}
if (sourceIdentities.empty()) {
return;
}
for (auto& [_, cache] : s_tlutObjectCaches) {
for (auto it = cache.dynamicPaletteTextures.begin(); it != cache.dynamicPaletteTextures.end();) {
if (sourceIdentities.contains(it->first.sourceIdentity)) {
cache.dynamicPaletteTextures.erase(it++);
} else {
++it;
}
}
}
}
void resolve_sampled_textures(const ShaderInfo& info) noexcept {
ZoneScoped;
for (u32 i = 0; i < MaxTextures; ++i) {
if (!info.sampledTextures.test(i)) {
continue;
}
GXTexObj_ obj = g_gxState.loadedTextures[i];
auto& textureBind = g_gxState.textures[i];
if (obj.texObjId != 0 && obj.texObjId == textureBind.texObj.texObjId &&
obj.texDataVersion == textureBind.texObj.texDataVersion) {
// Texture bind unchanged
continue;
}
gfx::TextureHandle handle;
const auto copyIt = g_gxState.copyTextures.find(obj.data);
const GXState::CopyTextureRef* copyRef = copyIt != g_gxState.copyTextures.end() ? &copyIt->second : nullptr;
if (is_palette_format(obj.format())) {
const auto tlutIdx = static_cast<size_t>(obj.tlut);
if (tlutIdx < g_gxState.loadedTluts.size()) {
const auto& tlut = g_gxState.loadedTluts[tlutIdx];
if (tlut.data != nullptr) {
if (copyRef != nullptr) {
handle = resolve_dynamic_palette_texture(obj, *copyRef, tlut);
} else {
handle = resolve_static_palette_texture(obj, tlut);
}
}
}
} else if (copyRef != nullptr) {
handle = copyRef->handle;
} else if (obj.data != nullptr) {
handle = resolve_static_texture(obj);
}
obj.mFormat = resolved_format_for_handle(handle);
textureBind = gfx::TextureBind{obj, std::move(handle)};
}
}
static inline wgpu::BlendFactor to_blend_factor(GXBlendFactor fac, bool isDst) {
switch (fac) {
DEFAULT_FATAL("invalid blend factor {}", underlying(fac));
case GX_BL_ZERO:
return wgpu::BlendFactor::Zero;
case GX_BL_ONE:
return wgpu::BlendFactor::One;
case GX_BL_SRCCLR: // + GX_BL_DSTCLR
if (isDst) {
return wgpu::BlendFactor::Src;
} else {
return wgpu::BlendFactor::Dst;
}
case GX_BL_INVSRCCLR: // + GX_BL_INVDSTCLR
if (isDst) {
return wgpu::BlendFactor::OneMinusSrc;
} else {
return wgpu::BlendFactor::OneMinusDst;
}
case GX_BL_SRCALPHA:
return wgpu::BlendFactor::SrcAlpha;
case GX_BL_INVSRCALPHA:
return wgpu::BlendFactor::OneMinusSrcAlpha;
case GX_BL_DSTALPHA:
return wgpu::BlendFactor::DstAlpha;
case GX_BL_INVDSTALPHA:
return wgpu::BlendFactor::OneMinusDstAlpha;
}
}
static inline wgpu::CompareFunction to_compare_function(GXCompare func) {
switch (func) {
DEFAULT_FATAL("invalid depth fn {}", underlying(func));
case GX_NEVER:
return wgpu::CompareFunction::Never;
case GX_LESS:
return UseReversedZ ? wgpu::CompareFunction::Greater : wgpu::CompareFunction::Less;
case GX_EQUAL:
return wgpu::CompareFunction::Equal;
case GX_LEQUAL:
return UseReversedZ ? wgpu::CompareFunction::GreaterEqual : wgpu::CompareFunction::LessEqual;
case GX_GREATER:
return UseReversedZ ? wgpu::CompareFunction::Less : wgpu::CompareFunction::Greater;
case GX_NEQUAL:
return wgpu::CompareFunction::NotEqual;
case GX_GEQUAL:
return UseReversedZ ? wgpu::CompareFunction::LessEqual : wgpu::CompareFunction::GreaterEqual;
case GX_ALWAYS:
return wgpu::CompareFunction::Always;
}
}
static inline wgpu::BlendState to_blend_state(GXBlendMode mode, GXBlendFactor srcFac, GXBlendFactor dstFac,
GXLogicOp op, u32 dstAlpha) {
wgpu::BlendComponent colorBlendComponent;
switch (mode) {
DEFAULT_FATAL("unsupported blend mode {}", underlying(mode));
case GX_BM_NONE:
colorBlendComponent = {
.operation = wgpu::BlendOperation::Add,
.srcFactor = wgpu::BlendFactor::One,
.dstFactor = wgpu::BlendFactor::Zero,
};
break;
case GX_BM_BLEND:
colorBlendComponent = {
.operation = wgpu::BlendOperation::Add,
.srcFactor = to_blend_factor(srcFac, false),
.dstFactor = to_blend_factor(dstFac, true),
};
break;
case GX_BM_SUBTRACT:
colorBlendComponent = {
.operation = wgpu::BlendOperation::ReverseSubtract,
.srcFactor = wgpu::BlendFactor::One,
.dstFactor = wgpu::BlendFactor::One,
};
break;
case GX_BM_LOGIC:
switch (op) {
DEFAULT_FATAL("unsupported logic op {}", underlying(op));
case GX_LO_CLEAR:
colorBlendComponent = {
.operation = wgpu::BlendOperation::Add,
.srcFactor = wgpu::BlendFactor::Zero,
.dstFactor = wgpu::BlendFactor::Zero,
};
break;
case GX_LO_COPY:
colorBlendComponent = {
.operation = wgpu::BlendOperation::Add,
.srcFactor = wgpu::BlendFactor::One,
.dstFactor = wgpu::BlendFactor::Zero,
};
break;
case GX_LO_NOOP:
colorBlendComponent = {
.operation = wgpu::BlendOperation::Add,
.srcFactor = wgpu::BlendFactor::Zero,
.dstFactor = wgpu::BlendFactor::One,
};
break;
}
break;
}
wgpu::BlendComponent alphaBlendComponent;
if (dstAlpha != UINT32_MAX) {
alphaBlendComponent = wgpu::BlendComponent{
.operation = wgpu::BlendOperation::Add,
.srcFactor = wgpu::BlendFactor::Constant,
.dstFactor = wgpu::BlendFactor::Zero,
};
} else {
alphaBlendComponent = colorBlendComponent;
}
return {
.color = colorBlendComponent,
.alpha = alphaBlendComponent,
};
}
static inline wgpu::ColorWriteMask to_write_mask(bool colorUpdate, bool alphaUpdate) {
wgpu::ColorWriteMask writeMask = wgpu::ColorWriteMask::None;
if (colorUpdate) {
writeMask |= wgpu::ColorWriteMask::Red | wgpu::ColorWriteMask::Green | wgpu::ColorWriteMask::Blue;
}
if (alphaUpdate) {
writeMask |= wgpu::ColorWriteMask::Alpha;
}
return writeMask;
}
static inline wgpu::PrimitiveState to_primitive_state(GXCullMode gx_cullMode) {
auto cullMode = wgpu::CullMode::None;
switch (gx_cullMode) {
DEFAULT_FATAL("unsupported cull mode {}", underlying(gx_cullMode));
case GX_CULL_FRONT:
cullMode = wgpu::CullMode::Front;
break;
case GX_CULL_BACK:
cullMode = wgpu::CullMode::Back;
break;
case GX_CULL_NONE:
break;
}
return {
.topology = wgpu::PrimitiveTopology::TriangleList,
.stripIndexFormat = wgpu::IndexFormat::Undefined,
.frontFace = wgpu::FrontFace::CW,
.cullMode = cullMode,
};
}
wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef<wgpu::VertexBufferLayout> vtxBuffers,
wgpu::ShaderModule shader, const char* label) noexcept {
ZoneScoped;
const float depthBias = (UseReversedZ ? -1.0f : 1.0f) * std::bit_cast<float>(config.polygonOffsetBits);
const float depthBiasSlopeScale = (UseReversedZ ? -1.0f : 1.0f) * std::bit_cast<float>(config.polygonOffsetScaleBits);
const wgpu::DepthStencilState depthStencil{
.format = g_graphicsConfig.depthFormat,
.depthWriteEnabled = config.depthCompare && config.depthUpdate,
.depthCompare = config.depthCompare ? to_compare_function(config.depthFunc) : wgpu::CompareFunction::Always,
.depthBias = round_away_from_zero<int32_t>(depthBias),
.depthBiasSlopeScale = depthBiasSlopeScale,
.depthBiasClamp = std::bit_cast<float>(config.polygonOffsetClampBits),
};
const auto blendState =
to_blend_state(config.blendMode, config.blendFacSrc, config.blendFacDst, config.blendOp, config.dstAlpha);
const std::array colorTargets{wgpu::ColorTargetState{
.format = g_graphicsConfig.surfaceConfiguration.format,
.blend = &blendState,
.writeMask = to_write_mask(config.colorUpdate, config.alphaUpdate),
}};
const wgpu::FragmentState fragmentState{
.module = shader,
.entryPoint = "fs_main",
.targetCount = colorTargets.size(),
.targets = colorTargets.data(),
};
const wgpu::RenderPipelineDescriptor descriptor{
.label = label,
.layout = sPipelineLayout,
.vertex =
{
.module = shader,
.entryPoint = "vs_main",
.bufferCount = static_cast<uint32_t>(vtxBuffers.size()),
.buffers = vtxBuffers.data(),
},
.primitive = to_primitive_state(config.cullMode),
.depthStencil = &depthStencil,
.multisample =
wgpu::MultisampleState{
.count = config.msaaSamples,
},
.fragment = &fragmentState,
};
return g_device.CreateRenderPipeline(&descriptor);
}
void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXVtxFmt fmt) noexcept {
ZoneScoped;
const auto& vtxFmt = g_gxState.vtxFmts[fmt];
config.shaderConfig.fogType = g_gxState.fog.type;
u8 vtxOffset = 0;
for (int i = GX_VA_PNMTXIDX; i <= GX_VA_TEX7; ++i) {
const auto attr = static_cast<GXAttr>(i);
const auto type = g_gxState.vtxDesc[i];
auto& mapping = config.shaderConfig.attrs[i];
if (type == GX_NONE) {
mapping = {};
continue;
}
const auto& attrFmt = vtxFmt.attrs[i];
const auto cnt = comp_cnt_count(attr, attrFmt.cnt);
const bool nbt3 = attr == GX_VA_NRM && attrFmt.cnt == GX_NRM_NBT3;
mapping = AttrConfig{
.attrType = static_cast<u8>(type),
.cnt = cnt,
.compType = static_cast<u8>(attrFmt.type),
.offset = vtxOffset,
.stride = 0,
.frac = attrFmt.frac,
.le = false,
.nbt3 = nbt3,
};
switch (type) {
case GX_DIRECT: {
vtxOffset += comp_type_size(attr, attrFmt.type) * cnt;
break;
}
case GX_INDEX8:
mapping.stride = g_gxState.arrays[i].stride;
mapping.le = g_gxState.arrays[i].le;
vtxOffset += nbt3 ? 3 : 1;
break;
case GX_INDEX16:
mapping.stride = g_gxState.arrays[i].stride;
mapping.le = g_gxState.arrays[i].le;
vtxOffset += nbt3 ? 6 : 2;
break;
default:
Log.fatal("populate_pipeline_config: Invalid vertex type {}", type);
}
}
config.shaderConfig.vtxStride = vtxOffset;
if (primitive == GX_LINES) {
config.shaderConfig.lineMode = 1;
} else if (primitive == GX_LINESTRIP) {
config.shaderConfig.lineMode = 2;
} else if (primitive == GX_POINTS) {
config.shaderConfig.lineMode = 3;
} else {
config.shaderConfig.lineMode = 0;
}
config.shaderConfig.tevSwapTable = g_gxState.tevSwapTable;
for (u8 i = 0; i < g_gxState.numTevStages; ++i) {
config.shaderConfig.tevStages[i] = g_gxState.tevStages[i];
}
config.shaderConfig.tevStageCount = g_gxState.numTevStages;
for (u8 i = 0; i < g_gxState.numIndStages; ++i) {
config.shaderConfig.indStages[i] = g_gxState.indStages[i];
}
config.shaderConfig.numIndStages = g_gxState.numIndStages;
for (u8 i = 0; i < MaxColorChannels; ++i) {
const auto& cc = g_gxState.colorChannelConfig[i];
if (cc.lightingEnabled) {
config.shaderConfig.colorChannels[i] = cc;
} else {
// Only matSrc matters when lighting disabled
config.shaderConfig.colorChannels[i] = {
.matSrc = cc.matSrc,
};
}
}
for (u8 i = 0; i < g_gxState.numTexGens; ++i) {
config.shaderConfig.tcgs[i] = g_gxState.tcgs[i];
}
if (g_gxState.alphaCompare) {
config.shaderConfig.alphaCompare = g_gxState.alphaCompare;
}
const auto cullMode = config.shaderConfig.lineMode == 0 ? g_gxState.cullMode : GX_CULL_NONE;
const auto [polygonOffset, polygonOffsetScale] = polygon_offset_for_cull_mode(cullMode);
config = {
.msaaSamples = gfx::get_sample_count(),
.shaderConfig = config.shaderConfig,
.depthFunc = g_gxState.depthFunc,
.cullMode = cullMode,
.blendMode = g_gxState.blendMode,
.blendFacSrc = g_gxState.blendFacSrc,
.blendFacDst = g_gxState.blendFacDst,
.blendOp = g_gxState.blendOp,
.dstAlpha = g_gxState.dstAlpha,
.polygonOffsetBits = std::bit_cast<uint32_t>(polygonOffset),
.polygonOffsetScaleBits = std::bit_cast<uint32_t>(polygonOffsetScale),
.polygonOffsetClampBits = std::bit_cast<uint32_t>(g_gxState.clamp),
.depthCompare = g_gxState.depthCompare,
.depthUpdate = g_gxState.depthUpdate,
.alphaUpdate = g_gxState.alphaUpdate,
.colorUpdate = g_gxState.colorUpdate,
};
}
GXBindGroups build_bind_groups(const ShaderInfo& info) noexcept {
ZoneScoped;
if (!info.sampledTextures.any() && !info.sampledIndTextures.any()) {
// Don't bother re-binding anything
return {};
}
// Using C WGPU types instead of C++ wrappers to avoid destructor overhead
std::array<WGPUBindGroupEntry, MaxTextures * 2> textureEntries{};
for (u32 i = 0; i < MaxTextures; ++i) {
const auto& tex = g_gxState.textures[i];
WGPUBindGroupEntry& textureEntry = textureEntries[i * 2];
WGPUBindGroupEntry& samplerEntry = textureEntries[i * 2 + 1];
textureEntry.binding = i * 2;
samplerEntry.binding = i * 2 + 1;
if (tex && (info.sampledTextures[i] || info.sampledIndTextures[i])) {
textureEntry.textureView = tex.ref->sampleTextureView.Get();
samplerEntry.sampler = gfx::sampler_ref(tex.get_descriptor()).Get();
} else {
textureEntry.textureView = sEmptyTextureView.Get();
samplerEntry.sampler = sEmptySampler.Get();
}
}
const WGPUBindGroupDescriptor textureBindGroupDescriptor{
.label = {"GX Texture Bind Group", WGPU_STRLEN},
.layout = sTextureBindGroupLayout.Get(),
.entryCount = textureEntries.size(),
.entries = textureEntries.data(),
};
return {
.textureBindGroup = gfx::bind_group_ref(textureBindGroupDescriptor),
};
}
void initialize() noexcept {
{
std::array<wgpu::BindGroupLayoutEntry, MaxTextures * 2> textureEntries;
for (u32 i = 0; i < MaxTextures; ++i) {
textureEntries[i * 2] = {
.binding = i * 2,
.visibility = wgpu::ShaderStage::Fragment,
.texture =
{
.sampleType = wgpu::TextureSampleType::Float,
.viewDimension = wgpu::TextureViewDimension::e2D,
},
};
textureEntries[i * 2 + 1] = {
.binding = i * 2 + 1,
.visibility = wgpu::ShaderStage::Fragment,
.sampler = {.type = wgpu::SamplerBindingType::Filtering},
};
}
const wgpu::BindGroupLayoutDescriptor descriptor{
.label = "GX Texture Bind Group Layout",
.entryCount = textureEntries.size(),
.entries = textureEntries.data(),
};
sTextureBindGroupLayout = g_device.CreateBindGroupLayout(&descriptor);
}
{
constexpr wgpu::SamplerDescriptor descriptor{.label = "Empty sampler"};
sEmptySampler = gfx::sampler_ref(descriptor);
}
{
constexpr wgpu::TextureDescriptor descriptor{
.label = "Empty texture",
.usage = wgpu::TextureUsage::TextureBinding,
.size = {1, 1},
.format = wgpu::TextureFormat::RGBA8Unorm,
};
sEmptyTexture = g_device.CreateTexture(&descriptor);
sEmptyTextureView = sEmptyTexture.CreateView();
}
{
std::array<wgpu::BindGroupEntry, MaxTextures * 2> entries;
for (u32 i = 0; i < MaxTextures; ++i) {
entries[i * 2] = {
.binding = i * 2,
.textureView = sEmptyTextureView,
};
entries[i * 2 + 1] = {
.binding = i * 2 + 1,
.sampler = sEmptySampler,
};
}
const wgpu::BindGroupDescriptor desc{
.label = "GX Empty Texture Bind Group",
.layout = sTextureBindGroupLayout,
.entryCount = entries.size(),
.entries = entries.data(),
};
g_emptyTextureBindGroup = g_device.CreateBindGroup(&desc);
}
{
const std::array layouts{
gfx::g_staticBindGroupLayout,
gfx::g_uniformBindGroupLayout,
sTextureBindGroupLayout,
};
const wgpu::PipelineLayoutDescriptor desc{
.label = "GX Pipeline Layout",
.bindGroupLayoutCount = layouts.size(),
.bindGroupLayouts = layouts.data(),
};
sPipelineLayout = g_device.CreatePipelineLayout(&desc);
}
}
void shutdown() noexcept {
// TODO we should probably store this all in g_state.gx instead
sSamplerBindGroupLayout = {};
sTextureBindGroupLayout = {};
{
std::lock_guard lock{sBindGroupLayoutMutex};
sUniformBindGroupLayouts.clear();
sTextureBindGroupLayouts.clear();
}
for (auto& item : g_gxState.textures) {
item.ref.reset();
}
s_textureObjectCaches.clear();
s_tlutObjectCaches.clear();
g_gxState.loadedTextures.fill({});
g_gxState.loadedTluts.fill({});
clear_copy_texture_cache();
}
} // namespace aurora::gx
static wgpu::AddressMode wgpu_address_mode(GXTexWrapMode mode) {
switch (mode) {
DEFAULT_FATAL("invalid wrap mode {}", underlying(mode));
case GX_CLAMP:
return wgpu::AddressMode::ClampToEdge;
case GX_REPEAT:
return wgpu::AddressMode::Repeat;
case GX_MIRROR:
return wgpu::AddressMode::MirrorRepeat;
}
}
static std::pair<wgpu::FilterMode, wgpu::MipmapFilterMode> wgpu_filter_mode(GXTexFilter filter) {
switch (filter) {
DEFAULT_FATAL("invalid filter mode {}", static_cast<int>(filter));
case GX_NEAR:
return {wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Undefined};
case GX_LINEAR:
return {wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Undefined};
case GX_NEAR_MIP_NEAR:
return {wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Nearest};
case GX_LIN_MIP_NEAR:
return {wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Nearest};
case GX_NEAR_MIP_LIN:
return {wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Linear};
case GX_LIN_MIP_LIN:
return {wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Linear};
}
}
static u16 wgpu_aniso(GXAnisotropy aniso) {
switch (aniso) {
DEFAULT_FATAL("invalid aniso {}", static_cast<int>(aniso));
case GX_ANISO_1:
case GX_MAX_ANISOTROPY:
return 1;
case GX_ANISO_2:
return std::max<u16>(aurora::webgpu::g_graphicsConfig.textureAnisotropy / 2, 1);
case GX_ANISO_4:
return std::max<u16>(aurora::webgpu::g_graphicsConfig.textureAnisotropy, 1);
}
}
wgpu::SamplerDescriptor aurora::gfx::TextureBind::get_descriptor() const noexcept {
auto [minFilter, mipFilter] = wgpu_filter_mode(texObj.min_filter());
auto [magFilter, _] = wgpu_filter_mode(texObj.mag_filter());
const bool mipsEnabled = mipFilter != wgpu::MipmapFilterMode::Undefined;
float minLod = texObj.min_lod();
float maxLod = texObj.max_lod();
u16 maxAnisotropy = wgpu_aniso(texObj.max_aniso());
if (ref && ref->isReplacement) {
minLod = 0.f;
maxLod = 1000.f;
if (!mipsEnabled) {
mipFilter = wgpu::MipmapFilterMode::Nearest;
}
} else if (mipFilter == wgpu::MipmapFilterMode::Undefined) {
minLod = 0.f;
maxLod = 0.f;
}
if ((ref && ref->hasArbitraryMips) || !mipsEnabled) {
maxAnisotropy = 1;
} else if (maxAnisotropy > 1) {
magFilter = wgpu::FilterMode::Linear;
minFilter = wgpu::FilterMode::Linear;
mipFilter = wgpu::MipmapFilterMode::Linear;
}
return {
.label = "Generated Filtering Sampler",
.addressModeU = wgpu_address_mode(texObj.wrap_s()),
.addressModeV = wgpu_address_mode(texObj.wrap_t()),
.addressModeW = wgpu::AddressMode::Repeat,
.magFilter = magFilter,
.minFilter = minFilter,
.mipmapFilter = mipFilter,
.lodMinClamp = minLod,
.lodMaxClamp = maxLod,
.maxAnisotropy = maxAnisotropy,
};
} // namespace aurora::gx