Files
mitch030504--Wiicompiled_VR…/aurora-main/lib/gx/command_processor.cpp
T

2626 lines
96 KiB
C++

#include "command_processor.hpp"
#include "../gfx/common.hpp"
#include "../dolphin/gx/__gx.h"
#include "../gfx/texture_replacement.hpp"
#include "dolphin/gx/GXAurora.h"
#include "gx.hpp"
#include "gx_fmt.hpp"
#include "pipeline.hpp"
#include "shader_info.hpp"
#include "../internal.hpp"
#include <absl/container/flat_hash_map.h>
#include <tracy/Tracy.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <optional>
#include <vector>
namespace aurora::gx::fifo {
static Module Log("aurora::gx::fifo");
using IndexBuffer = std::vector<u16>;
static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount) {
size_t writePos = 0;
if (prim == GX_QUADS) {
// Retain the existing incomplete-quad behavior: every started group emits a complete six-index quad.
buf.resize(((static_cast<u32>(vtxCount) + 3u) / 4u) * 6u);
for (u16 v = 0; v < vtxCount; v += 4) {
const u16 idx0 = v;
const u16 idx1 = static_cast<u16>(v + 1);
const u16 idx2 = static_cast<u16>(v + 2);
const u16 idx3 = static_cast<u16>(v + 3);
buf[writePos++] = idx0;
buf[writePos++] = idx1;
buf[writePos++] = idx2;
buf[writePos++] = idx2;
buf[writePos++] = idx3;
buf[writePos++] = idx0;
}
} else if (prim == GX_TRIANGLES) {
buf.resize(vtxCount);
for (u16 v = 0; v < vtxCount; ++v) {
buf[writePos++] = v;
}
} else if (prim == GX_TRIANGLEFAN) {
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
buf.resize(indexCount);
for (u16 v = 0; v < vtxCount; ++v) {
if (v < 3) {
buf[writePos++] = v;
continue;
}
buf[writePos++] = 0;
buf[writePos++] = static_cast<u16>(v - 1);
buf[writePos++] = v;
}
} else if (prim == GX_TRIANGLESTRIP) {
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
buf.resize(indexCount);
for (u16 v = 0; v < vtxCount; ++v) {
if (v < 3) {
buf[writePos++] = v;
continue;
}
if ((v & 1) == 0) {
buf[writePos++] = static_cast<u16>(v - 2);
buf[writePos++] = static_cast<u16>(v - 1);
} else {
buf[writePos++] = static_cast<u16>(v - 1);
buf[writePos++] = static_cast<u16>(v - 2);
}
buf[writePos++] = v;
}
} else if (prim == GX_LINES || prim == GX_LINESTRIP || prim == GX_POINTS) {
buf = {0, 1, 3, 3, 2, 0};
writePos = 6;
} else
UNLIKELY FATAL("unsupported primitive type {}", static_cast<u32>(prim));
CHECK(writePos == buf.size(), "index template size mismatch ({} != {})", writePos, buf.size());
return static_cast<u32>(writePos);
}
// GX FIFO opcodes - use CP_ prefix to avoid clashing with GXCommandList.h macros
static constexpr u8 CP_CMD_NOP = GX_NOP;
static constexpr u8 CP_CMD_LOAD_CP_REG = GX_LOAD_CP_REG;
static constexpr u8 CP_CMD_LOAD_XF_REG = GX_LOAD_XF_REG;
static constexpr u8 CP_CMD_LOAD_INDX_A = GX_LOAD_INDX_A;
static constexpr u8 CP_CMD_LOAD_INDX_B = GX_LOAD_INDX_B;
static constexpr u8 CP_CMD_LOAD_INDX_C = GX_LOAD_INDX_C;
static constexpr u8 CP_CMD_LOAD_INDX_D = GX_LOAD_INDX_D;
static constexpr u8 CP_CMD_CALL_DL = GX_CMD_CALL_DL;
static constexpr u8 CP_CMD_INVAL_VTX = GX_CMD_INVL_VC;
static constexpr u8 CP_CMD_LOAD_BP_REG = GX_LOAD_BP_REG & GX_OPCODE_MASK;
// Primitive type mask
static constexpr u8 CP_OPCODE_MASK = GX_OPCODE_MASK;
static constexpr u8 CP_VAT_MASK = GX_VAT_MASK;
static constexpr u8 CP_PRIMITIVE_START = 0x80;
static constexpr u8 CP_PRIMITIVE_END = 0xBF;
// Read helpers for big/little endian
#if _MSC_VER
template <typename T>
__forceinline // Yes, this was necessary.
inline T unaligned_load(const T* ptr) {
return *static_cast<const __unaligned T*>(ptr);
}
#else
template <typename T>
inline T unaligned_load(const T* ptr) {
T copy;
memcpy(&copy, ptr, sizeof(T));
return copy;
}
#endif
static inline u16 read_u16(const u8* ptr, bool bigEndian) {
const u16 val = unaligned_load(reinterpret_cast<const u16*>(ptr));
if (bigEndian) {
return bswap(val);
}
return val;
}
static inline u32 read_u32(const u8* ptr, bool bigEndian) {
const u32 val = unaligned_load(reinterpret_cast<const u32*>(ptr));
if (bigEndian) {
return bswap(val);
}
return val;
}
static bool is_draw_cmd(u8 cmd) { return cmd >= CP_PRIMITIVE_START && cmd <= CP_PRIMITIVE_END; }
static GXPrimitive primitive_from_draw_cmd(u8 cmd) {
switch (cmd & CP_OPCODE_MASK) {
case GX_DRAW_QUADS:
case 0x88:
return GX_QUADS;
case GX_DRAW_TRIANGLES:
return GX_TRIANGLES;
case GX_DRAW_TRIANGLE_STRIP:
return GX_TRIANGLESTRIP;
case GX_DRAW_TRIANGLE_FAN:
return GX_TRIANGLEFAN;
case GX_DRAW_LINES:
return GX_LINES;
case GX_DRAW_LINE_STRIP:
return GX_LINESTRIP;
case GX_DRAW_POINTS:
return GX_POINTS;
default:
UNLIKELY FATAL("unsupported primitive command 0x{:02X}", cmd);
}
}
static u32 bp_get(u32 reg, u32 size, u32 shift);
static inline f32 read_f32(const u8* ptr, bool bigEndian);
static GXPixelFmt decode_pixel_fmt(u32 peCtrl, u32 cmode1) {
switch (bp_get(peCtrl, 3, 0)) {
case 0:
return GX_PF_RGB8_Z24;
case 1:
return GX_PF_RGBA6_Z24;
case 2:
return GX_PF_RGB565_Z16;
case 3:
return GX_PF_Z24;
case 4:
switch (bp_get(cmode1, 2, 9)) {
case 0:
return GX_PF_Y8;
case 1:
return GX_PF_U8;
case 2:
return GX_PF_V8;
default:
Log.warn("command_processor: unsupported cmode1 pixel subtype {}", bp_get(cmode1, 2, 9));
return GX_PF_Y8;
}
case 5:
return GX_PF_YUV420;
default:
Log.warn("command_processor: unsupported PE pixel format {}", bp_get(peCtrl, 3, 0));
return GX_PF_RGB8_Z24;
}
}
static inline u64 read_u64(const u8* ptr, bool bigEndian) {
u64 loaded;
// Unaligned-safe load
memcpy(&loaded, ptr, sizeof(u64));
if (bigEndian) {
return bswap(loaded);
}
return loaded;
}
struct TexBpRegMapping {
u8 texMapId;
enum class Kind : uint8_t { Mode0, Mode1, Image0, Image1, Image2, Image3, Tlut } kind;
};
static std::optional<TexBpRegMapping> decode_tex_bp_reg(u32 regId) {
constexpr std::array mode0Ids{0x80u, 0x81u, 0x82u, 0x83u, 0xA0u, 0xA1u, 0xA2u, 0xA3u};
constexpr std::array mode1Ids{0x84u, 0x85u, 0x86u, 0x87u, 0xA4u, 0xA5u, 0xA6u, 0xA7u};
constexpr std::array image0Ids{0x88u, 0x89u, 0x8Au, 0x8Bu, 0xA8u, 0xA9u, 0xAAu, 0xABu};
constexpr std::array image1Ids{0x8Cu, 0x8Du, 0x8Eu, 0x8Fu, 0xACu, 0xADu, 0xAEu, 0xAFu};
constexpr std::array image2Ids{0x90u, 0x91u, 0x92u, 0x93u, 0xB0u, 0xB1u, 0xB2u, 0xB3u};
constexpr std::array image3Ids{0x94u, 0x95u, 0x96u, 0x97u, 0xB4u, 0xB5u, 0xB6u, 0xB7u};
constexpr std::array tlutIds{0x98u, 0x99u, 0x9Au, 0x9Bu, 0xB8u, 0xB9u, 0xBAu, 0xBBu};
for (u8 i = 0; i < MaxTextures; ++i) {
if (regId == mode0Ids[i]) {
return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Mode0};
}
if (regId == mode1Ids[i]) {
return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Mode1};
}
if (regId == image0Ids[i]) {
return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image0};
}
if (regId == image1Ids[i]) {
return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image1};
}
if (regId == image2Ids[i]) {
return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image2};
}
if (regId == image3Ids[i]) {
return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Image3};
}
if (regId == tlutIds[i]) {
return TexBpRegMapping{.texMapId = i, .kind = TexBpRegMapping::Kind::Tlut};
}
}
return std::nullopt;
}
// Helper to convert packed RGBA8 to Vec4<float>
static Vec4<float> unpack_color(u32 packed) {
return {
static_cast<float>(packed >> 24 & 0xFF) / 255.f,
static_cast<float>(packed >> 16 & 0xFF) / 255.f,
static_cast<float>(packed >> 8 & 0xFF) / 255.f,
static_cast<float>(packed & 0xFF) / 255.f,
};
}
static inline f32 read_f32(const u8* ptr, bool bigEndian) {
u32 bits = read_u32(ptr, bigEndian);
f32 val;
std::memcpy(&val, &bits, sizeof(val));
return val;
}
// Marks draw state dirty *and* invalidates the resolved-pipeline memo.
static inline void mark_pipeline_state_dirty() noexcept {
g_gxState.stateDirty = true;
g_gxState.pipelineStateGeneration = next_gx_state_epoch();
}
// Rejects a malformed XF write in release builds.
#define XF_REQUIRE(cond, msg, ...) \
do { \
if (!(cond)) \
UNLIKELY { \
CHECK(cond, msg, ##__VA_ARGS__); \
Log.warn(msg, ##__VA_ARGS__); \
return true; \
} \
} while (0)
static bool copy_xf_data(u32 addr, const u8* data, u32 len, bool bigEndian) {
if (addr < 0x78) {
// Position matrices (0x0000 - 0x0077)
u32 mtxIdx = addr / 12;
u32 startOffset = addr % 12;
// We only support full writes to matrices
XF_REQUIRE(mtxIdx < MaxPnMtx, "XF: PosMtx copy oob; mtxIdx={}", mtxIdx);
XF_REQUIRE(startOffset == 0 && len == 12, "XF: PosMtx sub-copy unsupported: offs={}, len={}", startOffset, len);
auto& mtx = g_gxState.pnMtx[mtxIdx].pos;
f32* flat = reinterpret_cast<f32*>(&mtx);
for (u32 i = 0; i < len; i++) {
flat[i] = read_f32(data + i * 4, bigEndian);
}
g_gxState.stateDirty = true;
return true;
} else if (addr < 0x0F0) {
// Texture matrices (0x078-0x0EF)
u32 texBase = addr - 0x078;
u32 mtxIdx = texBase / 12;
u32 startOffset = texBase % 12;
XF_REQUIRE(mtxIdx < MaxTexMtx, "XF TexMtx copy oob; mtxIdx={}", mtxIdx);
XF_REQUIRE(startOffset == 0 && (len == 8 || len == 12), "XF TexMtx sub-copy unsupported: offs={}, len={}",
startOffset, len);
// Determine if 2x4 or 3x4 from count
auto& mtx = g_gxState.texMtxs[mtxIdx];
f32* flat = reinterpret_cast<f32*>(&mtx);
for (u32 i = 0; i < len; i++) {
flat[i] = read_f32(data + i * 4, bigEndian);
}
g_gxState.stateDirty = true;
return true;
} else if (addr >= 0x400 && addr < 0x45A) {
// Normal matrices (0x400-0x459)
u32 nrmBase = addr - 0x400;
u32 mtxIdx = nrmBase / 9;
u32 startOffset = nrmBase % 9;
// We only support full writes to matrices
XF_REQUIRE(mtxIdx < MaxPnMtx, "XF: NrmMtx copy oob; mtxIdx={}", mtxIdx);
XF_REQUIRE(startOffset == 0 && len == 9, "XF: NrmMtx sub-copy unsupported: offs={}, len={}", startOffset, len);
auto& mtx = g_gxState.pnMtx[mtxIdx].nrm;
f32* flat = reinterpret_cast<f32*>(&mtx);
for (u32 i = 0; i < len; i++) {
u32 xfIdx = i;
u32 row = xfIdx / 3;
u32 col = xfIdx % 3;
if (row < 3) {
flat[row * 4 + col] = read_f32(data + i * 4, bigEndian);
}
}
g_gxState.stateDirty = true;
return true;
} else if (addr >= 0x500 && addr < 0x5F0) {
// Post-transform texture matrices (0x500-0x5EF)
u32 ptBase = addr - 0x500;
u32 mtxIdx = ptBase / 12;
u32 startOffset = ptBase % 12;
XF_REQUIRE(mtxIdx < MaxPTTexMtx, "XF: PTTexMtx copy oob; mtxIdx={}", mtxIdx);
XF_REQUIRE(startOffset == 0 && len == 12, "XF: PTTexMtx sub-copy unsupported: offs={}, len={}", startOffset, len);
auto& mtx = g_gxState.ptTexMtxs[mtxIdx];
f32* flat = reinterpret_cast<f32*>(&mtx);
for (u32 i = 0; i < len; i++) {
flat[startOffset + i] = read_f32(data + i * 4, bigEndian);
}
g_gxState.stateDirty = true;
return true;
} else if (addr >= 0x600 && addr < 0x680) {
// Lights (0x600-0x67F) - 8 lights, 16 values each
u32 lightBase = addr - 0x600;
u32 lightIdx = lightBase / 0x10;
u32 startOffset = lightBase % 0x10;
XF_REQUIRE(lightIdx < GX::MaxLights, "XF: Light copy oob; lightIdx={}", lightIdx);
XF_REQUIRE(startOffset + len <= 0x10,
"XF: Light copy that crosses across light boundaries unsupported: offs={}, len={}", startOffset, len);
auto& light = g_gxState.lights[lightIdx];
for (u32 i = 0; i < len; i++) {
u32 field = startOffset + i;
f32 val = read_f32(data + i * 4, bigEndian);
u32 ival = read_u32(data + i * 4, bigEndian);
switch (field) {
case 3: // Color (packed u32)
light.color = unpack_color(ival);
break;
case 4:
light.cosAtt[0] = val;
break; // a0
case 5:
light.cosAtt[1] = val;
break; // a1
case 6:
light.cosAtt[2] = val;
break; // a2
case 7:
light.distAtt[0] = val;
break; // k0
case 8:
light.distAtt[1] = val;
break; // k1
case 9:
light.distAtt[2] = val;
break; // k2
case 10:
light.pos[0] = val;
break; // px
case 11:
light.pos[1] = val;
break; // py
case 12:
light.pos[2] = val;
break; // pz
case 13:
light.dir[0] = val;
break; // nx
case 14:
light.dir[1] = val;
break; // ny
case 15:
light.dir[2] = val;
break; // nz
default:
break; // padding (0-2)
}
}
g_gxState.preparedLightsDirty = true;
g_gxState.stateDirty = true;
return true;
}
return false;
}
static void apply_xf_viewport() {
const auto& vp = g_gxState.xfViewport;
const f32 sx = vp[0];
const f32 sy = vp[1];
const f32 sz = vp[2];
const f32 ox = vp[3];
const f32 oy = vp[4];
const f32 oz = vp[5];
const f32 width = sx * 2.0f;
const f32 height = -sy * 2.0f;
constexpr f32 z24Scale = 16777216.0f;
set_logical_viewport({
.left = ox - 340.0f - width / 2.0f,
.top = oy - 340.0f - height / 2.0f,
.width = width,
.height = height,
.znear = (oz - sz) / z24Scale,
.zfar = oz / z24Scale,
});
}
static void apply_xf_projection() {
const auto& raw = g_gxState.xfProjection;
auto& proj = g_gxState.proj;
proj = {};
proj.m0[0] = raw[0];
proj.m1[1] = raw[2];
proj.m2[2] = raw[4];
proj.m2[3] = raw[5];
if (g_gxState.projType == GX_ORTHOGRAPHIC) {
proj.m0[3] = raw[1];
proj.m1[3] = raw[3];
proj.m3[3] = 1.0f;
} else {
proj.m0[2] = raw[1];
proj.m1[2] = raw[3];
proj.m3[2] = -1.0f;
}
g_gxState.stateDirty = true;
}
// Forward declarations for register handlers
static void handle_bp(u32 value, bool bigEndian);
static void handle_cp(u8 addr, u32 value, bool bigEndian);
static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian);
static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian);
static bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian);
void process(const u8* data, u32 size, bool bigEndian) {
ZoneScoped;
// Everything decoded here mutates renderer state (GX state, the recorded command lists and the mapped staging
// buffers), so take the renderer GPU mutex once for the whole drain rather than once per draw command.
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
u32 pos = 0;
while (pos < size) {
u8 cmd = data[pos++];
u8 opcode = cmd & CP_OPCODE_MASK;
// Log.warn("Processing opcode {:02x} at pos {} (size {})", opcode, pos - 1, size);
switch (opcode) {
case CP_CMD_NOP:
continue;
case CP_CMD_LOAD_BP_REG: {
CHECK(pos + 4 <= size, "BP reg read overrun");
u32 value = read_u32(data + pos, bigEndian);
pos += 4;
handle_bp(value, bigEndian);
break;
}
case CP_CMD_LOAD_CP_REG: {
CHECK(pos + 5 <= size, "CP reg read overrun");
u8 addr = data[pos++];
u32 value = read_u32(data + pos, bigEndian);
pos += 4;
handle_cp(addr, value, bigEndian);
break;
}
case CP_CMD_LOAD_XF_REG: {
handle_xf(data, pos, size, bigEndian);
break;
}
case CP_CMD_LOAD_INDX_A:
case CP_CMD_LOAD_INDX_B:
case CP_CMD_LOAD_INDX_C:
case CP_CMD_LOAD_INDX_D: {
ZoneScopedN("LOAD_INDX");
CHECK(pos + 4 <= size, "indexed XF read overrun");
const u32 value = read_u32(data + pos, bigEndian);
pos += 4;
const u32 arrayType = GX_POS_MTX_ARRAY + ((opcode - CP_CMD_LOAD_INDX_A) / 0x08);
const u32 srcArrayIdx = value >> 16;
const u16 len = static_cast<u16>(((value >> 12) & 0x0f) + 1);
const u16 dstAddr = static_cast<u16>(value & 0x0fff);
auto const& array = g_gxState.arrays[arrayType];
const u32 byteOffset = srcArrayIdx * array.stride;
const u32 byteCount = static_cast<u32>(len) * 4u;
if (array.data == nullptr || array.stride == 0 || byteOffset + byteCount > array.size) {
static u32 invalidIndexedXfLogCount = 0;
if (invalidIndexedXfLogCount < 16) {
Log.warn(
"Skipping indexed XF load with invalid source array: array={} idx={} stride={} offset={} bytes={} "
"size={} dst=0x{:04X}",
arrayType, srcArrayIdx, array.stride, byteOffset, byteCount, array.size, dstAddr);
++invalidIndexedXfLogCount;
}
break;
}
u8* srcData = ((u8*)array.data) + byteOffset;
if (!copy_xf_data(dstAddr, srcData, len, bigEndian)) {
#ifndef NDEBUG
Log.debug("Unimplemented indexed XF load (opcode 0x{:02X}, dstAddr=0x{:04X})", opcode, dstAddr);
#endif
}
break;
}
case CP_CMD_CALL_DL: {
// Call display list: 8 bytes (address + size)
CHECK(pos + 8 <= size, "call DL read overrun");
Log.warn("Ignoring nested GX_CMD_CALL_DL");
pos += 8;
break;
}
case CP_CMD_INVAL_VTX: {
// GXInvalidateVtxCache tells the GPU that CPU-written indexed vertex arrays must be observed by subsequent draws.
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
g_gxState.arrays[i].cachedRange = {};
}
break;
}
case GX_LOAD_AURORA: {
if (!handle_aurora(data, pos, size, bigEndian)) {
return;
}
break;
}
default:
// Draw commands occupy the full 0x80-0xBF range.
if (is_draw_cmd(cmd)) {
if (!handle_draw(cmd, data, pos, size, bigEndian)) {
return;
}
} else {
static u32 unknownLogCount = 0;
if (unknownLogCount < 16) {
// Hex dump surrounding bytes for debugging
u32 dumpStart = (pos > 17) ? pos - 17 : 0;
u32 dumpEnd = (pos + 16 < size) ? pos + 16 : size;
std::string hex;
for (u32 i = dumpStart; i < dumpEnd; i++) {
if (i == pos - 1)
hex += fmt::format("[{:02x}]", data[i]);
else
hex += fmt::format(" {:02x}", data[i]);
}
Log.warn(" hex dump (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex);
Log.warn("command_processor: unknown opcode 0x{:02X} at pos {}", cmd, pos - 1);
++unknownLogCount;
}
}
break;
}
}
}
// Helper to extract bit fields from a 32-bit register
inline static u32 bp_get(u32 reg, u32 size, u32 shift) { return reg >> shift & (1u << size) - 1; }
static u8 normal_frac_bits(GXCompType type) {
switch (type) {
case GX_U8:
return 7;
case GX_S8:
return 6;
case GX_U16:
return 15;
case GX_S16:
return 14;
default:
return 0;
}
}
static void refresh_copy_filter_flags() {
bool aa = false;
for (const auto& sample : g_gxState.copyFilterSamplePattern) {
aa |= sample[0] != 6 || sample[1] != 6;
}
g_gxState.copyFilterAa = aa;
static constexpr std::array<u8, 7> DefaultVFilter{0, 0, 21, 22, 21, 0, 0};
bool vf = false;
for (size_t i = 0; i < DefaultVFilter.size(); ++i) {
vf |= g_gxState.copyFilterVFilter[i] != DefaultVFilter[i];
}
g_gxState.copyFilterVf = vf;
}
// BP register handler - decodes BP (RAS/pixel engine) register writes and updates g_gxState
static void handle_bp(u32 value, bool bigEndian) {
u32 regId = (value >> 24) & 0xFF;
// Mask off the register ID from the value for field extraction
// (the regId is stored in bits 24-31, data is in bits 0-23)
if (regId == 0xFE) {
g_gxState.bpRegCache[regId] = value & 0x00FFFFFF;
return;
} else {
const u32 ssMask = g_gxState.bpRegCache[0xFE];
// A preceding 0xFE write is rare; the common path only has to prove the mask is already wide open.
if (ssMask != 0x00FFFFFF)
UNLIKELY { g_gxState.bpRegCache[0xFE] = 0x00FFFFFF; }
const u32 merged = (g_gxState.bpRegCache[regId] & ~ssMask) | (value & ssMask);
value = (regId << 24) | (merged & 0x00FFFFFF);
if (g_gxState.bpRegCache[regId] == value)
return;
g_gxState.bpRegCache[regId] = value;
}
// TEV color combiner stages (0xC0, 0xC2, 0xC4, ... 0xDE)
if (regId >= 0xC0 && regId <= 0xDE && (regId & 1) == 0) {
u32 stage = (regId - 0xC0) / 2;
if (stage < MaxTevStages) {
auto& s = g_gxState.tevStages[stage];
s.colorPass.d = static_cast<GXTevColorArg>(bp_get(value, 4, 0));
s.colorPass.c = static_cast<GXTevColorArg>(bp_get(value, 4, 4));
s.colorPass.b = static_cast<GXTevColorArg>(bp_get(value, 4, 8));
s.colorPass.a = static_cast<GXTevColorArg>(bp_get(value, 4, 12));
s.colorOp.clamp = bp_get(value, 1, 19) != 0;
s.colorOp.outReg = static_cast<GXTevRegID>(bp_get(value, 2, 22));
if (bp_get(value, 2, 16) == 3) {
// Bias==3 means compare mode: reconstruct GXTevOp enum (8 + 3-bit hw value)
u32 hwOp = bp_get(value, 1, 18) | (bp_get(value, 2, 20) << 1);
s.colorOp.op = static_cast<GXTevOp>(hwOp + 8);
s.colorOp.bias = GX_TB_ZERO;
s.colorOp.scale = GX_CS_SCALE_1;
} else {
// Normal mode: bit18 is op (0=ADD, 1=SUB), bits16-17 is bias, bits20-21 is scale
s.colorOp.op = static_cast<GXTevOp>(bp_get(value, 1, 18));
s.colorOp.bias = static_cast<GXTevBias>(bp_get(value, 2, 16));
s.colorOp.scale = static_cast<GXTevScale>(bp_get(value, 2, 20));
}
mark_pipeline_state_dirty();
}
return;
}
// TEV alpha combiner stages (0xC1, 0xC3, 0xC5, ... 0xDF)
if (regId >= 0xC1 && regId <= 0xDF && (regId & 1) == 1) {
u32 stage = (regId - 0xC1) / 2;
if (stage < MaxTevStages) {
auto& s = g_gxState.tevStages[stage];
s.tevSwapRas = static_cast<GXTevSwapSel>(bp_get(value, 2, 0));
s.tevSwapTex = static_cast<GXTevSwapSel>(bp_get(value, 2, 2));
s.alphaPass.d = static_cast<GXTevAlphaArg>(bp_get(value, 3, 4));
s.alphaPass.c = static_cast<GXTevAlphaArg>(bp_get(value, 3, 7));
s.alphaPass.b = static_cast<GXTevAlphaArg>(bp_get(value, 3, 10));
s.alphaPass.a = static_cast<GXTevAlphaArg>(bp_get(value, 3, 13));
s.alphaOp.clamp = bp_get(value, 1, 19) != 0;
s.alphaOp.outReg = static_cast<GXTevRegID>(bp_get(value, 2, 22));
if (bp_get(value, 2, 16) == 3) {
u32 hwOp = bp_get(value, 1, 18) | (bp_get(value, 2, 20) << 1);
s.alphaOp.op = static_cast<GXTevOp>(hwOp + 8);
s.alphaOp.bias = GX_TB_ZERO;
s.alphaOp.scale = GX_CS_SCALE_1;
} else {
s.alphaOp.op = static_cast<GXTevOp>(bp_get(value, 1, 18));
s.alphaOp.bias = static_cast<GXTevBias>(bp_get(value, 2, 16));
s.alphaOp.scale = static_cast<GXTevScale>(bp_get(value, 2, 20));
}
mark_pipeline_state_dirty();
}
return;
}
switch (regId) {
// genMode (0x00)
case 0x00: {
g_gxState.numTexGens = bp_get(value, 4, 0);
g_gxState.numChans = bp_get(value, 3, 4);
// genMode owns the same numTexGens/numChans that XF 0x3F/0x09 decode, so the XF cache can no longer vouch for those
// two slots.
g_gxState.invalidateXfReg(0x3F);
g_gxState.invalidateXfReg(0x09);
g_gxState.numTevStages = bp_get(value, 4, 10) + 1;
u32 hwCull = bp_get(value, 2, 14);
// Swap front/back to match GX convention
switch (hwCull) {
case GX_CULL_FRONT:
g_gxState.cullMode = GX_CULL_BACK;
break;
case GX_CULL_BACK:
g_gxState.cullMode = GX_CULL_FRONT;
break;
default:
g_gxState.cullMode = static_cast<GXCullMode>(hwCull);
break;
}
g_gxState.numIndStages = bp_get(value, 3, 16);
mark_pipeline_state_dirty();
break;
}
// Display copy sample pattern (0x01-0x04), six 4-bit samples per BP reg.
case 0x01:
case 0x02:
case 0x03:
case 0x04: {
const size_t first = static_cast<size_t>(regId - 0x01) * 6;
for (size_t i = 0; i < 6; ++i) {
const size_t sample = first + i;
g_gxState.copyFilterSamplePattern[sample / 2][sample % 2] = static_cast<u8>(bp_get(value, 4, i * 4));
}
refresh_copy_filter_flags();
break;
}
// Indirect texture mask (0x0F).
case 0x0F:
g_gxState.indTexMask = static_cast<u8>(value & 0xFF);
g_gxState.stateDirty = true;
break;
// TEV indirect stages (0x10-0x1F)
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x17:
case 0x18:
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
case 0x1F: {
u32 stage = regId - 0x10;
if (stage < MaxTevStages) {
auto& s = g_gxState.tevStages[stage];
s.indTexStage = static_cast<GXIndTexStageID>(bp_get(value, 2, 0));
s.indTexFormat = static_cast<GXIndTexFormat>(bp_get(value, 2, 2));
s.indTexBiasSel = static_cast<GXIndTexBiasSel>(bp_get(value, 3, 4));
s.indTexAlphaSel = static_cast<GXIndTexAlphaSel>(bp_get(value, 2, 7));
s.indTexMtxId = static_cast<GXIndTexMtxID>(bp_get(value, 4, 9));
s.indTexWrapS = static_cast<GXIndTexWrap>(bp_get(value, 3, 13));
s.indTexWrapT = static_cast<GXIndTexWrap>(bp_get(value, 3, 16));
s.indTexUseOrigLOD = bp_get(value, 1, 19) != 0;
s.indTexAddPrev = bp_get(value, 1, 20) != 0;
mark_pipeline_state_dirty();
}
break;
}
// Scissor registers (0x20, 0x21)
case 0x20:
case 0x21: {
const u32 scis0 = g_gxState.bpRegCache[0x20];
const u32 scis1 = g_gxState.bpRegCache[0x21];
const int32_t tp = static_cast<int32_t>(bp_get(scis0, 11, 0)) - 342;
const int32_t lf = static_cast<int32_t>(bp_get(scis0, 11, 12)) - 342;
const int32_t bm = static_cast<int32_t>(bp_get(scis1, 11, 0)) - 342;
const int32_t rt = static_cast<int32_t>(bp_get(scis1, 11, 12)) - 342;
const int32_t wd = std::max(rt - lf + 1, 0);
const int32_t ht = std::max(bm - tp + 1, 0);
set_logical_scissor({lf, tp, wd, ht});
break;
}
// Line/point size (0x22)
case 0x22: {
g_gxState.lineWidth = static_cast<u8>(bp_get(value, 8, 0));
g_gxState.pointSize = static_cast<u8>(bp_get(value, 8, 8));
g_gxState.lineTexOffset = static_cast<GXTexOffset>(bp_get(value, 3, 16));
g_gxState.pointTexOffset = static_cast<GXTexOffset>(bp_get(value, 3, 19));
g_gxState.lineHalfAspect = bp_get(value, 1, 22) != 0;
g_gxState.stateDirty = true;
break;
}
// Indirect texture scale (0x25, 0x26)
case 0x25: {
if (MaxIndStages > 0) {
g_gxState.indStages[0].scaleS = static_cast<GXIndTexScale>(bp_get(value, 4, 0));
g_gxState.indStages[0].scaleT = static_cast<GXIndTexScale>(bp_get(value, 4, 4));
}
if (MaxIndStages > 1) {
g_gxState.indStages[1].scaleS = static_cast<GXIndTexScale>(bp_get(value, 4, 8));
g_gxState.indStages[1].scaleT = static_cast<GXIndTexScale>(bp_get(value, 4, 12));
}
mark_pipeline_state_dirty();
break;
}
case 0x26: {
if (MaxIndStages > 2) {
g_gxState.indStages[2].scaleS = static_cast<GXIndTexScale>(bp_get(value, 4, 0));
g_gxState.indStages[2].scaleT = static_cast<GXIndTexScale>(bp_get(value, 4, 4));
}
if (MaxIndStages > 3) {
g_gxState.indStages[3].scaleS = static_cast<GXIndTexScale>(bp_get(value, 4, 8));
g_gxState.indStages[3].scaleT = static_cast<GXIndTexScale>(bp_get(value, 4, 12));
}
mark_pipeline_state_dirty();
break;
}
// Indirect texture reference (0x27)
case 0x27: {
for (u32 i = 0; i < MaxIndStages; i++) {
g_gxState.indStages[i].texMapId = static_cast<GXTexMapID>(bp_get(value, 3, i * 6));
g_gxState.indStages[i].texCoordId = static_cast<GXTexCoordID>(bp_get(value, 3, i * 6 + 3));
}
mark_pipeline_state_dirty();
break;
}
// TEV order / tref (0x28-0x2F) - 2 stages per register
case 0x28:
case 0x29:
case 0x2A:
case 0x2B:
case 0x2C:
case 0x2D:
case 0x2E:
case 0x2F: {
u32 idx = regId - 0x28;
u32 stage0 = idx * 2;
u32 stage1 = idx * 2 + 1;
// Channel ID reverse mapping from hardware to GX
static const GXChannelID r2c[] = {GX_COLOR0A0, GX_COLOR1A1, GX_COLOR0A0, GX_COLOR1A1,
GX_COLOR0A0, GX_ALPHA_BUMP, GX_ALPHA_BUMPN, GX_COLOR_ZERO};
if (stage0 < MaxTevStages) {
auto& s = g_gxState.tevStages[stage0];
s.texMapId = static_cast<GXTexMapID>(bp_get(value, 3, 0));
s.texCoordId = static_cast<GXTexCoordID>(bp_get(value, 3, 3));
// bit 6 = tex enable
if (!bp_get(value, 1, 6)) {
s.texMapId = GX_TEXMAP_NULL;
}
u32 chanHw = bp_get(value, 3, 7);
s.channelId = (chanHw < 8) ? r2c[chanHw] : GX_COLOR_NULL;
}
if (stage1 < MaxTevStages) {
auto& s = g_gxState.tevStages[stage1];
s.texMapId = static_cast<GXTexMapID>(bp_get(value, 3, 12));
s.texCoordId = static_cast<GXTexCoordID>(bp_get(value, 3, 15));
if (!bp_get(value, 1, 18)) {
s.texMapId = GX_TEXMAP_NULL;
}
u32 chanHw = bp_get(value, 3, 19);
s.channelId = (chanHw < 8) ? r2c[chanHw] : GX_COLOR_NULL;
}
mark_pipeline_state_dirty();
break;
}
// Z mode (0x40)
case 0x40: {
g_gxState.depthCompare = bp_get(value, 1, 0) != 0;
g_gxState.depthFunc = static_cast<GXCompare>(bp_get(value, 3, 1));
g_gxState.depthUpdate = bp_get(value, 1, 4) != 0;
mark_pipeline_state_dirty();
break;
}
// Blend mode / cmode0 (0x41)
case 0x41: {
bool blendEn = bp_get(value, 1, 0) != 0;
bool logicEn = bp_get(value, 1, 1) != 0;
bool dither = bp_get(value, 1, 2) != 0;
g_gxState.colorUpdate = bp_get(value, 1, 3) != 0;
g_gxState.alphaUpdate = bp_get(value, 1, 4) != 0;
g_gxState.blendFacDst = static_cast<GXBlendFactor>(bp_get(value, 3, 5));
g_gxState.blendFacSrc = static_cast<GXBlendFactor>(bp_get(value, 3, 8));
bool subtract = bp_get(value, 1, 11) != 0;
g_gxState.blendOp = static_cast<GXLogicOp>(bp_get(value, 4, 12));
if (subtract) {
g_gxState.blendMode = GX_BM_SUBTRACT;
} else if (blendEn) {
g_gxState.blendMode = GX_BM_BLEND;
} else if (logicEn) {
g_gxState.blendMode = GX_BM_LOGIC;
} else {
g_gxState.blendMode = GX_BM_NONE;
}
mark_pipeline_state_dirty();
break;
}
// Dst alpha / cmode1 (0x42)
case 0x42: {
u8 alpha = bp_get(value, 8, 0);
bool enabled = bp_get(value, 1, 8) != 0;
g_gxState.dstAlpha = enabled ? alpha : UINT32_MAX;
g_gxState.pixelFmt = decode_pixel_fmt(g_gxState.bpRegCache[0x43], value);
mark_pipeline_state_dirty();
break;
}
// PE control (0x43) - pixel format, z format, zcomp location
case 0x43: {
g_gxState.pixelFmt = decode_pixel_fmt(value, g_gxState.bpRegCache[0x42]);
g_gxState.zFmt = static_cast<GXZFmt16>(bp_get(value, 3, 3));
g_gxState.zCompLocBeforeTex = bp_get(value, 1, 6) != 0;
mark_pipeline_state_dirty();
break;
}
case 0x44:
g_gxState.fieldMask = value & 0x3u;
break;
// Bounding box clear/update registers (0x55, 0x56)
case 0x55:
case 0x56: {
const u32 offset = (regId & 2u);
g_gxState.boundingBox[offset] = static_cast<u16>(value & 0x3ffu);
g_gxState.boundingBox[offset + 1] = static_cast<u16>((value >> 10) & 0x3ffu);
break;
}
case 0x58:
g_gxState.revBits = value & 0x00FFFFFFu;
break;
// Scissor box offset (0x59)
case 0x59: {
g_gxState.scissorOffsetX = static_cast<s32>(((value & 0x3ffu) << 1) - 0x156u);
g_gxState.scissorOffsetY = static_cast<s32>(((value & 0xffc00u) >> 9) - 0x156u);
set_logical_scissor(g_gxState.logicalScissor);
break;
}
// TLUT load address / execute (0x64, 0x65)
case 0x64:
break;
case 0x65: {
const auto idx = bp_get(value, 10, 0);
if (idx < MaxTluts) {
auto& slot = g_gxState.loadedTluts[idx];
slot.loadTlut0 = g_gxState.bpRegCache[0x64];
slot.numEntries = static_cast<u16>(bp_get(value, 10, 10) + 1);
}
break;
}
case 0x68:
g_gxState.fieldMode = value & 0x1u;
break;
// Alpha compare (0xF3)
case 0xF3: {
g_gxState.alphaCompare.ref0 = bp_get(value, 8, 0);
g_gxState.alphaCompare.ref1 = bp_get(value, 8, 8);
g_gxState.alphaCompare.comp0 = static_cast<GXCompare>(bp_get(value, 3, 16));
g_gxState.alphaCompare.comp1 = static_cast<GXCompare>(bp_get(value, 3, 19));
g_gxState.alphaCompare.op = static_cast<GXAlphaOp>(bp_get(value, 2, 22));
mark_pipeline_state_dirty();
break;
}
// TEV K color/alpha select (0xF6-0xFD)
case 0xF6:
case 0xF7:
case 0xF8:
case 0xF9:
case 0xFA:
case 0xFB:
case 0xFC:
case 0xFD: {
u32 kselIdx = regId - 0xF6;
// Swap table entries (packed into pairs of ksel registers)
if (kselIdx < MaxTevSwap * 2) {
u32 swapIdx = kselIdx / 2;
if (kselIdx & 1) {
g_gxState.tevSwapTable[swapIdx].blue = static_cast<GXTevColorChan>(bp_get(value, 2, 0));
g_gxState.tevSwapTable[swapIdx].alpha = static_cast<GXTevColorChan>(bp_get(value, 2, 2));
} else {
g_gxState.tevSwapTable[swapIdx].red = static_cast<GXTevColorChan>(bp_get(value, 2, 0));
g_gxState.tevSwapTable[swapIdx].green = static_cast<GXTevColorChan>(bp_get(value, 2, 2));
}
}
// K color/alpha selection for 2 stages per register
u32 stage0 = kselIdx * 2;
u32 stage1 = kselIdx * 2 + 1;
if (stage0 < MaxTevStages) {
g_gxState.tevStages[stage0].kcSel = static_cast<GXTevKColorSel>(bp_get(value, 5, 4));
g_gxState.tevStages[stage0].kaSel = static_cast<GXTevKAlphaSel>(bp_get(value, 5, 9));
}
if (stage1 < MaxTevStages) {
g_gxState.tevStages[stage1].kcSel = static_cast<GXTevKColorSel>(bp_get(value, 5, 14));
g_gxState.tevStages[stage1].kaSel = static_cast<GXTevKAlphaSel>(bp_get(value, 5, 19));
}
mark_pipeline_state_dirty();
break;
}
// Fog A/B parameters (0xEE-0xF0)
// FOG0 (0xEE): A parameter - sign(1)|exp(8)|mantissa(11) partial IEEE 754 float
case 0xEE: {
g_gxState.fog.fog0Raw = value;
u32 a_mant = bp_get(value, 11, 0);
u32 a_exp = bp_get(value, 8, 11);
u32 a_sign = bp_get(value, 1, 19);
u32 a_bits = (a_sign << 31) | (a_exp << 23) | (a_mant << 12);
std::memcpy(&g_gxState.fog.aRaw, &a_bits, sizeof(g_gxState.fog.aRaw));
u32 b_s = g_gxState.fog.fog2Raw & 0x1F;
g_gxState.fog.a = std::ldexp(g_gxState.fog.aRaw, static_cast<int>(b_s));
g_gxState.stateDirty = true;
break;
}
// FOG1 (0xEF): B mantissa (24-bit)
case 0xEF: {
g_gxState.fog.fog1Raw = value;
g_gxState.fog.bMagnitude = bp_get(value, 24, 0);
u32 b_s = g_gxState.fog.fog2Raw & 0x1F;
float B_mant = static_cast<float>(g_gxState.fog.bMagnitude) / 8388638.0f;
g_gxState.fog.b = std::ldexp(B_mant, static_cast<int>(b_s) - 1);
g_gxState.stateDirty = true;
break;
}
// FOG2 (0xF0): B shift/exponent (5-bit)
case 0xF0: {
g_gxState.fog.fog2Raw = value;
u32 b_s = bp_get(value, 5, 0);
g_gxState.fog.bShift = b_s;
u32 a_mant = bp_get(g_gxState.fog.fog0Raw, 11, 0);
u32 a_exp = bp_get(g_gxState.fog.fog0Raw, 8, 11);
u32 a_sign = bp_get(g_gxState.fog.fog0Raw, 1, 19);
u32 a_bits = (a_sign << 31) | (a_exp << 23) | (a_mant << 12);
std::memcpy(&g_gxState.fog.aRaw, &a_bits, sizeof(g_gxState.fog.aRaw));
g_gxState.fog.a = std::ldexp(g_gxState.fog.aRaw, static_cast<int>(b_s));
g_gxState.fog.bMagnitude = bp_get(g_gxState.fog.fog1Raw, 24, 0);
float B_mant = static_cast<float>(g_gxState.fog.bMagnitude) / 8388638.0f;
g_gxState.fog.b = std::ldexp(B_mant, static_cast<int>(b_s) - 1);
g_gxState.stateDirty = true;
break;
}
// Fog type + C parameter from FOG3 (0xF1)
case 0xF1: {
const u32 fogFunc = bp_get(value, 3, 21);
const u32 fogProj = bp_get(value, 1, 20);
GXFogType fogType = static_cast<GXFogType>(fogFunc | (fogProj << 3));
g_gxState.fog.type = fogType;
// Decode C parameter (same partial float encoding as A)
u32 c_mant = bp_get(value, 11, 0);
u32 c_exp = bp_get(value, 8, 11);
u32 c_sign = bp_get(value, 1, 19);
u32 c_bits = (c_sign << 31) | (c_exp << 23) | (c_mant << 12);
std::memcpy(&g_gxState.fog.c, &c_bits, sizeof(g_gxState.fog.c));
mark_pipeline_state_dirty();
break;
}
// Fog color from FOGCLR (0xF2)
case 0xF2: {
u8 b = bp_get(value, 8, 0);
u8 g = bp_get(value, 8, 8);
u8 r = bp_get(value, 8, 16);
g_gxState.fog.color = {
static_cast<float>(r) / 255.f,
static_cast<float>(g) / 255.f,
static_cast<float>(b) / 255.f,
1.f,
};
g_gxState.stateDirty = true;
break;
}
// TEV and K color registers (0xE0-0xE7): even are RA, odd are BG.
// Bit 23 selects a K color register over a TEV color register.
case 0xE0:
case 0xE1:
case 0xE2:
case 0xE3:
case 0xE4:
case 0xE5:
case 0xE6:
case 0xE7: {
u32 idx = (regId - 0xE0) / 2;
bool isRA = (regId & 1) == 0;
bool isKColor = bp_get(value, 1, 23) != 0;
if (isKColor) {
// K color register (8-bit components)
if (idx < GX_MAX_KCOLOR) {
auto& kc = g_gxState.kcolors[idx];
if (isRA) {
kc[0] = static_cast<float>(bp_get(value, 8, 0)) / 255.f; // R
kc[3] = static_cast<float>(bp_get(value, 8, 12)) / 255.f; // A
} else {
kc[2] = static_cast<float>(bp_get(value, 8, 0)) / 255.f; // B
kc[1] = static_cast<float>(bp_get(value, 8, 12)) / 255.f; // G
}
g_gxState.stateDirty = true;
}
} else {
// TEV color register (11-bit signed components)
if (idx < MaxTevRegs) {
auto& cr = g_gxState.colorRegs[idx];
if (isRA) {
// 11-bit signed: sign-extend from 11 bits
s32 r = bp_get(value, 11, 0);
if (r & 0x400)
r |= ~0x7FF; // sign extend
s32 a = bp_get(value, 11, 12);
if (a & 0x400)
a |= ~0x7FF;
cr[0] = static_cast<float>(r) / 255.f;
cr[3] = static_cast<float>(a) / 255.f;
} else {
s32 b = bp_get(value, 11, 0);
if (b & 0x400)
b |= ~0x7FF;
s32 g = bp_get(value, 11, 12);
if (g & 0x400)
g |= ~0x7FF;
cr[2] = static_cast<float>(b) / 255.f;
cr[1] = static_cast<float>(g) / 255.f;
}
g_gxState.stateDirty = true;
}
}
break;
}
// Indirect texture matrices (0x06-0x0E), three consecutive registers per 3x2 matrix:
// matrix 0 at 0x06, matrix 1 at 0x09, matrix 2 at 0x0C.
case 0x06:
case 0x07:
case 0x08:
case 0x09:
case 0x0A:
case 0x0B:
case 0x0C:
case 0x0D:
case 0x0E: {
u32 idx = (regId - 0x06) / 3; // matrix index (0-2)
u32 column = (regId - 0x06) % 3; // column index (0-2)
auto& info = g_gxState.indTexMtxs[idx];
// Decode one packed matrix column: [m[0][column], m[1][column]].
s32 col0 = bp_get(value, 11, 0);
if (col0 & 0x400)
col0 |= ~0x7FF; // sign-extend from 11 bits
s32 col1 = bp_get(value, 11, 11);
if (col1 & 0x400)
col1 |= ~0x7FF;
auto& packedColumn = column == 0 ? info.mtx.m0 : (column == 1 ? info.mtx.m1 : info.mtx.m2);
packedColumn.x = static_cast<float>(col0) / 1024.0f;
packedColumn.y = static_cast<float>(col1) / 1024.0f;
// Accumulate the indirect matrix scale exponent. The SDK writes two bits per column, but the
// hardware ignores the third column's top bit, leaving 5 bits for adjScale = scaleExp + 17.
u32 scaleBits = bp_get(value, 2, 22);
u32 shift = column * 2;
if (column == 2) {
info.adjScaleRaw = (info.adjScaleRaw & ~(1u << shift)) | ((scaleBits & 1u) << shift);
} else {
info.adjScaleRaw = (info.adjScaleRaw & ~(3u << shift)) | (scaleBits << shift);
}
info.scaleExp = static_cast<s8>(info.adjScaleRaw) - 17;
g_gxState.stateDirty = true;
break;
}
// SU texture coordinate scale registers (0x30-0x3F): even (suTs0) carry S-axis scale, bias, cyl
// wrap and line/point offset; odd (suTs1) carry the T-axis equivalents.
case 0x30:
case 0x31:
case 0x32:
case 0x33:
case 0x34:
case 0x35:
case 0x36:
case 0x37:
case 0x38:
case 0x39:
case 0x3A:
case 0x3B:
case 0x3C:
case 0x3D:
case 0x3E:
case 0x3F: {
u32 coordIdx = (regId - 0x30) / 2;
bool isT = (regId & 1) != 0;
auto& tcs = g_gxState.texCoordScales[coordIdx];
if (isT) {
tcs.scaleT = static_cast<u16>(bp_get(value, 16, 0));
tcs.biasT = bp_get(value, 1, 16) != 0;
tcs.cylWrapT = bp_get(value, 1, 17) != 0;
} else {
tcs.scaleS = static_cast<u16>(bp_get(value, 16, 0));
tcs.biasS = bp_get(value, 1, 16) != 0;
tcs.cylWrapS = bp_get(value, 1, 17) != 0;
tcs.lineOffset = bp_get(value, 1, 18) != 0;
tcs.pointOffset = bp_get(value, 1, 19) != 0;
}
g_gxState.stateDirty = true;
break;
}
// Copy clear color (0x4F-0x50) and depth (0x51)
case 0x49: {
g_gxState.dispCopySrc.x = static_cast<int32_t>(bp_get(value, 10, 0));
g_gxState.dispCopySrc.y = static_cast<int32_t>(bp_get(value, 10, 10));
break;
}
case 0x4A: {
g_gxState.dispCopySrc.width = static_cast<int32_t>(bp_get(value, 10, 0) + 1u);
g_gxState.dispCopySrc.height = static_cast<int32_t>(bp_get(value, 10, 10) + 1u);
break;
}
case 0x4D: {
g_gxState.dispCopyDstWidth = static_cast<u16>(((bp_get(value, 10, 0) << 5) + 31u) >> 1);
break;
}
case 0x4E: {
const u32 iScale = bp_get(value, 9, 0);
if (iScale != 0) {
g_gxState.dispCopyYScale = 256.f / static_cast<float>(iScale);
}
break;
}
case 0x4F: {
u8 r = bp_get(value, 8, 0);
u8 a = bp_get(value, 8, 8);
g_gxState.clearColor[0] = static_cast<float>(r) / 255.f;
g_gxState.clearColor[3] = static_cast<float>(a) / 255.f;
g_gxState.stateDirty = true;
break;
}
case 0x50: {
u8 b = bp_get(value, 8, 0);
u8 g = bp_get(value, 8, 8);
g_gxState.clearColor[2] = static_cast<float>(b) / 255.f;
g_gxState.clearColor[1] = static_cast<float>(g) / 255.f;
g_gxState.stateDirty = true;
break;
}
case 0x51: {
g_gxState.clearDepth = bp_get(value, 24, 0);
g_gxState.stateDirty = true;
break;
}
case 0xF4: {
g_gxState.zTextureBias = value & 0x00FFFFFFu;
mark_pipeline_state_dirty();
break;
}
case 0xF5: {
g_gxState.zTextureFmt = static_cast<u8>(bp_get(value, 2, 0));
g_gxState.zTextureOp = static_cast<GXZTexOp>(bp_get(value, 2, 2));
mark_pipeline_state_dirty();
break;
}
case 0x52: {
g_gxState.copyClamp = static_cast<GXFBClamp>(bp_get(value, 2, 0));
g_gxState.texCopyFmt = static_cast<GXTexFmt>(bp_get(value, 4, 3));
g_gxState.dispCopyGamma = static_cast<GXGamma>(bp_get(value, 2, 7));
g_gxState.texCopyHalfScale = bp_get(value, 1, 9) != 0;
g_gxState.dispCopyFrame2Field = bp_get(value, 2, 12);
break;
}
case 0x53: {
g_gxState.copyFilterVFilter[0] = static_cast<u8>(bp_get(value, 6, 0));
g_gxState.copyFilterVFilter[1] = static_cast<u8>(bp_get(value, 6, 6));
g_gxState.copyFilterVFilter[2] = static_cast<u8>(bp_get(value, 6, 12));
g_gxState.copyFilterVFilter[3] = static_cast<u8>(bp_get(value, 6, 18));
refresh_copy_filter_flags();
break;
}
case 0x54: {
g_gxState.copyFilterVFilter[4] = static_cast<u8>(bp_get(value, 6, 0));
g_gxState.copyFilterVFilter[5] = static_cast<u8>(bp_get(value, 6, 6));
g_gxState.copyFilterVFilter[6] = static_cast<u8>(bp_get(value, 6, 12));
refresh_copy_filter_flags();
break;
}
case 0xE8:
case 0xE9:
case 0xEA:
case 0xEB:
case 0xEC:
case 0xED:
g_gxState.fogRange[regId - 0xE8] = value & 0x00FFFFFFu;
mark_pipeline_state_dirty();
break;
default:
if (const auto mapping = decode_tex_bp_reg(regId); mapping.has_value()) {
auto& slot = g_gxState.loadedTextures[mapping->texMapId];
bool changed = false;
switch (mapping->kind) {
case TexBpRegMapping::Kind::Mode0:
changed = slot.mode0 != value;
if (changed) {
slot.mode0 = value;
}
break;
case TexBpRegMapping::Kind::Mode1:
changed = slot.mode1 != value;
if (changed) {
slot.mode1 = value;
}
break;
case TexBpRegMapping::Kind::Image0:
changed = slot.image0 != value;
if (changed) {
slot.image0 = value;
slot.mWidth = 0;
slot.mHeight = 0;
slot.mFormat = gfx::InvalidTextureFormat;
}
break;
case TexBpRegMapping::Kind::Image3:
changed = slot.image3 != value;
if (changed) {
slot.image3 = value;
}
break;
case TexBpRegMapping::Kind::Tlut:
// TLUT region's TMEM offset
break;
case TexBpRegMapping::Kind::Image1:
case TexBpRegMapping::Kind::Image2:
// GXTexRegion regs
break;
}
if (changed) {
g_gxState.stateDirty = true;
}
} else {
#ifndef NDEBUG
Log.debug("Unhandled BP register 0x{:02X} (value 0x{:06X})", regId, value & 0xFFFFFF);
#endif
}
break;
}
}
extern "C" void GXApplyBPReg(u8 reg, u32 value) {
handle_bp((static_cast<u32>(reg) << 24) | (value & 0x00FFFFFFu), true);
}
static bool cacheable_cp_register(u8 addr) {
return addr == 0x30 || addr == 0x40 || addr == 0x50 || addr == 0x60 || (addr >= 0x70 && addr <= 0x97);
}
static std::array<u32, 0x100> s_cpRegisterCache{};
static std::array<bool, 0x100> s_cpRegisterCacheValid{};
void reset_cp_register_cache() {
s_cpRegisterCache.fill(0);
s_cpRegisterCacheValid.fill(false);
}
static bool cp_register_write_unchanged(u8 addr, u32 value) {
if (!cacheable_cp_register(addr))
return false;
if (s_cpRegisterCacheValid[addr] && s_cpRegisterCache[addr] == value) {
return true;
}
s_cpRegisterCacheValid[addr] = true;
s_cpRegisterCache[addr] = value;
return false;
}
// CP register handler - decodes CP register writes and updates g_gxState
static void handle_cp(u8 addr, u32 value, bool bigEndian) {
if (cp_register_write_unchanged(addr, value))
return;
switch (addr) {
// VCD low (0x50)
case 0x50: {
auto& vd = g_gxState.vtxDesc;
auto& svd = g_gxState.sourceVtxDesc;
vd[GX_VA_PNMTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 0));
vd[GX_VA_TEX0MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 1));
vd[GX_VA_TEX1MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 2));
vd[GX_VA_TEX2MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 3));
vd[GX_VA_TEX3MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 4));
vd[GX_VA_TEX4MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 5));
vd[GX_VA_TEX5MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 6));
vd[GX_VA_TEX6MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 7));
vd[GX_VA_TEX7MTXIDX] = static_cast<GXAttrType>(bp_get(value, 1, 8));
vd[GX_VA_POS] = static_cast<GXAttrType>(bp_get(value, 2, 9));
vd[GX_VA_NRM] = static_cast<GXAttrType>(bp_get(value, 2, 11));
vd[GX_VA_CLR0] = static_cast<GXAttrType>(bp_get(value, 2, 13));
vd[GX_VA_CLR1] = static_cast<GXAttrType>(bp_get(value, 2, 15));
for (int attr = GX_VA_PNMTXIDX; attr <= GX_VA_CLR1; ++attr) {
svd[attr] = vd[attr];
}
mark_pipeline_state_dirty();
g_gxState.clearVtxSizeCache();
break;
}
// VCD high (0x60)
case 0x60: {
auto& vd = g_gxState.vtxDesc;
auto& svd = g_gxState.sourceVtxDesc;
vd[GX_VA_TEX0] = static_cast<GXAttrType>(bp_get(value, 2, 0));
vd[GX_VA_TEX1] = static_cast<GXAttrType>(bp_get(value, 2, 2));
vd[GX_VA_TEX2] = static_cast<GXAttrType>(bp_get(value, 2, 4));
vd[GX_VA_TEX3] = static_cast<GXAttrType>(bp_get(value, 2, 6));
vd[GX_VA_TEX4] = static_cast<GXAttrType>(bp_get(value, 2, 8));
vd[GX_VA_TEX5] = static_cast<GXAttrType>(bp_get(value, 2, 10));
vd[GX_VA_TEX6] = static_cast<GXAttrType>(bp_get(value, 2, 12));
vd[GX_VA_TEX7] = static_cast<GXAttrType>(bp_get(value, 2, 14));
for (int attr = GX_VA_TEX0; attr <= GX_VA_TEX7; ++attr) {
svd[attr] = vd[attr];
}
mark_pipeline_state_dirty();
g_gxState.clearVtxSizeCache();
break;
}
// Matrix index A (0x30)
case 0x30: {
g_gxState.currentPnMtx = bp_get(value, 6, 0) / 3;
for (u32 i = 0; i < 4 && i < MaxTexCoord; i++) {
auto texMtx = static_cast<GXTexMtx>(bp_get(value, 6, 6 + i * 6));
assert(texMtx >= 0 && texMtx <= GXTexMtx::GX_IDENTITY);
g_gxState.tcgs[i].mtx = texMtx;
}
// Same matrix indices as XF 0x18, written from a different bank.
g_gxState.invalidateXfReg(0x18);
mark_pipeline_state_dirty();
break;
}
// Matrix index B (0x40)
case 0x40: {
for (u32 i = 0; i < 4 && (i + 4) < MaxTexCoord; i++) {
auto texMtx = static_cast<GXTexMtx>(bp_get(value, 6, i * 6));
assert(texMtx >= 0 && texMtx <= GXTexMtx::GX_IDENTITY);
g_gxState.tcgs[i + 4].mtx = texMtx;
}
// Same matrix indices as XF 0x19, written from a different bank.
g_gxState.invalidateXfReg(0x19);
mark_pipeline_state_dirty();
break;
}
default:
// VAT A registers (0x70-0x77)
if (addr >= 0x70 && addr <= 0x77) {
u32 fmt = addr - 0x70;
auto& vf = g_gxState.vtxFmts[fmt];
vf.attrs[GX_VA_POS].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 0));
vf.attrs[GX_VA_POS].type = static_cast<GXCompType>(bp_get(value, 3, 1));
vf.attrs[GX_VA_POS].frac = static_cast<u8>(bp_get(value, 5, 4));
vf.attrs[GX_VA_NRM].type = static_cast<GXCompType>(bp_get(value, 3, 10));
if (bp_get(value, 1, 31) != 0) {
vf.attrs[GX_VA_NRM].cnt = GX_NRM_NBT3;
} else {
vf.attrs[GX_VA_NRM].cnt = bp_get(value, 1, 9) != 0 ? GX_NRM_NBT : GX_NRM_XYZ;
}
vf.attrs[GX_VA_NRM].frac = normal_frac_bits(vf.attrs[GX_VA_NRM].type);
vf.attrs[GX_VA_CLR0].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 13));
vf.attrs[GX_VA_CLR0].type = static_cast<GXCompType>(bp_get(value, 3, 14));
vf.attrs[GX_VA_CLR0].frac = 0;
vf.attrs[GX_VA_CLR1].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 17));
vf.attrs[GX_VA_CLR1].type = static_cast<GXCompType>(bp_get(value, 3, 18));
vf.attrs[GX_VA_CLR1].frac = 0;
vf.attrs[GX_VA_TEX0].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 21));
vf.attrs[GX_VA_TEX0].type = static_cast<GXCompType>(bp_get(value, 3, 22));
vf.attrs[GX_VA_TEX0].frac = static_cast<u8>(bp_get(value, 5, 25));
mark_pipeline_state_dirty();
g_gxState.clearVtxSizeCache();
}
// VAT B registers (0x80-0x87)
else if (addr >= 0x80 && addr <= 0x87) {
u32 fmt = addr - 0x80;
auto& vf = g_gxState.vtxFmts[fmt];
vf.attrs[GX_VA_TEX1].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 0));
vf.attrs[GX_VA_TEX1].type = static_cast<GXCompType>(bp_get(value, 3, 1));
vf.attrs[GX_VA_TEX1].frac = static_cast<u8>(bp_get(value, 5, 4));
vf.attrs[GX_VA_TEX2].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 9));
vf.attrs[GX_VA_TEX2].type = static_cast<GXCompType>(bp_get(value, 3, 10));
vf.attrs[GX_VA_TEX2].frac = static_cast<u8>(bp_get(value, 5, 13));
vf.attrs[GX_VA_TEX3].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 18));
vf.attrs[GX_VA_TEX3].type = static_cast<GXCompType>(bp_get(value, 3, 19));
vf.attrs[GX_VA_TEX3].frac = static_cast<u8>(bp_get(value, 5, 22));
vf.attrs[GX_VA_TEX4].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 27));
vf.attrs[GX_VA_TEX4].type = static_cast<GXCompType>(bp_get(value, 3, 28));
// TEX4 frac is in VAT C
mark_pipeline_state_dirty();
g_gxState.clearVtxSizeCache();
}
// VAT C registers (0x90-0x97)
else if (addr >= 0x90 && addr <= 0x97) {
u32 fmt = addr - 0x90;
auto& vf = g_gxState.vtxFmts[fmt];
vf.attrs[GX_VA_TEX4].frac = static_cast<u8>(bp_get(value, 5, 0));
vf.attrs[GX_VA_TEX5].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 5));
vf.attrs[GX_VA_TEX5].type = static_cast<GXCompType>(bp_get(value, 3, 6));
vf.attrs[GX_VA_TEX5].frac = static_cast<u8>(bp_get(value, 5, 9));
vf.attrs[GX_VA_TEX6].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 14));
vf.attrs[GX_VA_TEX6].type = static_cast<GXCompType>(bp_get(value, 3, 15));
vf.attrs[GX_VA_TEX6].frac = static_cast<u8>(bp_get(value, 5, 18));
vf.attrs[GX_VA_TEX7].cnt = static_cast<GXCompCnt>(bp_get(value, 1, 23));
vf.attrs[GX_VA_TEX7].type = static_cast<GXCompType>(bp_get(value, 3, 24));
vf.attrs[GX_VA_TEX7].frac = static_cast<u8>(bp_get(value, 5, 27));
mark_pipeline_state_dirty();
g_gxState.clearVtxSizeCache();
}
// Array base addresses (0xA0-0xAF)
else if (addr >= 0xA0 && addr <= 0xAF) {
Log.error("CP_REG_ARRAYBASE_ID is not supported on Aurora. Use GX_LOAD_AURORA_ARRAYBASE instead.");
}
// Array strides (0xB0-0xBF)
else if (addr >= 0xB0 && addr <= 0xBF) {
u32 attrIdx = addr - 0xB0 + GX_VA_POS;
if (attrIdx < GX_VA_MAX_ATTR) {
auto& array = g_gxState.arrays[attrIdx];
const auto newStride = static_cast<u8>(value);
if (array.stride != newStride) {
array.stride = newStride;
mark_pipeline_state_dirty();
}
}
}
break;
}
}
// XF register handler - decodes XF (transform unit) register writes and updates g_gxState
static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian) {
// These bounds must hold in release too: CHECK() is a no-op under NDEBUG, so relying on it alone let a truncated
// guest display list read past `data`.
if (pos > size || size - pos < 4)
UNLIKELY {
CHECK(false, "XF header read overrun");
pos = size;
return;
}
u32 header = read_u32(data + pos, bigEndian);
pos += 4;
u32 count = ((header >> 16) & 0xFFFF) + 1;
u32 addr = header & 0xFFFF;
u32 dataBytes = count * 4;
// Log.warn(" xf: addr {:04x} count {} dataBytes {} pos {} -> {}", addr, count, dataBytes, pos, pos + dataBytes);
if (size - pos < dataBytes)
UNLIKELY {
CHECK(false, "XF data read overrun: need {} bytes at pos {}", dataBytes, pos);
pos = size;
return;
}
const u8* xfData = data + pos;
if (copy_xf_data(addr, xfData, count, bigEndian)) {
// copy_xf_data handled everything.
} else if (addr >= 0x1000) {
// XF registers (0x1000+)
u32 xfAddr = addr - 0x1000;
bool viewportUpdated = false;
bool projectionUpdated = false;
for (u32 i = 0; i < count; i++) {
u32 reg = xfAddr + i;
u32 val = read_u32(xfData + i * 4, bigEndian);
// Skip register writes that decode to state we already hold.
const bool cacheable = reg < g_gxState.xfRegCache.size();
const bool unchanged = cacheable && g_gxState.xfRegMatches(reg, val);
if (cacheable)
g_gxState.storeXfReg(reg, val);
// Viewport (0x1A-0x1F) and projection (0x20-0x26) keep their unconditional apply below; only the banks that
// already had skip semantics and the TexGen bank drop out here.
if (unchanged && (reg <= 0x19 || reg >= 0x3F))
continue;
switch (reg) {
case 0x00:
g_gxState.xfError = val;
break;
case 0x08:
// XF vertex specs (numColors, numNormals, numTexCoords) - informational
break;
case 0x09:
// numChans
g_gxState.numChans = val;
g_gxState.stateDirty = true;
break;
case 0x0A:
// Ambient color 0
g_gxState.colorChannelState[GX_COLOR0].ambColor = unpack_color(val);
g_gxState.colorChannelState[GX_ALPHA0].ambColor = unpack_color(val);
g_gxState.stateDirty = true;
break;
case 0x0B:
// Ambient color 1
g_gxState.colorChannelState[GX_COLOR1].ambColor = unpack_color(val);
g_gxState.colorChannelState[GX_ALPHA1].ambColor = unpack_color(val);
g_gxState.stateDirty = true;
break;
case 0x0C:
// Material color 0
g_gxState.colorChannelState[GX_COLOR0].matColor = unpack_color(val);
g_gxState.colorChannelState[GX_ALPHA0].matColor = unpack_color(val);
g_gxState.stateDirty = true;
break;
case 0x0D:
// Material color 1
g_gxState.colorChannelState[GX_COLOR1].matColor = unpack_color(val);
g_gxState.colorChannelState[GX_ALPHA1].matColor = unpack_color(val);
g_gxState.stateDirty = true;
break;
case 0x0E:
case 0x0F:
case 0x10:
case 0x11: {
// Channel control registers
u32 chanId = reg - 0x0E;
if (chanId < MaxColorChannels) {
auto& chan = g_gxState.colorChannelConfig[chanId];
chan.matSrc = static_cast<GXColorSrc>(bp_get(val, 1, 0));
chan.lightingEnabled = bp_get(val, 1, 1) != 0;
u32 lightsLo = bp_get(val, 4, 2);
chan.ambSrc = static_cast<GXColorSrc>(bp_get(val, 1, 6));
chan.diffFn = static_cast<GXDiffuseFn>(bp_get(val, 2, 7));
u32 lightsHi = bp_get(val, 4, 11);
switch (bp_get(val, 2, 9)) {
case 1:
chan.attnFn = GX_AF_SPEC;
break;
case 3:
chan.attnFn = GX_AF_SPOT;
break;
case 0:
case 2:
default:
chan.attnFn = GX_AF_NONE;
break;
}
u32 lightMask = lightsLo | (lightsHi << 4);
g_gxState.colorChannelState[chanId].lightMask = GX::LightMask{lightMask};
mark_pipeline_state_dirty();
}
break;
}
case 0x12:
g_gxState.dualTex = val;
mark_pipeline_state_dirty();
break;
case 0x18: {
// Matrix index A: PnMtx + TexCoord0-3 matrix indices
g_gxState.currentPnMtx = bp_get(val, 6, 0) / 3;
for (u32 i = 0; i < 4 && i < MaxTexCoord; i++) {
auto texMtx = static_cast<GXTexMtx>(bp_get(val, 6, 6 + i * 6));
assert(texMtx >= 0 && texMtx <= GXTexMtx::GX_IDENTITY);
g_gxState.tcgs[i].mtx = texMtx;
}
mark_pipeline_state_dirty();
break;
}
case 0x19: {
// Matrix index B: TexCoord4-7 matrix indices
for (u32 i = 0; i < 4 && (i + 4) < MaxTexCoord; i++) {
g_gxState.tcgs[i + 4].mtx = static_cast<GXTexMtx>(bp_get(val, 6, i * 6));
}
mark_pipeline_state_dirty();
break;
}
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
case 0x1F: {
// Viewport: sx, sy, sz, ox, oy, oz at XF 0x101A-0x101F
const u32 vpOff = reg - 0x1A;
g_gxState.xfViewport[vpOff] = read_f32(xfData + i * 4, bigEndian);
viewportUpdated = true;
break;
}
case 0x20:
case 0x21:
case 0x22:
case 0x23:
case 0x24:
case 0x25:
case 0x26: {
// Projection: 6 params + type at XF 0x1020-0x1026
const u32 projOff = reg - 0x20;
if (projOff < g_gxState.xfProjection.size()) {
g_gxState.xfProjection[projOff] = read_f32(xfData + i * 4, bigEndian);
} else {
g_gxState.projType = static_cast<GXProjectionType>(val);
}
projectionUpdated = true;
break;
}
case 0x3F:
// numTexGens
g_gxState.numTexGens = val;
mark_pipeline_state_dirty();
break;
default:
// TexGen config (0x40-0x4F) and post-transform (0x50-0x5F)
if (reg >= 0x40 && reg <= 0x4F) {
u32 tcIdx = reg - 0x40;
if (tcIdx < MaxTexCoord) {
auto& tcg = g_gxState.tcgs[tcIdx];
bool proj = bp_get(val, 1, 1) != 0;
u32 form = bp_get(val, 1, 2);
u32 tgType = bp_get(val, 3, 4);
u32 srcRow = bp_get(val, 5, 7);
tcg.inputFormAB11 = form == 0;
if (tgType == 0) {
tcg.type = proj ? GX_TG_MTX3x4 : GX_TG_MTX2x4;
} else if (tgType == 1) {
// Bump mapping
tcg.type = static_cast<GXTexGenType>(bp_get(val, 3, 15) + 2);
} else if (tgType == 2 || tgType == 3) {
tcg.type = GX_TG_SRTG;
}
// Decode source from row
static const GXTexGenSrc rowToSrc[] = {GX_TG_POS, GX_TG_NRM, GX_TG_COLOR0, GX_TG_BINRM, GX_TG_TANGENT,
GX_TG_TEX0, GX_TG_TEX1, GX_TG_TEX2, GX_TG_TEX3, GX_TG_TEX4,
GX_TG_TEX5, GX_TG_TEX6, GX_TG_TEX7};
if (srcRow < 13) {
tcg.src = rowToSrc[srcRow];
}
mark_pipeline_state_dirty();
}
} else if (reg >= 0x50 && reg <= 0x5F) {
u32 tcIdx = reg - 0x50;
if (tcIdx < MaxTexCoord) {
g_gxState.tcgs[tcIdx].postMtx = static_cast<GXPTTexMtx>(bp_get(val, 6, 0) + 64);
g_gxState.tcgs[tcIdx].normalize = bp_get(val, 1, 8) != 0;
mark_pipeline_state_dirty();
}
} else {
#ifndef NDEBUG
Log.debug("Unhandled XF register 0x{:04X} (value 0x{:08X})", reg, val);
#endif
}
break;
}
}
if (viewportUpdated) {
apply_xf_viewport();
}
if (projectionUpdated) {
apply_xf_projection();
}
}
pos += dataBytes;
}
static void handle_draw_overrun(u8 cmd, u16 vtxCount, u32 vtxSize, u32 totalVtxBytes, const u8* data, const u32& pos,
u32 size) {
static u32 truncatedDrawLogCount = 0;
if (truncatedDrawLogCount >= 64) {
if (truncatedDrawLogCount == 64) {
Log.warn("suppressing further truncated draw diagnostics");
}
++truncatedDrawLogCount;
return;
}
++truncatedDrawLogCount;
// Hex dump around the draw command for debugging
u32 cmdPos = pos - 2 - 1; // opcode byte position (before vtxCount and pos++)
u32 dumpStart = (cmdPos > 16) ? cmdPos - 16 : 0;
u32 dumpEnd = (cmdPos + 32 < size) ? cmdPos + 32 : size;
std::string hex;
for (u32 i = dumpStart; i < dumpEnd; i++) {
if (i == cmdPos)
hex += fmt::format("[{:02x}]", data[i]);
else
hex += fmt::format(" {:02x}", data[i]);
}
Log.warn(" hex dump around truncated draw cmd (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex);
const auto fmt = static_cast<GXVtxFmt>(cmd & CP_VAT_MASK);
const auto& vtxFmt = g_gxState.vtxFmts[fmt];
Log.warn(
" truncated draw cmd=0x{:02X} fmt={} vtxCount={} vtxSize={} desc pn={} pos={} nrm={} clr0={} clr1={} "
"tex0={} tex1={} tex2={} tex3={} tex4={} tex5={} tex6={} tex7={}",
cmd, static_cast<u32>(fmt), vtxCount, vtxSize, g_gxState.vtxDesc[GX_VA_PNMTXIDX], g_gxState.vtxDesc[GX_VA_POS],
g_gxState.vtxDesc[GX_VA_NRM], g_gxState.vtxDesc[GX_VA_CLR0], g_gxState.vtxDesc[GX_VA_CLR1],
g_gxState.vtxDesc[GX_VA_TEX0], g_gxState.vtxDesc[GX_VA_TEX1], g_gxState.vtxDesc[GX_VA_TEX2],
g_gxState.vtxDesc[GX_VA_TEX3], g_gxState.vtxDesc[GX_VA_TEX4], g_gxState.vtxDesc[GX_VA_TEX5],
g_gxState.vtxDesc[GX_VA_TEX6], g_gxState.vtxDesc[GX_VA_TEX7]);
Log.warn(" fmt {} attrs pos({},{}) nrm({},{}) clr0({},{}) tex0({},{}) tex1({},{})", static_cast<u32>(fmt),
static_cast<u32>(vtxFmt.attrs[GX_VA_POS].cnt), static_cast<u32>(vtxFmt.attrs[GX_VA_POS].type),
static_cast<u32>(vtxFmt.attrs[GX_VA_NRM].cnt), static_cast<u32>(vtxFmt.attrs[GX_VA_NRM].type),
static_cast<u32>(vtxFmt.attrs[GX_VA_CLR0].cnt), static_cast<u32>(vtxFmt.attrs[GX_VA_CLR0].type),
static_cast<u32>(vtxFmt.attrs[GX_VA_TEX0].cnt), static_cast<u32>(vtxFmt.attrs[GX_VA_TEX0].type),
static_cast<u32>(vtxFmt.attrs[GX_VA_TEX1].cnt), static_cast<u32>(vtxFmt.attrs[GX_VA_TEX1].type));
Log.warn("stopping FIFO decode at truncated draw: need {} bytes at pos {}, have {}", totalVtxBytes, pos, size);
}
// Uploads a draw's GX vertices with the stride populate_pipeline_config gave the shader. When that stride is padded
// (Android, see padded_upload_stride), each vertex is copied with zeroed trailing bytes; attribute offsets inside the
// vertex are unchanged. Every padded upload is a multiple of 4 bytes, so consecutive draws stay contiguous for merging.
static gfx::Range push_draw_vertices(const u8* vertices, u32 vtxCount, u32 vtxSize) {
const u32 uploadStride = padded_upload_stride(vtxSize);
if (uploadStride == vtxSize)
LIKELY { return gfx::push_verts(vertices, static_cast<size_t>(vtxCount) * vtxSize); }
auto [buffer, range] = gfx::map_verts(static_cast<size_t>(vtxCount) * uploadStride);
u8* dst = buffer.data();
const u32 padding = uploadStride - vtxSize;
for (u32 i = 0; i < vtxCount; ++i) {
std::memcpy(dst, vertices + static_cast<size_t>(i) * vtxSize, vtxSize);
std::memset(dst + vtxSize, 0, padding);
dst += uploadStride;
}
return range;
}
// Uploads an indexed vertex array with elements `uploadStride` bytes apart (see padded_upload_stride). A padded upload
// copies each element and zeroes its trailing bytes; a trailing partial element is copied as far as the array goes.
static gfx::Range push_vertex_array(const AttrArray& array, u32 uploadStride) {
const auto* data = static_cast<const u8*>(array.data);
if (uploadStride == array.stride || array.stride == 0)
LIKELY { return gfx::push_storage(data, array.size); }
const size_t count = (static_cast<size_t>(array.size) + array.stride - 1) / array.stride;
auto [buffer, range] = gfx::map_storage(count * uploadStride);
u8* dst = buffer.data();
std::memset(dst, 0, count * uploadStride);
for (size_t i = 0; i < count; ++i) {
const size_t start = i * array.stride;
std::memcpy(dst + i * uploadStride, data + start, std::min<size_t>(array.stride, array.size - start));
}
return range;
}
// Draw command handler - parses vertices inline and caches results
static u32 calculate_last_vtx_size(GXVtxFmt fmt) {
u32 vtxSize = 0;
const auto& vtxFmt = g_gxState.vtxFmts[fmt];
for (int i = GX_VA_PNMTXIDX; i <= GX_VA_TEX7; ++i) {
const auto attr = static_cast<GXAttr>(i);
const auto& attrFmt = vtxFmt.attrs[i];
switch (g_gxState.vtxDesc[i]) {
case GX_NONE:
break;
case GX_DIRECT: {
vtxSize += comp_type_size(attr, attrFmt.type) * comp_cnt_count(attr, attrFmt.cnt);
break;
}
case GX_INDEX8:
vtxSize += (attr == GX_VA_NRM && attrFmt.cnt == GX_NRM_NBT3) ? 3 : 1;
break;
case GX_INDEX16:
vtxSize += (attr == GX_VA_NRM && attrFmt.cnt == GX_NRM_NBT3) ? 6 : 2;
break;
}
}
g_gxState.lastVtxFmt = fmt;
g_gxState.lastVtxSize = vtxSize;
return vtxSize;
}
static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange,
uint16_t usedPnMtxMask, HashType matrixTopologySignature, HashType geometrySignature,
bool interpolationIdentityActive, const uint8_t* vertices, uint32_t vtxStride);
// The per-draw geometry signature, matrix-usage mask and draw-identity hashes exist purely to feed frame interpolation
// (build_uniform consumes them only after its `frame_interpolation_fps() == 0` early-out).
static inline bool frame_interpolation_identity_needed() noexcept {
return frame_interpolation_active() && g_gxState.projType == GX_PERSPECTIVE;
}
static uint32_t matrix_index_prefix_size(GXVtxFmt fmt) noexcept {
const auto& vtxFmt = g_gxState.vtxFmts[fmt];
uint32_t size = 0;
for (int i = GX_VA_PNMTXIDX; i < GX_VA_POS; ++i) {
const auto attr = static_cast<GXAttr>(i);
switch (g_gxState.vtxDesc[i]) {
case GX_NONE:
break;
case GX_DIRECT:
size += comp_type_size(attr, vtxFmt.attrs[i].type) * comp_cnt_count(attr, vtxFmt.attrs[i].cnt);
break;
case GX_INDEX8:
++size;
break;
case GX_INDEX16:
size += 2;
break;
}
}
return size;
}
// Screen-space bounds of a simple orthographic rectangle or line, textured or
// not: MKW's split-screen partition is a layout picture pane (a one-pixel quad
// sampling a pattern texture), so texture use cannot disqualify a candidate.
// The geometry rules in is_split_screen_furniture keep HUD art visible.
static std::optional<gfx::stereo_replay::SubviewRect> screen_rect(GXPrimitive prim, GXVtxFmt fmt,
const uint8_t* vertices, uint16_t count,
uint32_t stride) noexcept {
if (!aurora::stereo_frame_provider_active() || g_gxState.projType != GX_ORTHOGRAPHIC ||
!((count == 4 && (prim == GX_QUADS || prim == GX_TRIANGLESTRIP || prim == GX_TRIANGLEFAN)) ||
(count == 2 && prim == GX_LINES)))
return {};
const auto& projection = g_gxState.proj;
if (!gfx::stereo_replay::is_orthographic_projection(projection))
return {};
const auto& attr = g_gxState.vtxFmts[fmt].attrs[GX_VA_POS];
const uint32_t components = attr.cnt == GX_POS_XY ? 2 : 3;
const uint32_t componentBytes = comp_type_size(GX_VA_POS, attr.type);
if (componentBytes == 0)
return {};
const uint32_t offset = matrix_index_prefix_size(fmt);
std::array<std::array<float, 2>, 4> points{};
float left = INFINITY, top = INFINITY, right = -INFINITY, bottom = -INFINITY;
for (uint32_t i = 0; i < count; ++i) {
const auto* vertex = vertices + i * stride;
const auto* position = vertex + offset;
bool bigEndian = true;
const auto type = g_gxState.vtxDesc[GX_VA_POS];
if (type == GX_INDEX8 || type == GX_INDEX16) {
const uint32_t index = type == GX_INDEX8 ? *position : read_u16(position, true);
const auto& array = g_gxState.arrays[GX_VA_POS];
const size_t start = size_t(index) * array.stride;
if (array.data == nullptr || start + components * componentBytes > array.size)
return {};
position = static_cast<const uint8_t*>(array.data) + start;
bigEndian = !array.le;
} else if (type != GX_DIRECT || offset + components * componentBytes > stride) {
return {};
}
std::array<float, 3> point{};
for (uint32_t c = 0; c < components; ++c) {
const auto* value = position + c * componentBytes;
switch (attr.type) {
case GX_U8:
point[c] = *value;
break;
case GX_S8:
point[c] = static_cast<int8_t>(*value);
break;
case GX_U16:
point[c] = read_u16(value, bigEndian);
break;
case GX_S16:
point[c] = static_cast<int16_t>(read_u16(value, bigEndian));
break;
case GX_F32:
point[c] = read_f32(value, bigEndian);
break;
default:
return {};
}
if (attr.type != GX_F32)
point[c] = std::ldexp(point[c], -int(attr.frac));
}
const uint32_t matrixIndex =
g_gxState.vtxDesc[GX_VA_PNMTXIDX] == GX_DIRECT ? vertex[0] / 3u : g_gxState.currentPnMtx;
if (matrixIndex >= g_gxState.pnMtx.size())
return {};
const auto& matrix = g_gxState.pnMtx[matrixIndex].pos;
const auto transform = [](const Vec4<float>& row, const std::array<float, 3>& p) {
return row[0] * p[0] + row[1] * p[1] + row[2] * p[2] + row[3];
};
const std::array<float, 3> view{transform(matrix.m0, point), transform(matrix.m1, point),
transform(matrix.m2, point)};
const auto& vp = g_gxState.renderViewport;
const float x = vp.left + (transform(projection.m0, view) + 1.f) * vp.width * 0.5f;
const float y = vp.top + (1.f - transform(projection.m1, view)) * vp.height * 0.5f;
if (!std::isfinite(x) || !std::isfinite(y))
return {};
points[i] = {x, y};
left = std::min(left, x);
right = std::max(right, x);
top = std::min(top, y);
bottom = std::max(bottom, y);
}
// A diagonal/rotated HUD polygon's bounding box is not a screen mask.
uint32_t corners = 0;
for (uint32_t i = 0; i < count; ++i) {
const auto& p = points[i];
if ((std::abs(p[0] - left) > 0.01f && std::abs(p[0] - right) > 0.01f) ||
(std::abs(p[1] - top) > 0.01f && std::abs(p[1] - bottom) > 0.01f))
return {};
corners |= 1u << ((std::abs(p[0] - right) < 0.01f ? 1 : 0) + (std::abs(p[1] - bottom) < 0.01f ? 2 : 0));
}
if ((count == 4 && corners != 15) || (count == 2 && right - left > 0.01f && bottom - top > 0.01f))
return {};
return gfx::stereo_replay::SubviewRect{left, top, right - left, bottom - top};
}
static HashType draw_geometry_signature(GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount,
uint32_t vtxStride) noexcept {
Hasher hasher;
hasher.update(vtxCount);
hasher.update(vtxStride);
// Matrix-index bytes select an instance's current XF palette slots, so they are deliberately excluded from mesh
// identity.
const uint32_t matrixPrefix = std::min(matrix_index_prefix_size(fmt), vtxStride);
if (matrixPrefix == 0) {
// Most draws have no direct matrix-index prefix.
hasher.update(vertices, static_cast<size_t>(vtxCount) * vtxStride);
} else {
for (uint16_t vertex = 0; vertex < vtxCount; ++vertex) {
hasher.update(vertices + static_cast<size_t>(vertex) * vtxStride + matrixPrefix, vtxStride - matrixPrefix);
}
}
// Identical index streams can address different vertex arrays.
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
if (g_gxState.vtxDesc[i] != GX_INDEX8 && g_gxState.vtxDesc[i] != GX_INDEX16) {
continue;
}
const auto& array = g_gxState.arrays[i];
const auto attribute = static_cast<uint32_t>(i);
const auto source = reinterpret_cast<uintptr_t>(array.data);
hasher.update(attribute);
hasher.update(source);
hasher.update(array.stride);
}
return static_cast<HashType>(hasher.digest());
}
struct PnMtxUsage {
uint16_t mask = 0;
HashType topologySignature = 0;
};
// Slot mask only.
static uint16_t pn_mtx_mask(const uint8_t* vertices, uint16_t vtxCount, uint32_t vtxStride) noexcept {
if (g_gxState.vtxDesc[GX_VA_PNMTXIDX] != GX_DIRECT) {
return static_cast<uint16_t>(1u << std::min<uint32_t>(g_gxState.currentPnMtx, MaxPnMtx - 1));
}
uint16_t mask = 0;
for (uint16_t vertex = 0; vertex < vtxCount; ++vertex) {
const uint32_t matrixIndex = vertices[static_cast<size_t>(vertex) * vtxStride] / 3u;
if (matrixIndex < MaxPnMtx) {
mask |= static_cast<uint16_t>(1u << matrixIndex);
}
}
return mask;
}
static PnMtxUsage pn_mtx_usage(const uint8_t* vertices, uint16_t vtxCount, uint32_t vtxStride) noexcept {
if (g_gxState.vtxDesc[GX_VA_PNMTXIDX] != GX_DIRECT) {
const uint32_t matrixIndex = std::min<uint32_t>(g_gxState.currentPnMtx, MaxPnMtx - 1);
return {
.mask = static_cast<uint16_t>(1u << matrixIndex),
// There is no vertex matrix-index topology in this path.
.topologySignature = 0,
};
}
Hasher topologyHasher;
topologyHasher.update(vtxCount);
uint16_t mask = 0;
for (uint16_t vertex = 0; vertex < vtxCount; ++vertex) {
// GX matrix-index attributes precede every other vertex attribute and are always one byte.
const uint8_t rawMatrixIndex = vertices[static_cast<size_t>(vertex) * vtxStride];
topologyHasher.update(rawMatrixIndex);
const uint32_t matrixIndex = rawMatrixIndex / 3u;
if (matrixIndex < MaxPnMtx) {
mask |= static_cast<uint16_t>(1u << matrixIndex);
}
}
return {
.mask = mask,
.topologySignature = static_cast<HashType>(topologyHasher.digest()),
};
}
// Whether the most recent unmerged draw recorded an interpolation snapshot, so a draw merged into it knows there is a
// snapshot to extend.
static bool s_lastDrawRecordedInterpolation = false;
struct CachedIndexTemplate {
bool valid = false;
GXPrimitive prim = static_cast<GXPrimitive>(0);
u16 vtxCount = 0;
u32 indexCount = 0;
IndexBuffer indices{};
};
static const CachedIndexTemplate& cached_index_template(GXPrimitive prim, u16 vtxCount) {
// Topology expansion is immutable for a primitive/count pair.
constexpr size_t CacheSize = 256;
static std::array<CachedIndexTemplate, CacheSize> cache{};
const u32 key = (static_cast<u32>(underlying(prim)) << 16) | vtxCount;
auto& entry = cache[(key ^ (key >> 9)) & (CacheSize - 1)];
if (entry.valid && entry.prim == prim && entry.vtxCount == vtxCount)
LIKELY { return entry; }
entry.valid = true;
entry.prim = prim;
entry.vtxCount = vtxCount;
entry.indexCount = prepare_idx_template(entry.indices, prim, vtxCount);
return entry;
}
static IndexBuffer handle_draw_idx_buf;
static ArrayRef<u16> offset_index_template(const CachedIndexTemplate& indexTemplate, u16 vtxStart) {
// Grow-only: resizing down and back up made every merge zero-fill the buffer before the transform immediately
// overwrote it.
const size_t count = indexTemplate.indices.size();
if (handle_draw_idx_buf.size() < count) {
handle_draw_idx_buf.resize(count);
}
const u16* src = indexTemplate.indices.data();
u16* dst = handle_draw_idx_buf.data();
for (size_t i = 0; i < count; ++i) {
dst[i] = static_cast<u16>(src[i] + vtxStart);
}
return {dst, count};
}
struct CachedPipelineState {
gfx::PipelineRef ref = 0;
HashType configHash = 0;
// Carried here so the draw can be recorded without keeping the PipelineConfig that produced it alive; it is the only
// field of the config the draw itself still needs.
u32 dstAlpha = UINT32_MAX;
ShaderInfo shaderInfo{};
};
static const CachedPipelineState& cached_pipeline_state(const PipelineConfig& config) {
constexpr size_t CacheSize = 1024;
struct Entry {
bool valid = false;
PipelineConfig config{};
CachedPipelineState state{};
};
static std::array<Entry, CacheSize> cache{};
const HashType hash = xxh3_hash(config, static_cast<HashType>(gfx::ShaderType::GX));
auto& entry = cache[hash & (CacheSize - 1)];
if (entry.valid && entry.state.configHash == hash && std::memcmp(&entry.config, &config, sizeof(config)) == 0)
LIKELY { return entry.state; }
entry.valid = true;
entry.config = config;
entry.state = {
.ref = gfx::pipeline_ref(config),
.configHash = hash,
.dstAlpha = config.dstAlpha,
.shaderInfo = build_shader_info(config.shaderConfig),
};
return entry.state;
}
// Resolving a pipeline the long way costs a ~2.7KB zero-init, a full populate_pipeline_config, an XXH3 over the whole
// config and a memcmp against the hash-indexed slot -- roughly 11KB of memory traffic for a result that is almost
// always identical to the previous draw's.
static const CachedPipelineState& resolve_pipeline_state(GXPrimitive prim, GXVtxFmt fmt) {
struct Memo {
const CachedPipelineState* state = nullptr;
u32 generation = 0;
u32 sampleCount = 0;
GXPrimitive prim = static_cast<GXPrimitive>(0);
GXVtxFmt fmt = static_cast<GXVtxFmt>(0);
};
static Memo memo{};
const u32 sampleCount = gfx::get_sample_count();
const u32 generation = g_gxState.pipelineStateGeneration;
if (memo.state != nullptr && memo.generation == generation && memo.sampleCount == sampleCount && memo.prim == prim &&
memo.fmt == fmt)
LIKELY { return *memo.state; }
PipelineConfig config{};
populate_pipeline_config(config, prim, fmt);
// cached_pipeline_state hands back a reference into a fixed direct-mapped table, so the address stays valid; the
// entry it points at can only be rewritten by another call to that function, and every such call goes through this
// miss path and replaces the memo in the same breath.
const CachedPipelineState& state = cached_pipeline_state(config);
memo = Memo{
.state = &state,
.generation = generation,
.sampleCount = sampleCount,
.prim = prim,
.fmt = fmt,
};
return state;
}
// Exact Screen Depth changes shader outputs but no GX pipeline state. Resolve a
// sibling pipeline only for orthographic draws that can actually reach the VR
// screen, and memoize it with the same state epoch as the ordinary pipeline.
static gfx::PipelineRef resolve_exact_screen_depth_pipeline(GXPrimitive prim, GXVtxFmt fmt) {
struct Memo {
gfx::PipelineRef ref = 0;
u32 generation = 0;
u32 sampleCount = 0;
GXPrimitive prim = static_cast<GXPrimitive>(0);
GXVtxFmt fmt = static_cast<GXVtxFmt>(0);
};
static Memo memo{};
const u32 sampleCount = gfx::get_sample_count();
const u32 generation = g_gxState.pipelineStateGeneration;
if (memo.ref != 0 && memo.generation == generation && memo.sampleCount == sampleCount && memo.prim == prim &&
memo.fmt == fmt)
LIKELY { return memo.ref; }
PipelineConfig config{};
populate_pipeline_config(config, prim, fmt);
config.shaderConfig.exactScreenDepth = 1;
const gfx::PipelineRef ref = gfx::pipeline_ref(config);
memo = Memo{
.ref = ref,
.generation = generation,
.sampleCount = sampleCount,
.prim = prim,
.fmt = fmt,
};
return ref;
}
bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount, uint32_t vertexBytes) {
ZoneScoped;
if (vertices == nullptr || vtxCount == 0 || vertexBytes == 0) {
return false;
}
if (__gx->dirtyState != 0)
UNLIKELY { __GXSetDirtyState(); }
// Raw bridge draws consume live decoded GX state that is also maintained by the HLE producer.
drain();
u32 vtxSize;
if (g_gxState.lastVtxFmt == fmt)
LIKELY { vtxSize = g_gxState.lastVtxSize; }
else
UNLIKELY { vtxSize = calculate_last_vtx_size(fmt); }
const u32 expectedVertexBytes = static_cast<u32>(vtxCount) * vtxSize;
if (expectedVertexBytes == 0 || expectedVertexBytes != vertexBytes) {
static uint32_t rawVertexSizeMismatchCount = 0;
if (rawVertexSizeMismatchCount++ < 64) {
Log.warn("raw draw vertex-size mismatch prim={} fmt={} count={} cached_stride={} expected={} supplied={}",
static_cast<uint32_t>(prim), static_cast<uint32_t>(fmt), vtxCount, vtxSize, expectedVertexBytes,
vertexBytes);
}
return false;
}
// This entry point bypasses process(), so it owns the renderer lock itself.
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
const gfx::Range vertRange = push_draw_vertices(vertices, vtxCount, vtxSize);
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
const PnMtxUsage matrixUsage = interpolationIdentityActive ? pn_mtx_usage(vertices, vtxCount, vtxSize) : PnMtxUsage{};
handle_draw_unmerged(prim, fmt, vtxCount, vertRange, matrixUsage.mask, matrixUsage.topologySignature,
interpolationIdentityActive ? draw_geometry_signature(fmt, vertices, vtxCount, vtxSize) : 0,
interpolationIdentityActive, vertices, vtxSize);
return true;
}
static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian) {
ZoneScoped;
GXVtxFmt fmt = static_cast<GXVtxFmt>(cmd & CP_VAT_MASK);
GXPrimitive prim = primitive_from_draw_cmd(cmd);
if (pos + 2 > size) {
return false;
}
u16 vtxCount = read_u16(data + pos, bigEndian);
pos += 2;
u32 vtxSize;
if (g_gxState.lastVtxFmt == fmt)
LIKELY { vtxSize = g_gxState.lastVtxSize; }
else
UNLIKELY { vtxSize = calculate_last_vtx_size(fmt); }
u32 totalVtxBytes = vtxCount * vtxSize;
if (pos + totalVtxBytes > size)
UNLIKELY {
handle_draw_overrun(cmd, vtxCount, vtxSize, totalVtxBytes, data, pos, size);
return false;
}
// Push raw vertex data to buffer
const uint8_t* vertices = data + pos;
gfx::Range vertRange = push_draw_vertices(vertices, vtxCount, vtxSize);
pos += totalVtxBytes;
// Try to merge with previous draw call
if (!g_gxState.stateDirty && !(aurora::stereo_frame_provider_active() && g_gxState.projType == GX_ORTHOGRAPHIC))
LIKELY {
auto* lastDraw = gfx::get_last_draw_command<DrawData>();
// Only if the previous draw call was a single instance draw (no lines/points handling)
if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS &&
lastDraw->instanceCount == 1)
LIKELY {
const auto& indexTemplate = cached_index_template(prim, vtxCount);
const auto indices = offset_index_template(indexTemplate, lastDraw->vtxCount);
const u32 numIndices = indexTemplate.indexCount;
const gfx::Range idxRange = gfx::push_indices(indices);
CHECK(lastDraw->vertRange.offset + lastDraw->vertRange.size == vertRange.offset,
"Non-consecutive vertex ranges ({} < {})", lastDraw->vertRange.offset + lastDraw->vertRange.size,
vertRange.offset);
CHECK(lastDraw->idxRange.offset + lastDraw->idxRange.size == idxRange.offset,
"Non-consecutive index ranges ({} < {})", lastDraw->idxRange.offset + lastDraw->idxRange.size,
idxRange.offset);
lastDraw->vertRange.size += vertRange.size;
lastDraw->idxRange.size += idxRange.size;
lastDraw->vtxCount += vtxCount;
lastDraw->indexCount += numIndices;
++gfx::g_mergedDrawCallCount;
// This primitive now renders through the draw we merged into, so its palette slots belong to that draw's
// interpolation snapshot as well.
if (s_lastDrawRecordedInterpolation)
UNLIKELY { extend_interpolation_draw(pn_mtx_mask(vertices, vtxCount, vtxSize)); }
return true;
}
}
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
const PnMtxUsage matrixUsage = interpolationIdentityActive ? pn_mtx_usage(vertices, vtxCount, vtxSize) : PnMtxUsage{};
handle_draw_unmerged(prim, fmt, vtxCount, vertRange, matrixUsage.mask, matrixUsage.topologySignature,
interpolationIdentityActive ? draw_geometry_signature(fmt, vertices, vtxCount, vtxSize) : 0,
interpolationIdentityActive, vertices, vtxSize);
return true;
}
static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange,
uint16_t usedPnMtxMask, HashType matrixTopologySignature, HashType geometrySignature,
bool interpolationIdentityActive, const uint8_t* vertices, uint32_t vtxStride) {
ZoneScoped;
// GX_CULL_ALL rasterizes nothing on hardware - no color, no depth.
if (g_gxState.cullMode == GX_CULL_ALL && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS)
UNLIKELY {
// Leave stateDirty alone: the next draw re-resolving its pipeline is the safe direction, and nothing about this
// draw reached the GPU.
return;
}
// Callers hold the renderer GPU mutex for the whole drain (process() and submit_raw_draw); taking it again per draw
// only cost a recursive re-entry.
const auto& indexTemplate = cached_index_template(prim, vtxCount);
const u32 numIndices = indexTemplate.indexCount;
const gfx::Range idxRange =
gfx::push_indices(ArrayRef<u16>{indexTemplate.indices.data(), indexTemplate.indices.size()});
// Build pipeline, bind groups, and push draw command
BindGroupRanges ranges{};
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
if (g_gxState.vtxDesc[i] != GX_INDEX8 && g_gxState.vtxDesc[i] != GX_INDEX16) {
continue;
}
auto& array = g_gxState.arrays[i];
const u32 uploadStride = padded_upload_stride(array.stride);
if (array.cachedRange.size > 0 && array.cachedStride == uploadStride) {
ranges.vaRanges[i - GX_VA_POS] = array.cachedRange;
} else {
const auto range = push_vertex_array(array, uploadStride);
ranges.vaRanges[i - GX_VA_POS] = range;
array.cachedRange = range;
array.cachedStride = uploadStride;
}
}
const auto& pipelineState = resolve_pipeline_state(prim, fmt);
const auto& info = pipelineState.shaderInfo;
resolve_sampled_textures(info);
const auto bindGroups = build_bind_groups(info);
const auto pipeline = pipelineState.ref;
// Draw-identity hashing only feeds frame interpolation, and only for perspective draws: build_uniform reads the
// identity exclusively past its `!perspective || frame_interpolation_fps() == 0` early-out.
FrameInterpolationDrawIdentity drawIdentity{};
if (interpolationIdentityActive)
UNLIKELY {
const HashType drawShape = static_cast<HashType>(vtxCount) | (static_cast<HashType>(underlying(prim)) << 16) |
(static_cast<HashType>(underlying(fmt)) << 24);
const HashType pipelineDrawSignature = xxh3_hash(pipelineState.configHash, drawShape);
const HashType textureSignature = xxh3_hash(bindGroups.textureBindGroup);
const HashType materialAndTopology =
xxh3_hash(matrixTopologySignature, xxh3_hash(bindGroups.textureBindGroup, pipelineDrawSignature));
drawIdentity = FrameInterpolationDrawIdentity{
.combined = xxh3_hash(geometrySignature, materialAndTopology),
.pipeline = pipelineDrawSignature,
.texture = textureSignature,
.matrixTopology = matrixTopologySignature,
};
}
const bool perspective = g_gxState.projType == GX_PERSPECTIVE;
const auto uniformRanges = build_uniform(info, vertRange.offset, ranges, drawIdentity, perspective, usedPnMtxMask);
const auto& replayLayout = uniformRanges.replayLayout;
const gfx::PipelineRef exactScreenDepthPipeline =
aurora::stereo_frame_provider_active() && !replayLayout.perspective && !replayLayout.nativeEfbEffect
? resolve_exact_screen_depth_pipeline(prim, fmt)
: 0;
s_lastDrawRecordedInterpolation = interpolationIdentityActive;
uint32_t instanceCount = 1;
if (prim == GX_LINES) {
instanceCount = vtxCount / 2;
} else if (prim == GX_LINESTRIP) {
instanceCount = vtxCount - 1;
} else if (prim == GX_POINTS) {
instanceCount = vtxCount;
}
gfx::push_draw_command(DrawData{
.pipeline = pipeline,
.exactScreenDepthPipeline = exactScreenDepthPipeline,
.vertRange = vertRange,
.idxRange = idxRange,
.uniformRange = uniformRanges.current,
.interpolatedUniformRanges = uniformRanges.interpolated,
.stereoUniformRanges = {},
.previousUniformRange = uniformRanges.previous,
.uniformReplayLayout = uniformRanges.replayLayout,
.vtxCount = vtxCount,
.indexCount = numIndices,
.instanceCount = instanceCount,
.bindGroups = bindGroups,
.dstAlpha = pipelineState.dstAlpha,
.screenRect = screen_rect(prim, fmt, vertices, vtxCount, vtxStride),
});
g_gxState.stateDirty = false;
}
std::string read_string(const u8* data, u32& pos, u32 size, bool bigEndian) {
CHECK(pos + 2 <= size, "Aurora string length read overrun");
const u16 length = read_u16(data + pos, bigEndian);
pos += 2;
CHECK(pos + length <= size, "Aurora string read overrun");
std::string str(reinterpret_cast<const char*>(data) + pos, length);
pos += length;
return str;
}
bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) {
ZoneScoped;
if (pos + 2 > size) {
return false;
}
u16 subCmd = read_u16(data + pos, bigEndian);
pos += 2;
// Setting of vertex array bases.
if (subCmd == GX_LOAD_AURORA_VIEWPORT_RENDER) {
CHECK(pos + 24 <= size, "GX_LOAD_AURORA_VIEWPORT_RENDER read overrun");
const f32 left = read_f32(data + pos, bigEndian);
pos += 4;
const f32 top = read_f32(data + pos, bigEndian);
pos += 4;
const f32 width = read_f32(data + pos, bigEndian);
pos += 4;
const f32 height = read_f32(data + pos, bigEndian);
pos += 4;
const f32 nearZ = read_f32(data + pos, bigEndian);
pos += 4;
const f32 farZ = read_f32(data + pos, bigEndian);
pos += 4;
set_render_viewport({
.left = left,
.top = top,
.width = width,
.height = height,
.znear = nearZ,
.zfar = farZ,
});
} else if (subCmd == GX_LOAD_AURORA_SCISSOR_RENDER) {
CHECK(pos + 16 <= size, "GX_LOAD_AURORA_SCISSOR_RENDER read overrun");
const int32_t left = static_cast<int32_t>(read_u32(data + pos, bigEndian));
pos += 4;
const int32_t top = static_cast<int32_t>(read_u32(data + pos, bigEndian));
pos += 4;
const int32_t width = static_cast<int32_t>(read_u32(data + pos, bigEndian));
pos += 4;
const int32_t height = static_cast<int32_t>(read_u32(data + pos, bigEndian));
pos += 4;
set_render_scissor({left, top, width, height});
} else if (subCmd >= GX_LOAD_AURORA_ARRAYBASE && subCmd <= (GX_LOAD_AURORA_ARRAYBASE | 0x0f)) {
CHECK(pos + 13 <= size, "GX_LOAD_AURORA_ARRAYBASE read overrun");
u32 attrIdx = subCmd - GX_LOAD_AURORA_ARRAYBASE + GX_VA_POS;
u64 arrayAddr = read_u64(data + pos, bigEndian);
pos += 8;
u32 arraySize = read_u32(data + pos, bigEndian);
pos += 4;
bool le = data[pos] == 1;
pos += 1;
auto& array = g_gxState.arrays[attrIdx];
const auto newData = reinterpret_cast<void*>(arrayAddr);
if (array.data != newData || array.size != arraySize || array.le != le) {
array.data = newData;
array.size = arraySize;
array.le = le;
// Only drop the cached upload when the backing array actually changes.
array.cachedRange = {};
mark_pipeline_state_dirty();
}
} else if (subCmd == GX_LOAD_AURORA_TEXOBJ) {
CHECK(pos + 34 <= size, "GX_LOAD_AURORA_TEXOBJ read overrun");
const auto texMapId = data[pos];
pos += 1;
CHECK(texMapId < MaxTextures, "invalid texture map id {}", texMapId);
auto& slot = g_gxState.loadedTextures[texMapId];
GXTexObj_ next = slot;
next.data = reinterpret_cast<const void*>(read_u64(data + pos, bigEndian));
pos += 8;
next.mWidth = read_u32(data + pos, bigEndian);
pos += 4;
next.mHeight = read_u32(data + pos, bigEndian);
pos += 4;
next.mFormat = static_cast<GXTexFmt>(read_u32(data + pos, bigEndian));
pos += 4;
next.tlut = static_cast<GXTlut>(read_u32(data + pos, bigEndian));
pos += 4;
if (data[pos] != 0) {
next.flags |= 1u;
} else {
next.flags &= ~1u;
}
pos += 1;
next.texObjId = read_u32(data + pos, bigEndian);
pos += 4;
next.texDataVersion = read_u32(data + pos, bigEndian);
pos += 4;
next.set_no_cache(false); // Reset no-cache flag
const bool changed = slot.data != next.data || slot.mWidth != next.mWidth || slot.mHeight != next.mHeight ||
slot.mFormat != next.mFormat || slot.tlut != next.tlut || slot.flags != next.flags ||
slot.texObjId != next.texObjId || slot.texDataVersion != next.texDataVersion;
slot = next;
if (changed) {
g_gxState.stateDirty = true;
}
} else if (subCmd == GX_LOAD_AURORA_TLUT) {
CHECK(pos + 23 <= size, "GX_LOAD_AURORA_TLUT read overrun");
const auto idx = data[pos];
pos += 1;
CHECK(idx < MaxTluts, "invalid tlut slot {}", idx);
auto& slot = g_gxState.loadedTluts[idx];
slot.data = reinterpret_cast<const void*>(read_u64(data + pos, bigEndian));
pos += 8;
slot.format = static_cast<GXTlutFmt>(read_u32(data + pos, bigEndian));
pos += 4;
slot.numEntries = read_u16(data + pos, bigEndian);
pos += 2;
slot.tlutObjId = read_u32(data + pos, bigEndian);
pos += 4;
slot.tlutDataVersion = read_u32(data + pos, bigEndian);
pos += 4;
slot.set_no_cache(false); // Reset no-cache flag
g_gxState.stateDirty = true;
} else if (subCmd == GX_LOAD_AURORA_DESTROY_TEXOBJ) {
CHECK(pos + 4 <= size, "GX_LOAD_AURORA_DESTROY_TEXOBJ read overrun");
evict_texture_object(read_u32(data + pos, bigEndian));
pos += 4;
} else if (subCmd == GX_LOAD_AURORA_DESTROY_TLUT) {
CHECK(pos + 4 <= size, "GX_LOAD_AURORA_DESTROY_TLUT read overrun");
evict_tlut_object(read_u32(data + pos, bigEndian));
pos += 4;
} else if (subCmd == GX_LOAD_AURORA_DESTROY_COPY_TEX) {
CHECK(pos + 8 <= size, "GX_LOAD_AURORA_DESTROY_COPY_TEX read overrun");
evict_copy_texture(reinterpret_cast<const void*>(read_u64(data + pos, bigEndian)));
pos += 8;
} else if (subCmd == GX_LOAD_AURORA_INVALIDATE_TEX_ALL) {
invalidate_static_texture_cache();
} else if (subCmd == GX_LOAD_AURORA_DEBUG_GROUP_PUSH) {
auto label = read_string(data, pos, size, bigEndian);
gfx::push_debug_group(std::move(label));
} else if (subCmd == GX_LOAD_AURORA_DEBUG_GROUP_POP) {
aurora_pop_debug_group();
} else if (subCmd == GX_LOAD_AURORA_DEBUG_MARKER_INSERT) {
auto label = read_string(data, pos, size, bigEndian);
gfx::insert_debug_marker(std::move(label));
}
else {
static u32 unknownAuroraLogCount = 0;
if (unknownAuroraLogCount < 16) {
Log.warn("Unknown Aurora subcommand: {:04X}; stopping FIFO decode", subCmd);
++unknownAuroraLogCount;
}
return false;
}
return true;
}
} // namespace aurora::gx::fifo