2026-02-17 17:35:13 -07:00
|
|
|
#include "command_processor.hpp"
|
|
|
|
|
|
2026-03-05 20:31:51 -07:00
|
|
|
#include "../gfx/common.hpp"
|
2026-04-12 15:35:28 -06:00
|
|
|
#include "dolphin/gx/GXAurora.h"
|
2026-02-17 17:35:13 -07:00
|
|
|
#include "gx.hpp"
|
|
|
|
|
#include "gx_fmt.hpp"
|
2026-03-05 20:31:51 -07:00
|
|
|
#include "pipeline.hpp"
|
2026-02-17 17:35:13 -07:00
|
|
|
#include "shader_info.hpp"
|
|
|
|
|
#include "../internal.hpp"
|
|
|
|
|
|
|
|
|
|
#include <absl/container/flat_hash_map.h>
|
2026-04-12 15:35:28 -06:00
|
|
|
#include <tracy/Tracy.hpp>
|
2026-02-17 17:35:13 -07:00
|
|
|
|
2026-02-18 13:41:42 -07:00
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstring>
|
2026-04-12 15:35:28 -06:00
|
|
|
#include <optional>
|
2026-04-09 03:19:58 +02:00
|
|
|
|
2026-03-05 20:31:51 -07:00
|
|
|
namespace aurora::gx::fifo {
|
|
|
|
|
static Module Log("aurora::gx::fifo");
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
static u16 prepare_idx_buffer(ByteBuffer& buf, GXPrimitive prim, u16 vtxStart, u16 vtxCount) {
|
|
|
|
|
u16 numIndices = 0;
|
|
|
|
|
if (prim == GX_QUADS) {
|
|
|
|
|
buf.reserve_extra((vtxCount / 4) * 6 * sizeof(u16));
|
|
|
|
|
|
|
|
|
|
for (u16 v = 0; v < vtxCount; v += 4) {
|
|
|
|
|
u16 idx0 = vtxStart + v;
|
|
|
|
|
u16 idx1 = vtxStart + v + 1;
|
|
|
|
|
u16 idx2 = vtxStart + v + 2;
|
|
|
|
|
u16 idx3 = vtxStart + v + 3;
|
|
|
|
|
|
|
|
|
|
buf.append(idx0);
|
|
|
|
|
buf.append(idx1);
|
|
|
|
|
buf.append(idx2);
|
|
|
|
|
numIndices += 3;
|
|
|
|
|
|
|
|
|
|
buf.append(idx2);
|
|
|
|
|
buf.append(idx3);
|
|
|
|
|
buf.append(idx0);
|
|
|
|
|
numIndices += 3;
|
|
|
|
|
}
|
|
|
|
|
} else if (prim == GX_TRIANGLES) {
|
|
|
|
|
buf.reserve_extra(vtxCount * sizeof(u16));
|
|
|
|
|
for (u16 v = 0; v < vtxCount; ++v) {
|
|
|
|
|
const u16 idx = vtxStart + v;
|
|
|
|
|
buf.append(idx);
|
|
|
|
|
++numIndices;
|
|
|
|
|
}
|
|
|
|
|
} else if (prim == GX_TRIANGLEFAN) {
|
|
|
|
|
buf.reserve_extra(((u32(vtxCount) - 3) * 3 + 3) * sizeof(u16));
|
|
|
|
|
for (u16 v = 0; v < vtxCount; ++v) {
|
|
|
|
|
const u16 idx = vtxStart + v;
|
|
|
|
|
if (v < 3) {
|
|
|
|
|
buf.append(idx);
|
|
|
|
|
++numIndices;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
buf.append(std::array{vtxStart, static_cast<u16>(idx - 1), idx});
|
|
|
|
|
numIndices += 3;
|
|
|
|
|
}
|
|
|
|
|
} else if (prim == GX_TRIANGLESTRIP) {
|
|
|
|
|
buf.reserve_extra(((static_cast<u32>(vtxCount) - 3) * 3 + 3) * sizeof(u16));
|
|
|
|
|
for (u16 v = 0; v < vtxCount; ++v) {
|
|
|
|
|
const u16 idx = vtxStart + v;
|
|
|
|
|
if (v < 3) {
|
|
|
|
|
buf.append(idx);
|
|
|
|
|
++numIndices;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ((v & 1) == 0) {
|
|
|
|
|
buf.append(std::array{static_cast<u16>(idx - 2), static_cast<u16>(idx - 1), idx});
|
|
|
|
|
} else {
|
|
|
|
|
buf.append(std::array{static_cast<u16>(idx - 1), static_cast<u16>(idx - 2), idx});
|
|
|
|
|
}
|
|
|
|
|
numIndices += 3;
|
|
|
|
|
}
|
2026-03-31 00:43:08 -06:00
|
|
|
} else if (prim == GX_LINES || prim == GX_LINESTRIP || prim == GX_POINTS) {
|
2026-03-29 22:33:28 -06:00
|
|
|
buf.reserve_extra(6 * sizeof(u16));
|
|
|
|
|
buf.append<u16>(0);
|
|
|
|
|
buf.append<u16>(1);
|
|
|
|
|
buf.append<u16>(3);
|
|
|
|
|
buf.append<u16>(3);
|
|
|
|
|
buf.append<u16>(2);
|
|
|
|
|
buf.append<u16>(0);
|
|
|
|
|
numIndices = 6;
|
2026-02-17 17:35:13 -07:00
|
|
|
} else
|
|
|
|
|
UNLIKELY FATAL("unsupported primitive type {}", static_cast<u32>(prim));
|
|
|
|
|
return numIndices;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
// Read helpers for big/little endian
|
2026-04-09 03:51:06 +02:00
|
|
|
#if _MSC_VER
|
2026-06-03 19:52:26 -07:00
|
|
|
template <typename T>
|
2026-04-09 03:51:06 +02:00
|
|
|
__forceinline // Yes, this was necessary.
|
2026-06-03 19:52:26 -07:00
|
|
|
inline T unaligned_load(const T* ptr) {
|
2026-04-09 03:51:06 +02:00
|
|
|
return *static_cast<const __unaligned T*>(ptr);
|
|
|
|
|
}
|
|
|
|
|
#else
|
2026-06-03 19:52:26 -07:00
|
|
|
template <typename T>
|
2026-04-09 03:51:06 +02:00
|
|
|
inline T unaligned_load(const T* ptr) {
|
|
|
|
|
T copy;
|
|
|
|
|
memcpy(©, ptr, sizeof(T));
|
|
|
|
|
return copy;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-02-17 17:35:13 -07:00
|
|
|
static inline u16 read_u16(const u8* ptr, bool bigEndian) {
|
2026-04-09 03:51:06 +02:00
|
|
|
const u16 val = unaligned_load(reinterpret_cast<const u16*>(ptr));
|
2026-02-17 17:35:13 -07:00
|
|
|
if (bigEndian) {
|
2026-04-09 03:51:06 +02:00
|
|
|
return bswap(val);
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
2026-04-09 03:51:06 +02:00
|
|
|
return val;
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static inline u32 read_u32(const u8* ptr, bool bigEndian) {
|
2026-04-09 03:51:06 +02:00
|
|
|
const u32 val = unaligned_load(reinterpret_cast<const u32*>(ptr));
|
2026-02-17 17:35:13 -07:00
|
|
|
if (bigEndian) {
|
2026-04-09 03:51:06 +02:00
|
|
|
return bswap(val);
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
2026-04-09 03:51:06 +02:00
|
|
|
return val;
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:49:28 -06:00
|
|
|
static u32 bp_get(u32 reg, u32 size, u32 shift);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 06:27:16 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 00:14:00 -06:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 11:26:51 -08:00
|
|
|
// 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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:35:13 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 11:26:51 -08:00
|
|
|
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
|
|
|
|
|
CHECK(mtxIdx < MaxPnMtx, "XF: PosMtx copy oob? Should never happen; mtxIdx={}", mtxIdx);
|
|
|
|
|
CHECK(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;
|
|
|
|
|
} else if (addr < 0x0F0) {
|
|
|
|
|
// Texture matrices (0x078-0x0EF)
|
|
|
|
|
u32 texBase = addr - 0x078;
|
|
|
|
|
u32 mtxIdx = texBase / 12;
|
|
|
|
|
u32 startOffset = texBase % 12;
|
|
|
|
|
CHECK(mtxIdx < MaxTexMtx, "XF TexMtx copy oob? Should never happen; mtxIdx={}", mtxIdx);
|
|
|
|
|
CHECK(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
|
|
|
|
|
CHECK(mtxIdx < MaxPnMtx, "XF: NrmMtx copy oob? Should never happen; mtxIdx={}", mtxIdx);
|
|
|
|
|
CHECK(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;
|
|
|
|
|
CHECK(mtxIdx < MaxPTTexMtx, "XF: PTTexMtx copy oob? Should never happen; mtxIdx={}", mtxIdx);
|
|
|
|
|
CHECK(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;
|
|
|
|
|
CHECK(lightIdx < 8, "XF: Light copy oob? Should never happen; lightIdx={}", lightIdx);
|
2026-03-18 23:58:00 -06:00
|
|
|
CHECK(startOffset + len <= 0x10, "XF: Light copy that crosses across light boundaries unsupported: offs={}, len={}",
|
|
|
|
|
startOffset, len);
|
2026-03-07 11:26:51 -08:00
|
|
|
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.stateDirty = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 23:58:00 -06:00
|
|
|
// Forward declarations for register handlers
|
2026-02-17 17:35:13 -07:00
|
|
|
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 void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian);
|
2026-03-06 06:27:16 +01:00
|
|
|
static void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian);
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
void process(const u8* data, u32 size, bool bigEndian) {
|
2026-04-09 03:19:58 +02:00
|
|
|
ZoneScoped;
|
2026-02-17 17:35:13 -07:00
|
|
|
u32 pos = 0;
|
|
|
|
|
|
|
|
|
|
while (pos < size) {
|
|
|
|
|
u8 cmd = data[pos++];
|
|
|
|
|
u8 opcode = cmd & CP_OPCODE_MASK;
|
2026-02-17 20:44:09 -07:00
|
|
|
// Log.warn("Processing opcode {:02x} at pos {} (size {})", opcode, pos - 1, size);
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
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: {
|
2026-04-09 03:19:58 +02:00
|
|
|
ZoneScopedN("LOAD_INDX");
|
2026-02-17 17:35:13 -07:00
|
|
|
// Indexed XF load: 4 bytes of data
|
|
|
|
|
CHECK(pos + 4 <= size, "indexed XF read overrun");
|
2026-03-07 11:26:51 -08:00
|
|
|
u32 arrayType = GX_POS_MTX_ARRAY + (opcode - (CP_CMD_LOAD_INDX_A / 0x08));
|
|
|
|
|
u8 srcArrayIdx = data[pos++];
|
|
|
|
|
auto const& array = g_gxState.arrays[arrayType];
|
|
|
|
|
u8* srcData = ((u8*)array.data) + srcArrayIdx * array.stride;
|
|
|
|
|
u16 addrLen = read_u16(data + pos, bigEndian);
|
|
|
|
|
u16 len = (addrLen >> 12) + 1;
|
|
|
|
|
u16 dstAddr = addrLen & 0x0FFF;
|
|
|
|
|
if (!copy_xf_data(dstAddr, srcData, len, bigEndian)) {
|
2026-04-12 15:35:28 -06:00
|
|
|
#ifndef NDEBUG
|
2026-03-09 21:36:35 -06:00
|
|
|
Log.debug("Unimplemented indexed XF load (opcode 0x{:02X}, dstAddr=%04x)", opcode, dstAddr);
|
2026-04-12 15:35:28 -06:00
|
|
|
#endif
|
2026-03-07 11:26:51 -08:00
|
|
|
}
|
2026-02-17 17:35:13 -07:00
|
|
|
pos += 4;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case CP_CMD_CALL_DL: {
|
|
|
|
|
// Call display list: 8 bytes (address + size)
|
|
|
|
|
CHECK(pos + 8 <= size, "call DL read overrun");
|
2026-02-18 00:26:18 -07:00
|
|
|
Log.warn("Ignoring nested GX_CMD_CALL_DL");
|
2026-02-17 17:35:13 -07:00
|
|
|
pos += 8;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case CP_CMD_INVAL_VTX: {
|
2026-02-18 13:41:42 -07:00
|
|
|
// Invalidate vertex cache
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 06:27:16 +01:00
|
|
|
case GX_LOAD_AURORA: {
|
|
|
|
|
handle_aurora(data, pos, size, bigEndian);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:35:13 -07:00
|
|
|
// Draw commands: 0x80-0xBF
|
|
|
|
|
case GX_DRAW_QUADS:
|
|
|
|
|
case GX_DRAW_TRIANGLES:
|
|
|
|
|
case GX_DRAW_TRIANGLE_STRIP:
|
|
|
|
|
case GX_DRAW_TRIANGLE_FAN:
|
|
|
|
|
case GX_DRAW_LINES:
|
|
|
|
|
case GX_DRAW_LINE_STRIP:
|
|
|
|
|
case GX_DRAW_POINTS: {
|
|
|
|
|
handle_draw(cmd, data, pos, size, bigEndian);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Check if it's a draw command (0x80-0xBF range)
|
|
|
|
|
if (cmd >= 0x80) {
|
|
|
|
|
handle_draw(cmd, data, pos, size, bigEndian);
|
|
|
|
|
} else {
|
2026-02-17 20:44:09 -07:00
|
|
|
// 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]);
|
|
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
Log.error(" hex dump (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex);
|
2026-02-17 20:44:09 -07:00
|
|
|
}
|
2026-02-17 17:35:13 -07:00
|
|
|
FATAL("command_processor: unknown opcode 0x{:02X} at pos {}", cmd, pos - 1);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper to extract bit fields from a 32-bit register
|
2026-04-09 03:51:06 +02:00
|
|
|
inline static u32 bp_get(u32 reg, u32 size, u32 shift) { return reg >> shift & (1u << size) - 1; }
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
2026-03-04 23:19:38 -08:00
|
|
|
if (regId == 0xFE) {
|
|
|
|
|
g_gxState.bpRegCache[regId] = value & 0x00FFFFFF;
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
u32 ssMask = g_gxState.bpRegCache[0xFE];
|
|
|
|
|
g_gxState.bpRegCache[0xFE] = 0x00FFFFFF;
|
2026-04-09 00:14:00 -06:00
|
|
|
const u32 merged = (g_gxState.bpRegCache[regId] & ~ssMask) | (value & ssMask);
|
|
|
|
|
value = (regId << 24) | (merged & 0x00FFFFFF);
|
2026-06-03 19:52:26 -07:00
|
|
|
if (g_gxState.bpRegCache[regId] == value)
|
|
|
|
|
return;
|
2026-03-04 23:19:38 -08:00
|
|
|
g_gxState.bpRegCache[regId] = value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:35:13 -07:00
|
|
|
// TEV color combiner stages (0xC0, 0xC2, 0xC4, ... 0xDE)
|
|
|
|
|
if (regId >= 0xC0 && regId <= 0xDE && (regId & 1) == 0) {
|
|
|
|
|
u32 stage = (regId - 0xC0) / 2;
|
2026-03-05 20:31:51 -07:00
|
|
|
if (stage < MaxTevStages) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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) {
|
2026-02-17 20:44:09 -07:00
|
|
|
// 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);
|
2026-02-17 17:35:13 -07:00
|
|
|
s.colorOp.bias = GX_TB_ZERO;
|
|
|
|
|
s.colorOp.scale = GX_CS_SCALE_1;
|
2026-02-17 20:44:09 -07:00
|
|
|
} 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));
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TEV alpha combiner stages (0xC1, 0xC3, 0xC5, ... 0xDF)
|
|
|
|
|
if (regId >= 0xC1 && regId <= 0xDF && (regId & 1) == 1) {
|
|
|
|
|
u32 stage = (regId - 0xC1) / 2;
|
2026-03-05 20:31:51 -07:00
|
|
|
if (stage < MaxTevStages) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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) {
|
2026-02-17 20:44:09 -07:00
|
|
|
u32 hwOp = bp_get(value, 1, 18) | (bp_get(value, 2, 20) << 1);
|
|
|
|
|
s.alphaOp.op = static_cast<GXTevOp>(hwOp + 8);
|
2026-02-17 17:35:13 -07:00
|
|
|
s.alphaOp.bias = GX_TB_ZERO;
|
|
|
|
|
s.alphaOp.scale = GX_CS_SCALE_1;
|
2026-02-17 20:44:09 -07:00
|
|
|
} 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));
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (regId) {
|
|
|
|
|
// genMode (0x00)
|
|
|
|
|
case 0x00: {
|
|
|
|
|
g_gxState.numTexGens = bp_get(value, 4, 0);
|
|
|
|
|
g_gxState.numChans = bp_get(value, 3, 4);
|
|
|
|
|
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);
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BP mask (0x0F) - internal, applies to next BP write
|
|
|
|
|
case 0x0F:
|
2026-04-12 15:35:28 -06:00
|
|
|
#ifndef NDEBUG
|
2026-03-09 21:36:35 -06:00
|
|
|
Log.debug("BP mask set to {:06x}, but selective updates are not implemented", value & 0xFFFFFF);
|
2026-04-12 15:35:28 -06:00
|
|
|
#endif
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// TEV indirect stages (0x10-0x1F)
|
2026-03-05 20:31:51 -07:00
|
|
|
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: {
|
2026-02-17 17:35:13 -07:00
|
|
|
u32 stage = regId - 0x10;
|
2026-03-05 20:31:51 -07:00
|
|
|
if (stage < MaxTevStages) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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;
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scissor registers (0x20, 0x21)
|
2026-03-05 20:31:51 -07:00
|
|
|
case 0x20:
|
|
|
|
|
case 0x21: {
|
2026-04-18 14:10:32 -06:00
|
|
|
const u32 scis0 = g_gxState.bpRegCache[0x20];
|
|
|
|
|
const u32 scis1 = g_gxState.bpRegCache[0x21];
|
2026-04-30 09:40:43 -07:00
|
|
|
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});
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 13:41:42 -07:00
|
|
|
// Line/point size (0x22)
|
|
|
|
|
case 0x22: {
|
2026-03-29 22:33:28 -06:00
|
|
|
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;
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
2026-02-18 13:41:42 -07:00
|
|
|
}
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
// Indirect texture scale (0x25, 0x26)
|
|
|
|
|
case 0x25: {
|
2026-03-05 20:31:51 -07:00
|
|
|
if (MaxIndStages > 0) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
if (MaxIndStages > 1) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 0x26: {
|
2026-03-05 20:31:51 -07:00
|
|
|
if (MaxIndStages > 2) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
if (MaxIndStages > 3) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Indirect texture reference (0x27)
|
|
|
|
|
case 0x27: {
|
2026-03-05 20:31:51 -07:00
|
|
|
for (u32 i = 0; i < MaxIndStages; i++) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TEV order / tref (0x28-0x2F) - 2 stages per register
|
2026-03-05 20:31:51 -07:00
|
|
|
case 0x28:
|
|
|
|
|
case 0x29:
|
|
|
|
|
case 0x2A:
|
|
|
|
|
case 0x2B:
|
|
|
|
|
case 0x2C:
|
|
|
|
|
case 0x2D:
|
|
|
|
|
case 0x2E:
|
|
|
|
|
case 0x2F: {
|
2026-02-17 17:35:13 -07:00
|
|
|
u32 idx = regId - 0x28;
|
|
|
|
|
u32 stage0 = idx * 2;
|
|
|
|
|
u32 stage1 = idx * 2 + 1;
|
|
|
|
|
|
|
|
|
|
// Channel ID reverse mapping from hardware to GX
|
2026-03-11 02:19:34 -06:00
|
|
|
static const GXChannelID r2c[] = {GX_COLOR0A0, GX_COLOR1A1, GX_COLOR0A0, GX_COLOR1A1,
|
|
|
|
|
GX_COLOR0A0, GX_ALPHA_BUMP, GX_ALPHA_BUMPN, GX_COLOR_ZERO};
|
2026-02-17 17:35:13 -07:00
|
|
|
|
2026-03-05 20:31:51 -07:00
|
|
|
if (stage0 < MaxTevStages) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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;
|
|
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
if (stage1 < MaxTevStages) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
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;
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
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;
|
2026-04-01 00:49:28 -06:00
|
|
|
g_gxState.pixelFmt = decode_pixel_fmt(g_gxState.bpRegCache[0x43], value);
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:49:28 -06:00
|
|
|
// PE control (0x43) - pixel format, z format, zcomp location
|
2026-02-18 13:41:42 -07:00
|
|
|
case 0x43: {
|
2026-04-01 00:49:28 -06:00
|
|
|
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;
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
2026-02-18 13:41:42 -07:00
|
|
|
}
|
2026-02-17 17:35:13 -07:00
|
|
|
|
2026-04-09 00:14:00 -06:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:35:13 -07:00
|
|
|
// Alpha compare (0xF3)
|
|
|
|
|
case 0xF3: {
|
|
|
|
|
g_gxState.alphaCompare.ref0 = bp_get(value, 8, 0);
|
2026-02-17 20:44:09 -07:00
|
|
|
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));
|
2026-02-17 17:35:13 -07:00
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TEV K color/alpha select (0xF6-0xFD)
|
2026-03-05 20:31:51 -07:00
|
|
|
case 0xF6:
|
|
|
|
|
case 0xF7:
|
|
|
|
|
case 0xF8:
|
|
|
|
|
case 0xF9:
|
|
|
|
|
case 0xFA:
|
|
|
|
|
case 0xFB:
|
|
|
|
|
case 0xFC:
|
|
|
|
|
case 0xFD: {
|
2026-02-17 17:35:13 -07:00
|
|
|
u32 kselIdx = regId - 0xF6;
|
|
|
|
|
// Swap table entries (packed into pairs of ksel registers)
|
2026-03-05 20:31:51 -07:00
|
|
|
if (kselIdx < MaxTevSwap * 2) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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;
|
2026-03-05 20:31:51 -07:00
|
|
|
if (stage0 < MaxTevStages) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
if (stage1 < MaxTevStages) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 13:41:42 -07:00
|
|
|
// 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;
|
|
|
|
|
// Reconstruct A = a_encoded * 2^b_s
|
|
|
|
|
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);
|
|
|
|
|
float a_encoded;
|
|
|
|
|
std::memcpy(&a_encoded, &a_bits, sizeof(a_encoded));
|
|
|
|
|
u32 b_s = g_gxState.fog.fog2Raw & 0x1F;
|
|
|
|
|
g_gxState.fog.a = std::ldexp(a_encoded, static_cast<int>(b_s));
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// FOG1 (0xEF): B mantissa (24-bit)
|
|
|
|
|
case 0xEF: {
|
|
|
|
|
g_gxState.fog.fog1Raw = value;
|
|
|
|
|
u32 b_m = bp_get(value, 24, 0);
|
|
|
|
|
u32 b_s = g_gxState.fog.fog2Raw & 0x1F;
|
|
|
|
|
float B_mant = static_cast<float>(b_m) / 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);
|
|
|
|
|
// Recompute A with updated 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);
|
|
|
|
|
float a_encoded;
|
|
|
|
|
std::memcpy(&a_encoded, &a_bits, sizeof(a_encoded));
|
|
|
|
|
g_gxState.fog.a = std::ldexp(a_encoded, static_cast<int>(b_s));
|
|
|
|
|
// Recompute B with updated b_s
|
|
|
|
|
u32 b_m = bp_get(g_gxState.fog.fog1Raw, 24, 0);
|
|
|
|
|
float B_mant = static_cast<float>(b_m) / 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)
|
2026-02-17 17:35:13 -07:00
|
|
|
case 0xF1: {
|
|
|
|
|
GXFogType fogType = static_cast<GXFogType>(bp_get(value, 3, 21));
|
|
|
|
|
g_gxState.fog.type = fogType;
|
2026-02-18 13:41:42 -07:00
|
|
|
// 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));
|
2026-02-17 17:35:13 -07:00
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
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 color registers / K color registers (0xE0-0xE7)
|
|
|
|
|
// RA registers: 0xE0, 0xE2, 0xE4, 0xE6 (even)
|
|
|
|
|
// BG registers: 0xE1, 0xE3, 0xE5, 0xE7 (odd)
|
|
|
|
|
// Bit 23 distinguishes: 0 = TEV color register, 1 = K color register
|
2026-03-05 20:31:51 -07:00
|
|
|
case 0xE0:
|
|
|
|
|
case 0xE1:
|
|
|
|
|
case 0xE2:
|
|
|
|
|
case 0xE3:
|
|
|
|
|
case 0xE4:
|
|
|
|
|
case 0xE5:
|
|
|
|
|
case 0xE6:
|
|
|
|
|
case 0xE7: {
|
2026-02-17 17:35:13 -07:00
|
|
|
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)
|
2026-03-05 20:31:51 -07:00
|
|
|
if (idx < MaxTevRegs) {
|
2026-02-17 17:35:13 -07:00
|
|
|
auto& cr = g_gxState.colorRegs[idx];
|
|
|
|
|
if (isRA) {
|
|
|
|
|
// 11-bit signed: sign-extend from 11 bits
|
|
|
|
|
s32 r = bp_get(value, 11, 0);
|
2026-03-05 20:31:51 -07:00
|
|
|
if (r & 0x400)
|
|
|
|
|
r |= ~0x7FF; // sign extend
|
2026-02-17 17:35:13 -07:00
|
|
|
s32 a = bp_get(value, 11, 12);
|
2026-03-05 20:31:51 -07:00
|
|
|
if (a & 0x400)
|
|
|
|
|
a |= ~0x7FF;
|
2026-02-17 17:35:13 -07:00
|
|
|
cr[0] = static_cast<float>(r) / 255.f;
|
|
|
|
|
cr[3] = static_cast<float>(a) / 255.f;
|
|
|
|
|
} else {
|
|
|
|
|
s32 b = bp_get(value, 11, 0);
|
2026-03-05 20:31:51 -07:00
|
|
|
if (b & 0x400)
|
|
|
|
|
b |= ~0x7FF;
|
2026-02-17 17:35:13 -07:00
|
|
|
s32 g = bp_get(value, 11, 12);
|
2026-03-05 20:31:51 -07:00
|
|
|
if (g & 0x400)
|
|
|
|
|
g |= ~0x7FF;
|
2026-02-17 17:35:13 -07:00
|
|
|
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)
|
2026-02-18 13:41:42 -07:00
|
|
|
// Each matrix uses 3 consecutive registers (one per row of the 3x2 matrix).
|
|
|
|
|
// Matrix 0: 0x06-0x08, Matrix 1: 0x09-0x0B, Matrix 2: 0x0C-0x0E
|
2026-03-05 20:31:51 -07:00
|
|
|
case 0x06:
|
|
|
|
|
case 0x07:
|
|
|
|
|
case 0x08:
|
|
|
|
|
case 0x09:
|
|
|
|
|
case 0x0A:
|
|
|
|
|
case 0x0B:
|
|
|
|
|
case 0x0C:
|
|
|
|
|
case 0x0D:
|
|
|
|
|
case 0x0E: {
|
2026-03-18 23:58:00 -06:00
|
|
|
u32 idx = (regId - 0x06) / 3; // matrix index (0-2)
|
2026-03-11 02:19:34 -06:00
|
|
|
u32 column = (regId - 0x06) % 3; // column index (0-2)
|
2026-02-18 13:41:42 -07:00
|
|
|
auto& info = g_gxState.indTexMtxs[idx];
|
|
|
|
|
|
2026-03-11 02:19:34 -06:00
|
|
|
// Decode one packed matrix column: [m[0][column], m[1][column]].
|
2026-02-18 13:41:42 -07:00
|
|
|
s32 col0 = bp_get(value, 11, 0);
|
2026-03-05 20:31:51 -07:00
|
|
|
if (col0 & 0x400)
|
|
|
|
|
col0 |= ~0x7FF; // sign-extend from 11 bits
|
2026-02-18 13:41:42 -07:00
|
|
|
s32 col1 = bp_get(value, 11, 11);
|
2026-03-05 20:31:51 -07:00
|
|
|
if (col1 & 0x400)
|
|
|
|
|
col1 |= ~0x7FF;
|
2026-02-18 13:41:42 -07:00
|
|
|
|
2026-03-11 02:19:34 -06:00
|
|
|
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;
|
2026-02-18 13:41:42 -07:00
|
|
|
|
2026-04-07 18:08:25 -06:00
|
|
|
// Accumulate the indirect matrix scale exponent. The SDK writes two bits per column, but
|
|
|
|
|
// the hardware appears to ignore the top bit from the third column, leaving an effective
|
|
|
|
|
// 5-bit value for adjScale = scaleExp + 17.
|
2026-02-18 13:41:42 -07:00
|
|
|
u32 scaleBits = bp_get(value, 2, 22);
|
2026-03-11 02:19:34 -06:00
|
|
|
u32 shift = column * 2;
|
2026-04-07 18:08:25 -06:00
|
|
|
if (column == 2) {
|
|
|
|
|
info.adjScaleRaw = (info.adjScaleRaw & ~(1u << shift)) | ((scaleBits & 1u) << shift);
|
|
|
|
|
} else {
|
|
|
|
|
info.adjScaleRaw = (info.adjScaleRaw & ~(3u << shift)) | (scaleBits << shift);
|
|
|
|
|
}
|
2026-02-18 13:41:42 -07:00
|
|
|
info.scaleExp = static_cast<s8>(info.adjScaleRaw) - 17;
|
|
|
|
|
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
2026-02-18 13:41:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SU texture coordinate scale registers (0x30-0x3F)
|
|
|
|
|
// Even registers (suTs0): S-axis scale, bias, cyl wrap, line/point offset
|
|
|
|
|
// Odd registers (suTs1): T-axis scale, bias, cyl wrap
|
2026-03-05 20:31:51 -07:00
|
|
|
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: {
|
2026-02-18 13:41:42 -07:00
|
|
|
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 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;
|
|
|
|
|
}
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
default:
|
2026-04-09 00:14:00 -06:00
|
|
|
if (const auto mapping = decode_tex_bp_reg(regId); mapping.has_value()) {
|
|
|
|
|
auto& slot = g_gxState.loadedTextures[mapping->texMapId];
|
|
|
|
|
switch (mapping->kind) {
|
|
|
|
|
case TexBpRegMapping::Kind::Mode0:
|
|
|
|
|
slot.mode0 = value;
|
|
|
|
|
break;
|
|
|
|
|
case TexBpRegMapping::Kind::Mode1:
|
|
|
|
|
slot.mode1 = value;
|
|
|
|
|
break;
|
|
|
|
|
case TexBpRegMapping::Kind::Image0:
|
|
|
|
|
slot.image0 = value;
|
2026-04-11 13:37:32 -06:00
|
|
|
slot.mWidth = 0;
|
|
|
|
|
slot.mHeight = 0;
|
|
|
|
|
slot.mFormat = gfx::InvalidTextureFormat;
|
2026-04-09 00:14:00 -06:00
|
|
|
break;
|
|
|
|
|
case TexBpRegMapping::Kind::Image3:
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-02-18 13:41:42 -07:00
|
|
|
} else {
|
2026-04-12 15:35:28 -06:00
|
|
|
#ifndef NDEBUG
|
2026-03-09 21:36:35 -06:00
|
|
|
Log.debug("Unhandled BP register 0x{:02X} (value 0x{:06X})", regId, value & 0xFFFFFF);
|
2026-04-12 15:35:28 -06:00
|
|
|
#endif
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CP register handler - decodes CP register writes and updates g_gxState
|
|
|
|
|
static void handle_cp(u8 addr, u32 value, bool bigEndian) {
|
|
|
|
|
switch (addr) {
|
|
|
|
|
// VCD low (0x50)
|
|
|
|
|
case 0x50: {
|
|
|
|
|
auto& vd = g_gxState.vtxDesc;
|
2026-03-05 20:31:51 -07:00
|
|
|
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));
|
2026-02-17 17:35:13 -07:00
|
|
|
g_gxState.stateDirty = true;
|
2026-04-09 03:51:06 +02:00
|
|
|
g_gxState.clearVtxSizeCache();
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VCD high (0x60)
|
|
|
|
|
case 0x60: {
|
|
|
|
|
auto& vd = g_gxState.vtxDesc;
|
|
|
|
|
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));
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-04-09 03:51:06 +02:00
|
|
|
g_gxState.clearVtxSizeCache();
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Matrix index A (0x30)
|
|
|
|
|
case 0x30: {
|
|
|
|
|
g_gxState.currentPnMtx = bp_get(value, 6, 0) / 3;
|
2026-05-14 16:29:45 -06:00
|
|
|
g_gxState.stateDirty = true;
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Matrix index B (0x40)
|
|
|
|
|
case 0x40:
|
|
|
|
|
// Texture matrix indices - used for multi-matrix texgen
|
|
|
|
|
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));
|
2026-06-03 19:52:26 -07:00
|
|
|
const auto nrm_cnt = bp_get(value, 1, 9);
|
|
|
|
|
const auto nrm_nbt3 = bp_get(value, 1, 31);
|
|
|
|
|
vf.attrs[GX_VA_NRM].cnt = static_cast<GXCompCnt>(nrm_nbt3 ? GX_NRM_NBT3 : (nrm_cnt ? GX_NRM_NBT : GX_NRM_XYZ));
|
2026-02-17 17:35:13 -07:00
|
|
|
vf.attrs[GX_VA_NRM].type = static_cast<GXCompType>(bp_get(value, 3, 10));
|
2026-03-10 00:22:11 -06:00
|
|
|
if (vf.attrs[GX_VA_NRM].type == GX_U8 || vf.attrs[GX_VA_NRM].type == GX_S8) {
|
|
|
|
|
vf.attrs[GX_VA_NRM].frac = 6;
|
|
|
|
|
} else if (vf.attrs[GX_VA_NRM].type == GX_U16 || vf.attrs[GX_VA_NRM].type == GX_S16) {
|
|
|
|
|
vf.attrs[GX_VA_NRM].frac = 14;
|
|
|
|
|
} else {
|
|
|
|
|
vf.attrs[GX_VA_NRM].frac = 0;
|
|
|
|
|
}
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-04-09 03:51:06 +02:00
|
|
|
g_gxState.clearVtxSizeCache();
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
// 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
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-04-09 03:51:06 +02:00
|
|
|
g_gxState.clearVtxSizeCache();
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
// 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));
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-04-09 03:51:06 +02:00
|
|
|
g_gxState.clearVtxSizeCache();
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
// Array base addresses (0xA0-0xAF)
|
|
|
|
|
else if (addr >= 0xA0 && addr <= 0xAF) {
|
2026-03-06 06:27:16 +01:00
|
|
|
Log.error("CP_REG_ARRAYBASE_ID is not supported on Aurora. Use GX_LOAD_AURORA_ARRAYBASE instead.");
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
// Array strides (0xB0-0xBF)
|
|
|
|
|
else if (addr >= 0xB0 && addr <= 0xBF) {
|
|
|
|
|
u32 attrIdx = addr - 0xB0 + GX_VA_POS;
|
|
|
|
|
if (attrIdx < GX_VA_MAX_ATTR) {
|
2026-03-13 19:46:11 -07:00
|
|
|
auto& array = g_gxState.arrays[attrIdx];
|
|
|
|
|
const auto newStride = static_cast<u8>(value);
|
|
|
|
|
if (array.stride != newStride) {
|
|
|
|
|
array.stride = newStride;
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
CHECK(pos + 4 <= size, "XF header read overrun");
|
|
|
|
|
u32 header = read_u32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
|
|
|
|
|
u32 count = ((header >> 16) & 0xFFFF) + 1;
|
|
|
|
|
u32 addr = header & 0xFFFF;
|
|
|
|
|
u32 dataBytes = count * 4;
|
2026-02-17 20:44:09 -07:00
|
|
|
// Log.warn(" xf: addr {:04x} count {} dataBytes {} pos {} -> {}", addr, count, dataBytes, pos, pos + dataBytes);
|
2026-02-17 17:35:13 -07:00
|
|
|
CHECK(pos + dataBytes <= size, "XF data read overrun: need {} bytes at pos {}", dataBytes, pos);
|
|
|
|
|
|
|
|
|
|
const u8* xfData = data + pos;
|
|
|
|
|
|
2026-03-07 11:26:51 -08:00
|
|
|
if (copy_xf_data(addr, xfData, count, bigEndian)) {
|
|
|
|
|
// copy_xf_data handled everything.
|
|
|
|
|
} else if (addr >= 0x1000) {
|
|
|
|
|
// XF registers (0x1000+)
|
2026-02-17 17:35:13 -07:00
|
|
|
u32 xfAddr = addr - 0x1000;
|
|
|
|
|
for (u32 i = 0; i < count; i++) {
|
|
|
|
|
u32 reg = xfAddr + i;
|
|
|
|
|
u32 val = read_u32(xfData + i * 4, bigEndian);
|
2026-04-12 18:40:21 -07:00
|
|
|
|
|
|
|
|
// Skip scalar register writes that haven't changed (viewport/projection handled below)
|
2026-06-03 19:52:26 -07:00
|
|
|
if (reg <= 0x19 && val == g_gxState.xfRegCache[reg])
|
|
|
|
|
continue;
|
|
|
|
|
if (reg <= 0x19)
|
|
|
|
|
g_gxState.xfRegCache[reg] = val;
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
switch (reg) {
|
|
|
|
|
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;
|
2026-03-05 20:31:51 -07:00
|
|
|
if (chanId < MaxColorChannels) {
|
2026-02-17 17:35:13 -07:00
|
|
|
auto& chan = g_gxState.colorChannelConfig[chanId];
|
2026-02-17 20:44:09 -07:00
|
|
|
chan.matSrc = static_cast<GXColorSrc>(bp_get(val, 1, 0));
|
|
|
|
|
chan.lightingEnabled = bp_get(val, 1, 1) != 0;
|
2026-02-17 17:35:13 -07:00
|
|
|
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));
|
2026-02-17 20:44:09 -07:00
|
|
|
// Encoding: bit 9 = (attnFn != GX_AF_SPEC), bit 10 = (attnFn != GX_AF_NONE)
|
|
|
|
|
bool bit9 = bp_get(val, 1, 9) != 0;
|
|
|
|
|
bool bit10 = bp_get(val, 1, 10) != 0;
|
2026-02-17 17:35:13 -07:00
|
|
|
u32 lightsHi = bp_get(val, 4, 11);
|
2026-02-17 20:44:09 -07:00
|
|
|
if (!bit10) {
|
2026-02-17 17:35:13 -07:00
|
|
|
chan.attnFn = GX_AF_NONE;
|
2026-02-17 20:44:09 -07:00
|
|
|
} else if (!bit9) {
|
2026-02-17 17:35:13 -07:00
|
|
|
chan.attnFn = GX_AF_SPEC;
|
|
|
|
|
} else {
|
|
|
|
|
chan.attnFn = GX_AF_SPOT;
|
|
|
|
|
}
|
|
|
|
|
u32 lightMask = lightsLo | (lightsHi << 4);
|
|
|
|
|
g_gxState.colorChannelState[chanId].lightMask = GX::LightMask{lightMask};
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-03-05 12:55:48 -07:00
|
|
|
case 0x18: {
|
|
|
|
|
// Matrix index A: PnMtx + TexCoord0-3 matrix indices
|
2026-02-17 17:35:13 -07:00
|
|
|
g_gxState.currentPnMtx = bp_get(val, 6, 0) / 3;
|
2026-03-05 20:31:51 -07:00
|
|
|
for (u32 i = 0; i < 4 && i < MaxTexCoord; i++) {
|
2026-03-08 09:12:56 +01:00
|
|
|
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;
|
2026-03-05 12:55:48 -07:00
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
2026-03-05 12:55:48 -07:00
|
|
|
}
|
|
|
|
|
case 0x19: {
|
|
|
|
|
// Matrix index B: TexCoord4-7 matrix indices
|
2026-03-05 20:31:51 -07:00
|
|
|
for (u32 i = 0; i < 4 && (i + 4) < MaxTexCoord; i++) {
|
2026-03-05 12:55:48 -07:00
|
|
|
g_gxState.tcgs[i + 4].mtx = static_cast<GXTexMtx>(bp_get(val, 6, i * 6));
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-02-17 17:35:13 -07:00
|
|
|
break;
|
2026-03-05 12:55:48 -07:00
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
case 0x1A:
|
|
|
|
|
case 0x1B:
|
|
|
|
|
case 0x1C:
|
|
|
|
|
case 0x1D:
|
|
|
|
|
case 0x1E:
|
|
|
|
|
case 0x1F: {
|
2026-02-17 17:35:13 -07:00
|
|
|
// Viewport: sx, sy, sz, ox, oy, oz at XF 0x101A-0x101F
|
|
|
|
|
u32 vpOff = reg - 0x1A;
|
|
|
|
|
if (vpOff == 0 && count >= 6) {
|
|
|
|
|
f32 sx = read_f32(xfData + 0, bigEndian);
|
|
|
|
|
f32 sy = read_f32(xfData + 4, bigEndian);
|
|
|
|
|
f32 sz = read_f32(xfData + 8, bigEndian);
|
|
|
|
|
f32 ox = read_f32(xfData + 12, bigEndian);
|
|
|
|
|
f32 oy = read_f32(xfData + 16, bigEndian);
|
|
|
|
|
f32 oz = read_f32(xfData + 20, bigEndian);
|
|
|
|
|
f32 width = sx * 2.0f;
|
|
|
|
|
f32 height = -sy * 2.0f;
|
2026-04-18 14:10:32 -06:00
|
|
|
set_logical_viewport({
|
|
|
|
|
.left = ox - 340.0f - width / 2.0f,
|
|
|
|
|
.top = oy - 340.0f - height / 2.0f,
|
|
|
|
|
.width = width,
|
|
|
|
|
.height = height,
|
|
|
|
|
.znear = (oz - sz) / 1.6777215e7f,
|
|
|
|
|
.zfar = oz / 1.6777215e7f,
|
|
|
|
|
});
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
case 0x20:
|
|
|
|
|
case 0x21:
|
|
|
|
|
case 0x22:
|
|
|
|
|
case 0x23:
|
|
|
|
|
case 0x24:
|
|
|
|
|
case 0x25:
|
|
|
|
|
case 0x26: {
|
2026-02-17 17:35:13 -07:00
|
|
|
// Projection: 6 params + type at XF 0x1020-0x1026
|
|
|
|
|
u32 projOff = reg - 0x20;
|
|
|
|
|
if (projOff == 0 && count >= 7) {
|
|
|
|
|
f32 p0 = read_f32(xfData + 0, bigEndian);
|
|
|
|
|
f32 p1 = read_f32(xfData + 4, bigEndian);
|
|
|
|
|
f32 p2 = read_f32(xfData + 8, bigEndian);
|
|
|
|
|
f32 p3 = read_f32(xfData + 12, bigEndian);
|
|
|
|
|
f32 p4 = read_f32(xfData + 16, bigEndian);
|
|
|
|
|
f32 p5 = read_f32(xfData + 20, bigEndian);
|
|
|
|
|
u32 projType = read_u32(xfData + 24, bigEndian);
|
|
|
|
|
g_gxState.projType = static_cast<GXProjectionType>(projType);
|
|
|
|
|
// Reconstruct 4x4 projection matrix from 6 params
|
|
|
|
|
auto& proj = g_gxState.proj;
|
|
|
|
|
proj = {};
|
|
|
|
|
proj.m0[0] = p0;
|
|
|
|
|
proj.m1[1] = p2;
|
|
|
|
|
proj.m2[2] = p4;
|
|
|
|
|
proj.m2[3] = p5;
|
|
|
|
|
if (projType == GX_ORTHOGRAPHIC) {
|
|
|
|
|
proj.m0[3] = p1;
|
|
|
|
|
proj.m1[3] = p3;
|
|
|
|
|
proj.m3[3] = 1.0f;
|
|
|
|
|
} else {
|
|
|
|
|
proj.m0[2] = p1;
|
|
|
|
|
proj.m1[2] = p3;
|
|
|
|
|
proj.m3[2] = -1.0f;
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 0x3F:
|
|
|
|
|
// numTexGens
|
|
|
|
|
g_gxState.numTexGens = val;
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// TexGen config (0x40-0x4F) and post-transform (0x50-0x5F)
|
|
|
|
|
if (reg >= 0x40 && reg <= 0x4F) {
|
|
|
|
|
u32 tcIdx = reg - 0x40;
|
2026-03-05 20:31:51 -07:00
|
|
|
if (tcIdx < MaxTexCoord) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
if (tgType == 0) {
|
|
|
|
|
tcg.type = proj ? GX_TG_MTX3x4 : GX_TG_MTX2x4;
|
|
|
|
|
} else if (tgType == 1) {
|
2026-06-03 19:52:26 -07:00
|
|
|
// Bump mapping: type encodes emboss light
|
2026-02-17 17:35:13 -07:00
|
|
|
tcg.type = static_cast<GXTexGenType>(bp_get(val, 3, 15) + 2);
|
|
|
|
|
} else if (tgType == 2 || tgType == 3) {
|
|
|
|
|
tcg.type = GX_TG_SRTG;
|
|
|
|
|
}
|
2026-06-03 19:52:26 -07:00
|
|
|
// Emboss source texcoord (bits 12-14); 0 for non-bump types
|
|
|
|
|
tcg.embossSrc = bp_get(val, 3, 12);
|
2026-02-17 17:35:13 -07:00
|
|
|
|
|
|
|
|
// Decode source from row
|
2026-03-05 20:31:51 -07:00
|
|
|
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};
|
2026-02-17 17:35:13 -07:00
|
|
|
if (srcRow < 13) {
|
|
|
|
|
tcg.src = rowToSrc[srcRow];
|
|
|
|
|
}
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
|
|
|
|
} else if (reg >= 0x50 && reg <= 0x5F) {
|
|
|
|
|
u32 tcIdx = reg - 0x50;
|
2026-03-05 20:31:51 -07:00
|
|
|
if (tcIdx < MaxTexCoord) {
|
2026-02-17 17:35:13 -07:00
|
|
|
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;
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
2026-02-21 16:40:40 -07:00
|
|
|
} else {
|
2026-04-12 15:35:28 -06:00
|
|
|
#ifndef NDEBUG
|
2026-03-09 21:36:35 -06:00
|
|
|
Log.debug("Unhandled XF register 0x{:04X} (value 0x{:08X})", reg, val);
|
2026-04-12 15:35:28 -06:00
|
|
|
#endif
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pos += dataBytes;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 03:51:06 +02:00
|
|
|
static void handle_draw_overrun [[noreturn]] (u32 totalVtxBytes, const u8* data, const u32& pos, u32 size) {
|
|
|
|
|
// 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.error(" hex dump around draw cmd (pos {}-{}):{}", dumpStart, dumpEnd - 1, hex);
|
|
|
|
|
FATAL("draw vertex data overrun: need {} bytes at pos {}, have {}", totalVtxBytes, pos, size);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:35:13 -07:00
|
|
|
// Draw command handler - parses vertices inline and caches results
|
2026-04-09 03:51:06 +02:00
|
|
|
static u32 calculate_last_vtx_size(GXVtxFmt fmt) {
|
2026-03-18 23:58:00 -06:00
|
|
|
u32 vtxSize = 0;
|
|
|
|
|
const auto& vtxFmt = g_gxState.vtxFmts[fmt];
|
|
|
|
|
for (int i = GX_VA_PNMTXIDX; i <= GX_VA_TEX7; ++i) {
|
|
|
|
|
switch (g_gxState.vtxDesc[i]) {
|
|
|
|
|
case GX_NONE:
|
|
|
|
|
break;
|
|
|
|
|
case GX_DIRECT: {
|
|
|
|
|
const auto attr = static_cast<GXAttr>(i);
|
|
|
|
|
const auto& attrFmt = vtxFmt.attrs[i];
|
|
|
|
|
vtxSize += comp_type_size(attr, attrFmt.type) * comp_cnt_count(attr, attrFmt.cnt);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case GX_INDEX8:
|
2026-06-03 19:52:26 -07:00
|
|
|
vtxSize += (i == GX_VA_NRM && vtxFmt.attrs[i].cnt == GX_NRM_NBT3) ? 3 : 1;
|
2026-03-18 23:58:00 -06:00
|
|
|
break;
|
|
|
|
|
case GX_INDEX16:
|
2026-06-03 19:52:26 -07:00
|
|
|
vtxSize += (i == GX_VA_NRM && vtxFmt.attrs[i].cnt == GX_NRM_NBT3) ? 6 : 2;
|
2026-03-18 23:58:00 -06:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-09 03:51:06 +02:00
|
|
|
|
|
|
|
|
g_gxState.lastVtxFmt = fmt;
|
|
|
|
|
g_gxState.lastVtxSize = vtxSize;
|
|
|
|
|
|
|
|
|
|
return vtxSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange);
|
|
|
|
|
|
|
|
|
|
// Draw command handler - parses vertices inline and caches results
|
|
|
|
|
static ByteBuffer handle_draw_idx_buf;
|
|
|
|
|
|
|
|
|
|
static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian) {
|
|
|
|
|
ZoneScoped;
|
|
|
|
|
u8 opcode = cmd & CP_OPCODE_MASK;
|
|
|
|
|
GXVtxFmt fmt = static_cast<GXVtxFmt>(cmd & CP_VAT_MASK);
|
|
|
|
|
GXPrimitive prim = static_cast<GXPrimitive>(opcode);
|
|
|
|
|
|
|
|
|
|
CHECK(pos + 2 <= size, "draw vtxCount read overrun");
|
|
|
|
|
u16 vtxCount = read_u16(data + pos, bigEndian);
|
|
|
|
|
pos += 2;
|
|
|
|
|
|
|
|
|
|
u32 vtxSize;
|
2026-06-03 19:52:26 -07:00
|
|
|
if (g_gxState.lastVtxFmt == fmt)
|
|
|
|
|
LIKELY { vtxSize = g_gxState.lastVtxSize; }
|
|
|
|
|
else
|
|
|
|
|
UNLIKELY { vtxSize = calculate_last_vtx_size(fmt); }
|
2026-04-09 03:51:06 +02:00
|
|
|
|
2026-02-17 17:35:13 -07:00
|
|
|
u32 totalVtxBytes = vtxCount * vtxSize;
|
2026-06-03 19:52:26 -07:00
|
|
|
if (pos + totalVtxBytes > size)
|
|
|
|
|
UNLIKELY { handle_draw_overrun(totalVtxBytes, data, pos, size); }
|
2026-02-17 17:35:13 -07:00
|
|
|
|
2026-06-04 23:43:13 -06:00
|
|
|
auto* lastDraw = !g_gxState.stateDirty ? gfx::get_last_draw_command<DrawData>() : nullptr;
|
|
|
|
|
const bool canMerge = lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS &&
|
|
|
|
|
lastDraw->instanceCount == 1;
|
|
|
|
|
|
|
|
|
|
// Push raw vertex data to buffer. Merged draws must remain byte-contiguous with the previous range.
|
|
|
|
|
gfx::Range vertRange = gfx::push_verts(data + pos, totalVtxBytes, canMerge ? 0 : 4);
|
2026-03-05 20:31:51 -07:00
|
|
|
pos += totalVtxBytes;
|
|
|
|
|
|
2026-03-29 22:33:28 -06:00
|
|
|
// Try to merge with previous draw call
|
2026-06-04 23:43:13 -06:00
|
|
|
if (canMerge) {
|
2026-03-29 22:33:28 -06:00
|
|
|
// Only if the previous draw call was a single instance draw (no lines/points handling)
|
2026-06-04 23:43:13 -06:00
|
|
|
u32 numIndices = 0;
|
|
|
|
|
gfx::Range idxRange;
|
|
|
|
|
const bool hadIndexRange = lastDraw->idxRange.size != 0;
|
|
|
|
|
if (lastDraw->indexCount == 0 && prim != GX_TRIANGLES) {
|
|
|
|
|
// Generate triangle index buffer for previous draw
|
|
|
|
|
lastDraw->indexCount = prepare_idx_buffer(handle_draw_idx_buf, GX_TRIANGLES, 0, lastDraw->vtxCount);
|
|
|
|
|
}
|
|
|
|
|
if (lastDraw->indexCount != 0) {
|
|
|
|
|
numIndices += prepare_idx_buffer(handle_draw_idx_buf, prim, lastDraw->vtxCount, vtxCount);
|
|
|
|
|
idxRange = gfx::push_indices(handle_draw_idx_buf.data(), handle_draw_idx_buf.size(), hadIndexRange ? 0 : 4);
|
2026-04-09 03:51:06 +02:00
|
|
|
handle_draw_idx_buf.clear();
|
2026-06-04 23:43:13 -06:00
|
|
|
}
|
|
|
|
|
CHECK(lastDraw->vertRange.offset + lastDraw->vertRange.size == vertRange.offset,
|
|
|
|
|
"Non-consecutive vertex ranges ({} < {})", lastDraw->vertRange.offset + lastDraw->vertRange.size,
|
|
|
|
|
vertRange.offset);
|
|
|
|
|
if (hadIndexRange) {
|
2026-03-05 20:31:51 -07:00
|
|
|
CHECK(lastDraw->idxRange.offset + lastDraw->idxRange.size == idxRange.offset,
|
|
|
|
|
"Non-consecutive index ranges ({} < {})", lastDraw->idxRange.offset + lastDraw->idxRange.size,
|
|
|
|
|
idxRange.offset);
|
|
|
|
|
}
|
2026-06-04 23:43:13 -06:00
|
|
|
lastDraw->vertRange.size += vertRange.size;
|
|
|
|
|
if (lastDraw->idxRange.size == 0) {
|
|
|
|
|
lastDraw->idxRange = idxRange;
|
|
|
|
|
} else {
|
|
|
|
|
lastDraw->idxRange.size += idxRange.size;
|
|
|
|
|
}
|
|
|
|
|
lastDraw->vtxCount += vtxCount;
|
|
|
|
|
lastDraw->indexCount += numIndices;
|
|
|
|
|
++gfx::g_mergedDrawCallCount;
|
|
|
|
|
return;
|
2026-03-05 20:31:51 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 03:51:06 +02:00
|
|
|
handle_draw_unmerged(prim, fmt, vtxCount, vertRange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static ByteBuffer handle_draw_unmerged_idxBuf;
|
|
|
|
|
|
|
|
|
|
static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, gfx::Range vertRange) {
|
|
|
|
|
ZoneScoped;
|
|
|
|
|
u32 numIndices = 0;
|
|
|
|
|
gfx::Range idxRange;
|
2026-04-09 03:19:58 +02:00
|
|
|
|
2026-06-04 23:43:13 -06:00
|
|
|
if (prim != GX_TRIANGLES) {
|
|
|
|
|
ZoneScopedN("build idx buffer");
|
|
|
|
|
auto& realBuf = handle_draw_unmerged_idxBuf;
|
2026-04-09 03:51:06 +02:00
|
|
|
numIndices = prepare_idx_buffer(realBuf, prim, 0, vtxCount);
|
2026-06-04 23:43:13 -06:00
|
|
|
idxRange = gfx::push_indices(realBuf.data(), realBuf.size(), 4);
|
2026-04-09 03:51:06 +02:00
|
|
|
realBuf.clear();
|
2026-02-17 17:35:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build pipeline, bind groups, and push draw command
|
|
|
|
|
BindGroupRanges ranges{};
|
2026-03-18 23:58:00 -06:00
|
|
|
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
|
2026-02-17 17:35:13 -07:00
|
|
|
if (g_gxState.vtxDesc[i] != GX_INDEX8 && g_gxState.vtxDesc[i] != GX_INDEX16) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
auto& array = g_gxState.arrays[i];
|
|
|
|
|
if (array.cachedRange.size > 0) {
|
2026-03-18 23:58:00 -06:00
|
|
|
ranges.vaRanges[i - GX_VA_POS] = array.cachedRange;
|
2026-02-17 17:35:13 -07:00
|
|
|
} else {
|
2026-03-05 20:31:51 -07:00
|
|
|
const auto range = gfx::push_storage(static_cast<const uint8_t*>(array.data), array.size);
|
2026-03-18 23:58:00 -06:00
|
|
|
ranges.vaRanges[i - GX_VA_POS] = range;
|
2026-02-17 17:35:13 -07:00
|
|
|
array.cachedRange = range;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 20:31:51 -07:00
|
|
|
PipelineConfig config{};
|
2026-03-18 23:58:00 -06:00
|
|
|
populate_pipeline_config(config, prim, fmt);
|
2026-02-17 17:35:13 -07:00
|
|
|
const auto info = build_shader_info(config.shaderConfig);
|
2026-04-09 00:14:00 -06:00
|
|
|
resolve_sampled_textures(info);
|
2026-04-12 18:55:35 -06:00
|
|
|
const auto bindGroups = build_bind_groups(info);
|
2026-03-05 20:31:51 -07:00
|
|
|
const auto pipeline = gfx::pipeline_ref(config);
|
2026-02-17 17:35:13 -07:00
|
|
|
|
2026-03-29 22:33:28 -06:00
|
|
|
uint32_t instanceCount = 1;
|
|
|
|
|
if (prim == GX_LINES) {
|
|
|
|
|
instanceCount = vtxCount / 2;
|
|
|
|
|
} else if (prim == GX_LINESTRIP) {
|
|
|
|
|
instanceCount = vtxCount - 1;
|
2026-03-31 00:43:08 -06:00
|
|
|
} else if (prim == GX_POINTS) {
|
|
|
|
|
instanceCount = vtxCount;
|
2026-03-29 22:33:28 -06:00
|
|
|
}
|
2026-03-05 20:31:51 -07:00
|
|
|
gfx::push_draw_command(DrawData{
|
2026-02-17 17:35:13 -07:00
|
|
|
.pipeline = pipeline,
|
|
|
|
|
.vertRange = vertRange,
|
|
|
|
|
.idxRange = idxRange,
|
2026-04-12 18:55:35 -06:00
|
|
|
.uniformRange = build_uniform(info, vertRange.offset, ranges),
|
2026-03-05 20:31:51 -07:00
|
|
|
.vtxCount = vtxCount,
|
2026-02-17 17:35:13 -07:00
|
|
|
.indexCount = numIndices,
|
2026-03-29 22:33:28 -06:00
|
|
|
.instanceCount = instanceCount,
|
2026-02-17 17:35:13 -07:00
|
|
|
.bindGroups = bindGroups,
|
|
|
|
|
.dstAlpha = g_gxState.dstAlpha,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 01:38:35 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 06:27:16 +01:00
|
|
|
void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) {
|
2026-04-09 03:19:58 +02:00
|
|
|
ZoneScoped;
|
2026-03-06 06:27:16 +01:00
|
|
|
CHECK(pos + 2 <= size, "Aurora cmd read overrun");
|
|
|
|
|
u16 subCmd = read_u16(data + pos, bigEndian);
|
|
|
|
|
pos += 2;
|
|
|
|
|
|
|
|
|
|
// Setting of vertex array bases.
|
2026-04-18 14:10:32 -06:00
|
|
|
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)) {
|
2026-03-31 01:04:04 -06:00
|
|
|
CHECK(pos + 13 <= size, "GX_LOAD_AURORA_ARRAYBASE read overrun");
|
2026-03-06 06:27:16 +01:00
|
|
|
u32 attrIdx = subCmd - GX_LOAD_AURORA_ARRAYBASE + GX_VA_POS;
|
|
|
|
|
|
|
|
|
|
u64 arrayAddr = read_u64(data + pos, bigEndian);
|
|
|
|
|
pos += 8;
|
2026-03-31 01:04:04 -06:00
|
|
|
u32 arraySize = read_u32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
bool le = data[pos] == 1;
|
|
|
|
|
pos += 1;
|
2026-03-06 06:27:16 +01:00
|
|
|
|
2026-03-13 19:46:11 -07:00
|
|
|
auto& array = g_gxState.arrays[attrIdx];
|
|
|
|
|
const auto newData = reinterpret_cast<void*>(arrayAddr);
|
2026-03-31 01:04:04 -06:00
|
|
|
if (array.data != newData || array.size != arraySize || array.le != le) {
|
2026-03-13 19:46:11 -07:00
|
|
|
array.data = newData;
|
2026-03-31 01:04:04 -06:00
|
|
|
array.size = arraySize;
|
|
|
|
|
array.le = le;
|
2026-03-13 19:46:11 -07:00
|
|
|
// Only drop the cached upload when the backing array actually changes.
|
|
|
|
|
array.cachedRange = {};
|
|
|
|
|
g_gxState.stateDirty = true;
|
|
|
|
|
}
|
2026-04-09 00:14:00 -06:00
|
|
|
} 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];
|
|
|
|
|
slot.data = reinterpret_cast<const void*>(read_u64(data + pos, bigEndian));
|
|
|
|
|
pos += 8;
|
|
|
|
|
slot.mWidth = read_u32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
slot.mHeight = read_u32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
slot.mFormat = static_cast<GXTexFmt>(read_u32(data + pos, bigEndian));
|
|
|
|
|
pos += 4;
|
|
|
|
|
slot.tlut = static_cast<GXTlut>(read_u32(data + pos, bigEndian));
|
|
|
|
|
pos += 4;
|
|
|
|
|
if (data[pos] != 0) {
|
|
|
|
|
slot.flags |= 1u;
|
|
|
|
|
} else {
|
|
|
|
|
slot.flags &= ~1u;
|
|
|
|
|
}
|
|
|
|
|
pos += 1;
|
|
|
|
|
slot.texObjId = read_u32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
slot.texDataVersion = read_u32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
2026-04-21 14:13:13 -06:00
|
|
|
slot.set_no_cache(false); // Reset no-cache flag
|
2026-04-09 00:14:00 -06:00
|
|
|
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;
|
2026-04-21 14:13:13 -06:00
|
|
|
slot.set_no_cache(false); // Reset no-cache flag
|
2026-04-09 00:14:00 -06:00
|
|
|
g_gxState.stateDirty = true;
|
2026-05-22 13:24:56 -04:00
|
|
|
} else if (subCmd == GX2_SET_POLYGON_OFFSET) {
|
|
|
|
|
CHECK(pos + 20 <= size, "GX2_SET_POLYGON_OFFSET read overrun");
|
|
|
|
|
g_gxState.frontOffset = read_f32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
g_gxState.frontScale = read_f32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
g_gxState.backOffset = read_f32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
g_gxState.backScale = read_f32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
g_gxState.clamp = read_f32(data + pos, bigEndian);
|
|
|
|
|
pos += 4;
|
|
|
|
|
g_gxState.stateDirty = true;
|
2026-04-09 00:14:00 -06:00
|
|
|
} 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;
|
2026-04-26 14:01:24 -06:00
|
|
|
} 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;
|
2026-03-11 01:38:35 +01:00
|
|
|
} 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) {
|
|
|
|
|
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 {
|
2026-03-06 06:27:16 +01:00
|
|
|
Log.error("Unknown Aurora subcommand: {:04X}", subCmd);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 20:31:51 -07:00
|
|
|
} // namespace aurora::gx::fifo
|