Files
Claude 4407013d61 Check TEV register packing by content instead of uniform size
The test expected one extra TEV register to grow the uniform by exactly
16 bytes, but the allocation is alignment-rounded, so it failed on the
fork's baseline. Build the uniform and check that the register and the
K color after it are packed next to each other instead.

From upstream patchzyy/Wiicompiled 6f14bde (#244, KartPad batch).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wg7mB8ogCWmp9GH19Uc82B
2026-10-05 13:29:26 +00:00

6192 lines
223 KiB
C++

// GX FIFO encode/decode round-trip tests: call a GX function, capture the FIFO bytes, reset
// g_gxState, feed them to command_processor::process(), then check the decoded state.
#include "gx_test_common.hpp"
#include "gfx/efb_ram_encoder.hpp"
#include "gfx/tex_copy_format_contract.hpp"
#include "gfx/texture.hpp"
#include "gx/shader_info.hpp"
#include "gx/pipeline.hpp"
#include "scene_camera.hpp"
#include "__gx.h"
#include <algorithm>
#include <cmath>
#include <initializer_list>
#include <limits>
using aurora::gx::g_gxState;
TEST(GXPipelineConfig, RejectsInvalidDeserializedEnums) {
aurora::gx::PipelineConfig config{};
EXPECT_TRUE(aurora::gx::valid_pipeline_config(config));
config.depthFunc = static_cast<GXCompare>(0x009F01F2);
EXPECT_FALSE(aurora::gx::valid_pipeline_config(config));
}
TEST(GXPipelineConfig, FoggedLateZLogicOrPreservesEggDofMask) {
EXPECT_EQ(aurora::gx::effective_pipeline_fog_type(GX_FOG_PERSP_LIN, GX_ZT_REPLACE, false, GX_BM_LOGIC, GX_LO_OR),
GX_FOG_PERSP_LIN);
// Keep the existing defensive suppression for late-Z mask passes whose
// integer logic operation still has only an inexact WebGPU fallback.
EXPECT_EQ(aurora::gx::effective_pipeline_fog_type(GX_FOG_PERSP_LIN, GX_ZT_REPLACE, false, GX_BM_LOGIC, GX_LO_COPY),
GX_FOG_NONE);
// Logic fallbacks do not require this workaround when Z-texturing is early.
EXPECT_EQ(aurora::gx::effective_pipeline_fog_type(GX_FOG_PERSP_LIN, GX_ZT_REPLACE, true, GX_BM_LOGIC, GX_LO_COPY),
GX_FOG_PERSP_LIN);
}
TEST(GXShaderInfo, ShaderLightSanitizesNonFiniteDirectionForUniforms) {
aurora::gx::Light light{};
light.dir = {
std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
};
const auto sanitized = aurora::gx::prepare_shader_light(light);
EXPECT_FLOAT_EQ(sanitized.dir[0], 0.0f);
EXPECT_FLOAT_EQ(sanitized.dir[1], 0.0f);
EXPECT_FLOAT_EQ(sanitized.dir[2], 0.0f);
light.dir = {3.0f, 4.0f, 0.0f};
const auto normalized = aurora::gx::prepare_shader_light(light);
EXPECT_FLOAT_EQ(normalized.dir[0], 0.6f);
EXPECT_FLOAT_EQ(normalized.dir[1], 0.8f);
EXPECT_FLOAT_EQ(normalized.dir[2], 0.0f);
light.dir = {0.0f, 0.0f, 0.0f};
const auto zero = aurora::gx::prepare_shader_light(light);
EXPECT_FLOAT_EQ(zero.dir[0], 0.0f);
EXPECT_FLOAT_EQ(zero.dir[1], 0.0f);
EXPECT_FLOAT_EQ(zero.dir[2], 0.0f);
}
TEST_F(GXFifoTest, UniformRetainsViewportWindowForVrReplay) {
gxState().proj = {};
gxState().proj.m0 = {1.0f, 0.0f, 0.0f, 0.0f};
gxState().proj.m1 = {0.0f, 1.0f, 0.0f, 0.0f};
gxState().proj.m2 = {0.0f, 0.0f, -0.001f, -0.5f};
gxState().proj.m3 = {0.0f, 0.0f, 0.0f, 1.0f};
gxState().renderViewport.znear = 0.2f;
gxState().renderViewport.zfar = 0.8f;
aurora::gfx::testing::reset_uniform_allocations();
const auto info = aurora::gx::build_shader_info({});
const auto ranges = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{},
aurora::gx::FrameInterpolationDrawIdentity{}, false);
const auto& bytes = aurora::gfx::testing::uniform_allocation(ranges.current.offset);
const auto read_float = [&](size_t offset) {
float value = 0.0f;
std::memcpy(&value, bytes.data() + offset, sizeof(value));
return value;
};
EXPECT_FLOAT_EQ(read_float(24), 0.2f);
EXPECT_FLOAT_EQ(read_float(28), 0.6f);
EXPECT_EQ(ranges.replayLayout.projectionOffset, 80u);
}
TEST(GXLighting, SpotCoefficientsAndPositionGetterMatchRevolutionSdk) {
GXLightObj light{};
GXInitLightSpot(&light, 60.0f, GX_SP_SHARP);
float a0 = 0.0f;
float a1 = 0.0f;
float a2 = 0.0f;
GXGetLightAttnA(&light, &a0, &a1, &a2);
EXPECT_NEAR(a0, -3.0f, 0.00001f);
EXPECT_NEAR(a1, 8.0f, 0.00001f);
EXPECT_NEAR(a2, -4.0f, 0.00001f);
GXInitLightSpot(&light, 60.0f, GX_SP_RING1);
GXGetLightAttnA(&light, &a0, &a1, &a2);
EXPECT_NEAR(a0, -8.0f, 0.00001f);
EXPECT_NEAR(a1, 24.0f, 0.00001f);
EXPECT_NEAR(a2, -16.0f, 0.00001f);
GXInitLightPos(&light, 10.0f, 20.0f, 30.0f);
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
GXGetLightPos(&light, &x, &y, &z);
EXPECT_FLOAT_EQ(x, 10.0f);
EXPECT_FLOAT_EQ(y, 20.0f);
EXPECT_FLOAT_EQ(z, 30.0f);
}
TEST(EfbRamEncoderContract, Z24X8WritesNativeFourByFourArGbPlanes) {
std::array<u8, 4 * 4 * 4> rgba{};
for (u32 i = 0; i < 16; ++i) {
rgba[i * 4 + 0] = static_cast<u8>(0x10 + i); // high Z
rgba[i * 4 + 1] = static_cast<u8>(0x40 + i); // middle Z
rgba[i * 4 + 2] = static_cast<u8>(0x80 + i); // low Z
rgba[i * 4 + 3] = 0xff;
}
std::array<u8, 64> encoded{};
ASSERT_TRUE(aurora::gfx::efb_ram::encode(encoded.data(), encoded.size(), GX_TF_Z24X8, 4, 4, rgba.data(), 4, 4, 16,
aurora::gfx::efb_ram::HostPixelOrder::RGBA));
EXPECT_EQ(aurora::gfx::efb_ram::encoded_size(GX_TF_Z24X8, 4, 4), encoded.size());
for (u32 i = 0; i < 16; ++i) {
EXPECT_EQ(encoded[i * 2 + 0], 0xff) << "AR plane texel " << i;
EXPECT_EQ(encoded[i * 2 + 1], 0x10 + i) << "AR plane texel " << i;
EXPECT_EQ(encoded[32 + i * 2 + 0], 0x40 + i) << "GB plane texel " << i;
EXPECT_EQ(encoded[32 + i * 2 + 1], 0x80 + i) << "GB plane texel " << i;
const u32 depth = (static_cast<u32>(encoded[i * 2 + 1]) << 16) | (static_cast<u32>(encoded[32 + i * 2 + 0]) << 8) |
encoded[32 + i * 2 + 1];
EXPECT_EQ(depth, ((0x10u + i) << 16) | ((0x40u + i) << 8) | (0x80u + i));
}
}
static bool has_bp_write(const std::vector<u8>& bytes, u8 reg) {
const std::array<u8, 2> pattern{0x61, reg};
return std::search(bytes.begin(), bytes.end(), pattern.begin(), pattern.end()) != bytes.end();
}
static void expect_fog_raw_fields_match_decoded_state() {
const float expectedA = std::ldexp(g_gxState.fog.aRaw, static_cast<int>(g_gxState.fog.bShift));
const float bMant = static_cast<float>(g_gxState.fog.bMagnitude) / 8388638.0f;
const float expectedB = std::ldexp(bMant, static_cast<int>(g_gxState.fog.bShift) - 1);
EXPECT_NEAR(g_gxState.fog.a, expectedA, std::max(std::abs(g_gxState.fog.a) * 1e-3f, 1e-6f));
EXPECT_NEAR(g_gxState.fog.b, expectedB, std::max(std::abs(g_gxState.fog.b) * 1e-3f, 1e-6f));
}
static bool has_aurora_cmd(const std::vector<u8>& bytes, u16 cmd) {
const std::array<u8, 3> pattern{GX_LOAD_AURORA, static_cast<u8>(cmd >> 8), static_cast<u8>(cmd & 0xFF)};
return std::search(bytes.begin(), bytes.end(), pattern.begin(), pattern.end()) != bytes.end();
}
static std::vector<u8> bp_cmd(u8 reg, u32 value) {
return {0x61, reg, static_cast<u8>((value >> 16) & 0xFF), static_cast<u8>((value >> 8) & 0xFF),
static_cast<u8>(value & 0xFF)};
}
static std::vector<u8> cp_cmd(u8 reg, u32 value) {
return {0x08,
reg,
static_cast<u8>((value >> 24) & 0xFF),
static_cast<u8>((value >> 16) & 0xFF),
static_cast<u8>((value >> 8) & 0xFF),
static_cast<u8>(value & 0xFF)};
}
static std::vector<u8> xf_cmd(u16 addr, std::initializer_list<u32> values) {
std::vector<u8> bytes;
bytes.reserve(5 + values.size() * 4);
bytes.push_back(0x10);
const u32 header = ((static_cast<u32>(values.size() - 1) & 0xFFFFu) << 16) | addr;
bytes.push_back(static_cast<u8>((header >> 24) & 0xFF));
bytes.push_back(static_cast<u8>((header >> 16) & 0xFF));
bytes.push_back(static_cast<u8>((header >> 8) & 0xFF));
bytes.push_back(static_cast<u8>(header & 0xFF));
for (const u32 value : values) {
bytes.push_back(static_cast<u8>((value >> 24) & 0xFF));
bytes.push_back(static_cast<u8>((value >> 16) & 0xFF));
bytes.push_back(static_cast<u8>((value >> 8) & 0xFF));
bytes.push_back(static_cast<u8>(value & 0xFF));
}
return bytes;
}
static u32 read_be32_at(const std::vector<u8>& bytes, size_t offset) {
return (static_cast<u32>(bytes[offset]) << 24) | (static_cast<u32>(bytes[offset + 1]) << 16) |
(static_cast<u32>(bytes[offset + 2]) << 8) | static_cast<u32>(bytes[offset + 3]);
}
TEST_F(GXFifoTest, VrKeepsMatchedEndpointsWithDesktopInterpolationOff) {
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
gxState().currentPnMtx = 0;
gxState().pnMtx[0].pos = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, -50}};
gxState().pnMtx[0].nrm = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
const auto build = [&](float x, aurora::HashType identity, bool split = false) {
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
gxState().pnMtx[0].pos.m0[3] = x;
if (split)
aurora::gx::mark_frame_interpolation_replay_unsafe();
const auto result = aurora::gx::build_uniform(info, 0, {}, {identity, identity, 7}, true);
aurora::gx::finalize_frame_interpolation();
return result;
};
EXPECT_EQ(build(10, 100).previous.size, 0u); // Warm-up.
auto uniforms = build(20, 100);
ASSERT_NE(uniforms.previous.size, 0u);
const auto readX = [&](aurora::gfx::Range range) {
const auto& bytes = aurora::gfx::testing::uniform_allocation(range.offset);
float x;
std::memcpy(&x, bytes.data() + uniforms.replayLayout.positionOffset + 3 * sizeof(float), sizeof(x));
return x;
};
EXPECT_FLOAT_EQ(readX(uniforms.previous), 10);
EXPECT_FLOAT_EQ(readX(uniforms.current), 20);
EXPECT_EQ(aurora::gx::interpolated_frame_count(), 0u); // Desktop remains off.
EXPECT_TRUE(std::all_of(uniforms.interpolated.begin(), uniforms.interpolated.end(),
[](auto range) { return range.size == 0; }));
EXPECT_EQ(build(30, 200).previous.size, 0u); // Unmatched draws use current transforms.
EXPECT_EQ(build(40, 200, true).previous.size, 0u); // Readback split invalidates replay.
}
TEST_F(GXFifoTest, VrDiagnosticsExposeDistantCameraTurnRejectionDespiteMatchedIdentity) {
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
gxState().currentPnMtx = 0;
const auto build = [&](float yaw) {
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
const float c = std::cos(yaw), s = std::sin(yaw);
gxState().pnMtx[0].nrm = {{c, 0, s, 0}, {0, 1, 0, 0}, {-s, 0, c, 0}};
for (unsigned i = 0; i < 2; ++i) {
const float distance = i == 0 ? 1000.0f : 100000.0f;
// Two stationary objects, seen from one camera rotating by two degrees.
gxState().pnMtx[0].pos = {{c, 0, s, -s * distance}, {0, 1, 0, 0}, {-s, 0, c, -c * distance}};
aurora::gx::build_uniform(info, 0, {}, {100u + i, 100u + i, 7}, true);
}
aurora::gx::finalize_frame_interpolation();
};
build(0);
build(2.0f * 3.14159265f / 180.0f);
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.candidates, 2u);
EXPECT_EQ(diagnostics.matches, 2u);
EXPECT_EQ(diagnostics.preparedDraws, 1u);
EXPECT_EQ(diagnostics.rejectedDraws, 1u);
EXPECT_EQ(diagnostics.framesSealed, 2u); // VR-only frames must count too.
}
TEST_F(GXFifoTest, VrRetainsFastSpinningWheelEndpoints) {
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
gxState().currentPnMtx = 0;
const auto build = [&](float angle, float x) {
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
const float c = std::cos(angle), s = std::sin(angle);
gxState().pnMtx[0].pos = {{c, -s, 0, x}, {s, c, 0, 0}, {0, 0, 1, -50}};
gxState().pnMtx[0].nrm = {{c, -s, 0, 0}, {s, c, 0, 0}, {0, 0, 1, 0}};
const auto result = aurora::gx::build_uniform(info, 0, {}, {100, 42, 7}, true);
aurora::gx::finalize_frame_interpolation();
return result;
};
EXPECT_EQ(build(0, 10).previous.size, 0u);
const auto uniforms = build(2.0f * 3.14159265f / 3.0f, 30);
ASSERT_NE(uniforms.previous.size, 0u);
const auto& bytes = aurora::gfx::testing::uniform_allocation(uniforms.previous.offset);
aurora::Mat3x4<float> previousPosition{}, previousNormal{};
std::memcpy(static_cast<void*>(&previousPosition), bytes.data() + uniforms.replayLayout.positionOffset,
sizeof(previousPosition));
std::memcpy(static_cast<void*>(&previousNormal), bytes.data() + uniforms.replayLayout.normalOffset,
sizeof(previousNormal));
// Rejecting a >90-degree wheel spin used to copy the current position here too,
// leaving the entire wheel at 60 Hz even as the kart body moved smoothly.
EXPECT_FLOAT_EQ(previousPosition.m0.w(), 10);
EXPECT_FLOAT_EQ(previousPosition.m0.x(), 1);
EXPECT_FLOAT_EQ(previousNormal.m0.x(), 1);
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.matches, 1u);
EXPECT_EQ(diagnostics.preparedDraws, 1u);
EXPECT_EQ(diagnostics.rejectedDraws, 0u);
}
TEST_F(GXFifoTest, VrTextureAnimationRetainsSpatialHistoryWithStrictMeshIdentity) {
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
gxState().currentPnMtx = 0;
gxState().pnMtx[0].pos = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, -50}};
gxState().pnMtx[0].nrm = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
const auto begin = [&] {
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
};
const auto draw = [&](float x, aurora::HashType texture, aurora::HashType geometry = 123,
aurora::HashType topology = 0, aurora::HashType pipeline = 42) {
gxState().pnMtx[0].pos.m0[3] = x;
const aurora::gx::FrameInterpolationDrawIdentity identity{
.combined = texture + 1000, .pipeline = pipeline, .texture = texture,
.matrixTopology = topology, .geometry = geometry};
return aurora::gx::build_uniform(info, 0, {}, identity, true);
};
const auto previousX = [&](const auto& uniforms) {
const auto& bytes = aurora::gfx::testing::uniform_allocation(uniforms.previous.offset);
float x;
std::memcpy(&x, bytes.data() + uniforms.replayLayout.positionOffset + 3 * sizeof(float), sizeof(x));
return x;
};
begin();
draw(10, 1);
draw(100, 2);
draw(200, 3);
aurora::gx::finalize_frame_interpolation();
begin();
// The transparent sort reverses two animated instances. An unchanged exact
// match must also keep priority over the texture-independent mesh fallback.
const auto right = draw(110, 4);
const auto left = draw(20, 4);
const auto unchanged = draw(210, 3);
aurora::gx::finalize_frame_interpolation();
ASSERT_NE(right.previous.size, 0u);
ASSERT_NE(left.previous.size, 0u);
ASSERT_NE(unchanged.previous.size, 0u);
EXPECT_FLOAT_EQ(previousX(right), 100);
EXPECT_FLOAT_EQ(previousX(left), 10);
EXPECT_FLOAT_EQ(previousX(unchanged), 200);
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.preparedDraws, 3u);
EXPECT_EQ(diagnostics.rejectedDraws, 0u);
begin();
EXPECT_EQ(draw(120, 5, 456).previous.size, 0u); // Different mesh.
EXPECT_EQ(draw(120, 6, 0).previous.size, 0u); // Missing mesh identity.
EXPECT_EQ(draw(120, 7, 123, 9).previous.size, 0u); // Different palette topology.
EXPECT_EQ(draw(120, 8, 123, 0, 43).previous.size, 0u); // Different pipeline.
aurora::gx::finalize_frame_interpolation();
}
TEST(FrameInterpolationContract, FastRigidSpinsKeepAngularSpeedAndDiscontinuityGuards) {
const aurora::Mat3x4<float> previous{{1, 0, 0, 10}, {0, 1, 0, 0}, {0, 0, 1, 0}};
// Include rotations whose quaternion representation needs hemisphere correction.
for (float degrees : {120.0f, 170.0f, -120.0f, 240.0f}) {
const float angle = degrees * 3.14159265f / 180.0f;
const float c = std::cos(angle), s = std::sin(angle);
const aurora::Mat3x4<float> current{{c, -s, 0, 30}, {s, c, 0, 0}, {0, 0, 1, 0}};
aurora::Mat3x4<float> output{};
EXPECT_FALSE(aurora::gx::interpolate_transform(previous, current, 0.5f, output)); // Anchor cut guard.
for (float weight : {0.0f, 1.0f / 3, 2.0f / 3, 1.0f}) {
ASSERT_TRUE(aurora::gx::interpolate_draw_transform(previous, current, weight, output));
const float expectedAngle = (degrees > 180 ? degrees - 360 : degrees) * 3.14159265f / 180.0f * weight;
EXPECT_NEAR(output.m0.x(), std::cos(expectedAngle), 1e-5f);
EXPECT_NEAR(output.m1.x(), std::sin(expectedAngle), 1e-5f);
EXPECT_NEAR(output.m0.w(), 10 + 20 * weight, 1e-5f);
EXPECT_NEAR(output.m0.x() * output.m0.x() + output.m1.x() * output.m1.x(), 1, 1e-5f);
}
}
aurora::Mat3x4<float> invalid = previous, output{};
invalid.m0[3] = 2010;
EXPECT_FALSE(aurora::gx::interpolate_draw_transform(previous, invalid, 0.5f, output));
EXPECT_FLOAT_EQ(output.m0.w(), 2010);
invalid = previous;
invalid.m0[0] = 0; // Singular axis.
EXPECT_FALSE(aurora::gx::interpolate_draw_transform(previous, invalid, 0.5f, output));
invalid = previous;
invalid.m0[3] = std::numeric_limits<float>::quiet_NaN();
EXPECT_FALSE(aurora::gx::interpolate_draw_transform(previous, invalid, 0.5f, output));
}
TEST(FrameInterpolationContract, ShearedRigidDrawsMoveWithTheSkinnedMeshOnTheirBone) {
// Lakitu::Movement::UpdateScale sways MKW's Lakitu by tilting his Y axis: the model
// matrix gets a Y column of (A cos p, 1, A sin p). His goggles are rigid on the face
// bone and his head is skinned to that same bone. The rigid path rejected the shear,
// holding the goggles at the game frame while the head moved on, so they sank in.
const auto swaying = [](float amplitude, float phase, float lift) {
const float c = std::cos(0.698f), s = std::sin(0.698f); // the face bone's roll
const float x = amplitude * std::cos(phase), z = amplitude * std::sin(phase);
// [[1, x, 0], [0, 1, 0], [0, z, 1]] * Rz, then placed in front of the camera.
return aurora::Mat3x4<float>{{c + x * s, -s + x * c, 0, 10}, {s, c, 0, 50 + lift}, {z * s, z * c, 1, -300}};
};
const auto previous = swaying(0.25f, 0.3f, 0), current = swaying(0.3f, 0.5f, 3);
const auto apply = [](const aurora::Mat3x4<float>& matrix, const std::array<float, 3>& point) {
const aurora::Vec4<float>* rows[] = {&matrix.m0, &matrix.m1, &matrix.m2};
std::array<float, 3> result{};
for (size_t row = 0; row < 3; ++row)
result[row] = (*rows[row])[0] * point[0] + (*rows[row])[1] * point[1] + (*rows[row])[2] * point[2] +
(*rows[row])[3];
return result;
};
const std::array<float, 3> goggleCorner{30, -20, 10};
for (float weight : {0.0f, 0.25f, 0.5f, 0.75f, 1.0f}) {
aurora::Mat3x4<float> head{}, goggles{};
ASSERT_TRUE(aurora::gx::interpolate_indexed_transform(previous, current, weight, head));
ASSERT_TRUE(aurora::gx::interpolate_draw_transform(previous, current, weight, goggles));
const auto onHead = apply(head, goggleCorner), onGoggles = apply(goggles, goggleCorner);
for (size_t axis = 0; axis < 3; ++axis) EXPECT_NEAR(onGoggles[axis], onHead[axis], 0.05f) << weight;
}
// Camera and seat anchors are rigid; a sheared one still counts as a cut.
aurora::Mat3x4<float> anchor{};
EXPECT_FALSE(aurora::gx::interpolate_transform(previous, current, 0.5f, anchor));
}
TEST_F(GXFifoTest, VrCameraRebaseKeepsFarInstancesAndHeldParticlesContinuous) {
using aurora::Mat3x4;
using aurora::gfx::stereo_replay::compose_affine;
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::set_frame_interpolation_fps(120); // Desktop and VR enabled together.
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
gxState().currentPnMtx = 0;
const Mat3x4<float> identity{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
const float angle = 0.08f, c = std::cos(angle), s = std::sin(angle);
const Mat3x4<float> currentView{{c, 0, s, 0}, {0, 1, 0, 0}, {-s, 0, c, 0}};
aurora::stereo::SceneCameraMotion motion;
ASSERT_TRUE(motion.prepare(identity, currentView, identity, identity));
const auto draw = [&](const Mat3x4<float>& view, float x, bool particle = false) {
gxState().vtxDesc[GX_VA_POS] = particle ? GX_DIRECT : GX_INDEX16;
gxState().pnMtx[0].pos = particle ? identity : compose_affine(view, {{1, 0, 0, x}, {0, 1, 0, 0}, {0, 0, 1, -100000}});
if (particle) gxState().pnMtx[0].pos.m0[1] = -0.0f;
gxState().pnMtx[0].nrm = particle ? identity : view;
return aurora::gx::build_uniform(info, 0, {}, particle ? aurora::gx::FrameInterpolationDrawIdentity{200, 20, 1}
: aurora::gx::FrameInterpolationDrawIdentity{100, 10, 1}, true);
};
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
draw(identity, -2000);
draw(identity, 2000);
draw(identity, 0, true);
aurora::gx::finalize_frame_interpolation();
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
// All far instances move over the old 1500-unit gate just from camera yaw.
// Submission order also changes, and the right instance moves 30 world units.
const auto right = draw(currentView, 2030);
const auto left = draw(currentView, -2000);
const auto particle = draw(currentView, 0, true);
aurora::gx::set_frame_interpolation_view_rebase(&motion.currentFromPrevious, &motion.previousFromCurrent);
aurora::gx::finalize_frame_interpolation();
const auto read = [&](const auto& uniform, aurora::gfx::Range range) {
Mat3x4<float> matrix;
const auto& bytes = aurora::gfx::testing::uniform_allocation(range.offset);
std::memcpy(static_cast<void*>(&matrix), bytes.data() + uniform.replayLayout.positionOffset, sizeof(matrix));
return matrix;
};
ASSERT_NE(right.previous.size, 0u);
ASSERT_NE(left.previous.size, 0u);
ASSERT_NE(particle.previous.size, 0u);
const auto previousRight = read(right, right.previous);
const auto expectedRight = compose_affine(currentView, {{1, 0, 0, 2000}, {0, 1, 0, 0}, {0, 0, 1, -100000}});
EXPECT_NEAR(previousRight.m0.w(), expectedRight.m0.w(), 0.01f);
EXPECT_NEAR(previousRight.m2.w(), expectedRight.m2.w(), 0.01f);
EXPECT_NEAR(read(left, left.previous).m0.w(), read(left, left.current).m0.w(), 0.01f);
EXPECT_FLOAT_EQ(read(particle, particle.previous).m0.x(), 1);
EXPECT_FLOAT_EQ(read(particle, particle.previous).m0.w(), 0); // No camera applied twice to baked vertices.
// Desktop retains its original guarded camera-space behavior.
ASSERT_NE(right.interpolated[0].size, 0u);
EXPECT_NEAR(read(right, right.interpolated[0]).m0.w(), read(right, right.current).m0.w(), 0.01f);
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.matches, 3u);
EXPECT_EQ(diagnostics.preparedDraws, 3u);
EXPECT_EQ(diagnostics.rejectedDraws, 0u);
}
TEST_F(GXFifoTest, VrParticleCentersFollowMotionAcrossSortChangesAndCameraTurns) {
using aurora::Mat3x4;
using aurora::gx::offset_transform_origin;
using aurora::gfx::stereo_replay::compose_affine;
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::set_frame_interpolation_fps(120);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
const Mat3x4<float> identity{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
const float angle = 0.08f, c = std::cos(angle), s = std::sin(angle);
const Mat3x4<float> view{{c, 0, s, 0}, {0, 1, 0, 0}, {-s, 0, c, 0}};
aurora::stereo::SceneCameraMotion motion;
ASSERT_TRUE(motion.prepare(identity, view, identity, identity));
gxState().vtxDesc[GX_VA_POS] = GX_DIRECT;
gxState().pnMtx[0].pos = gxState().pnMtx[0].nrm = identity;
const auto center = [&](const Mat3x4<float>& camera, float x) {
const auto matrix = offset_transform_origin(camera, {x, 0, -5000});
return std::array<float, 3>{matrix.m0.w(), matrix.m1.w(), matrix.m2.w()};
};
const auto record = [&](const Mat3x4<float>& camera, float x, uint64_t geometry) {
return aurora::gx::build_uniform(info, 0, {}, {geometry, 7, 9, 0, geometry}, true, 1,
{center(camera, x), true});
};
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
record(identity, -600, 101);
record(identity, 600, 102);
aurora::gx::finalize_frame_interpolation();
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
// Vertex bytes change and transparent submission order reverses. Both draw
// matrices are identity, so matrix-only matching cannot identify the centers.
const auto right = record(view, 630, 201);
const auto left = record(view, -570, 202);
const auto newborn = record(view, 10000, 203);
aurora::gx::set_frame_interpolation_view_rebase(&motion.currentFromPrevious, &motion.previousFromCurrent);
aurora::gx::finalize_frame_interpolation();
const auto readCenter = [&](const auto& uniform, aurora::gfx::Range range) {
Mat3x4<float> matrix;
const auto& bytes = aurora::gfx::testing::uniform_allocation(range.offset);
std::memcpy(&matrix, bytes.data() + uniform.replayLayout.positionOffset, sizeof(matrix));
EXPECT_FLOAT_EQ(matrix.m0.x(), 1); // Keep the current billboard's orientation.
EXPECT_FLOAT_EQ(matrix.m2.x(), 0);
return offset_transform_origin(matrix, uniform.replayLayout.vertexMotion.center);
};
for (const auto& [uniform, x] : {std::pair{right, 600.f}, std::pair{left, -600.f}}) {
ASSERT_NE(uniform.previous.size, 0u);
const auto previous = readCenter(uniform, uniform.previous);
EXPECT_NEAR(previous.m0.w(), center(view, x)[0], 0.002f);
EXPECT_NEAR(previous.m2.w(), center(view, x)[2], 0.002f);
const auto current = readCenter(uniform, uniform.current);
Mat3x4<float> half;
ASSERT_TRUE(aurora::gx::interpolate_draw_transform(previous, current, 0.5f, half));
EXPECT_NEAR(half.m0.w(), center(view, x + 15)[0], 0.002f);
// Simultaneous desktop interpolation uses its original camera endpoint.
const auto desktop = readCenter(uniform, uniform.interpolated[0]);
EXPECT_NEAR(desktop.m0.w(), (center(identity, x)[0] + center(view, x + 30)[0]) * 0.5f, 0.002f);
}
EXPECT_NEAR(readCenter(newborn, newborn.previous).m0.w(), center(view, 10000)[0], 0.002f);
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.vertexMotionDraws, 2u);
}
namespace {
using Vec3f = std::array<float, 3>;
// One CPU-built particle quad the way nw4r::ef submits it: camera-space corners under
// an identity position matrix, all quads of an emitter sharing pipeline and texture.
aurora::gx::UniformRanges record_particle_quad(const aurora::gx::ShaderInfo& info, uint64_t geometry,
const Vec3f& center, const Vec3f& edge0, const Vec3f& edge1) {
return aurora::gx::build_uniform(info, 0, {}, {geometry, 7, 9, 0, geometry}, true, 1, {center, true},
{edge0, edge1});
}
// The centre a staged uniform draws the quad at.
Vec3f drawn_center(const aurora::gx::UniformRanges& uniform, aurora::gfx::Range range) {
aurora::Mat3x4<float> matrix;
const auto& bytes = aurora::gfx::testing::uniform_allocation(range.offset);
std::memcpy(static_cast<void*>(&matrix), bytes.data() + uniform.replayLayout.positionOffset, sizeof(matrix));
matrix = aurora::gx::offset_transform_origin(matrix, uniform.replayLayout.vertexMotion.center);
return {matrix.m0.w(), matrix.m1.w(), matrix.m2.w()};
}
Vec3f transform_point(const aurora::Mat3x4<float>& matrix, const Vec3f& point) {
return {matrix.m0[0] * point[0] + matrix.m0[1] * point[1] + matrix.m0[2] * point[2] + matrix.m0[3],
matrix.m1[0] * point[0] + matrix.m1[1] * point[1] + matrix.m1[2] * point[2] + matrix.m1[3],
matrix.m2[0] * point[0] + matrix.m2[1] * point[1] + matrix.m2[2] * point[2] + matrix.m2[3]};
}
bool same_point(const Vec3f& a, const Vec3f& b, float tolerance = 0.05f) {
return std::abs(a[0] - b[0]) <= tolerance && std::abs(a[1] - b[1]) <= tolerance &&
std::abs(a[2] - b[2]) <= tolerance;
}
struct ParticleInterpolationReset {
~ParticleInterpolationReset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
}
};
} // namespace
TEST_F(GXFifoTest, VrParticleQuadsPairByShapeWhereNearestCentresSwap) {
ParticleInterpolationReset reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
const aurora::Mat3x4<float> identity{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
gxState().currentPnMtx = 0;
gxState().vtxDesc[GX_VA_POS] = GX_DIRECT;
gxState().pnMtx[0].pos = gxState().pnMtx[0].nrm = identity;
// The camera flies 100 units forward and carries two streaks, each moving 60 along its
// own length. The one lying across the view ends nearer the other's old centre, so
// pairing centres alone swaps them and both sweep sideways.
const aurora::Mat3x4<float> forward{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 100}};
aurora::stereo::SceneCameraMotion motion;
ASSERT_TRUE(motion.prepare(identity, forward, identity, identity));
const Vec3f across{300, 0, 0}, upright{0, 300, 0}, thinX{12, 0, 0}, thinY{0, 12, 0};
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
record_particle_quad(info, 1, {-20, 0, -500}, across, thinY);
record_particle_quad(info, 2, {20, 0, -500}, upright, thinX);
aurora::gx::finalize_frame_interpolation();
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
const auto lying = record_particle_quad(info, 3, {40, 0, -500}, across, thinY);
const auto standing = record_particle_quad(info, 4, {20, 60, -500}, upright, thinX);
aurora::gx::set_frame_interpolation_view_rebase(&motion.currentFromPrevious, &motion.previousFromCurrent);
aurora::gx::finalize_frame_interpolation();
ASSERT_NE(lying.previous.size, 0u);
ASSERT_NE(standing.previous.size, 0u);
// Each starts from its own old place, as the new camera position sees it.
EXPECT_TRUE(same_point(drawn_center(lying, lying.previous), {-20, 0, -400}));
EXPECT_TRUE(same_point(drawn_center(standing, standing.previous), {20, 0, -400}));
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.vertexMotionDraws, 2u);
EXPECT_EQ(diagnostics.vertexMotionHeld, 0u);
}
TEST_F(GXFifoTest, VrParticleQuadBornElsewhereDoesNotSweepFromOneThatDied) {
ParticleInterpolationReset reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
const aurora::Mat3x4<float> identity{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
gxState().currentPnMtx = 0;
gxState().vtxDesc[GX_VA_POS] = GX_DIRECT;
gxState().pnMtx[0].pos = gxState().pnMtx[0].nrm = identity;
const aurora::Mat3x4<float> forward{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 100}};
aurora::stereo::SceneCameraMotion motion;
ASSERT_TRUE(motion.prepare(identity, forward, identity, identity));
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
record_particle_quad(info, 1, {-100, 0, -500}, {300, 0, 0}, {0, 12, 0});
aurora::gx::finalize_frame_interpolation();
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
// That streak died; another one starts elsewhere. One quad on each side of the frame
// is no evidence that they are the same particle.
const auto newborn = record_particle_quad(info, 2, {100, 0, -500}, {0, 300, 0}, {12, 0, 0});
aurora::gx::set_frame_interpolation_view_rebase(&motion.currentFromPrevious, &motion.previousFromCurrent);
aurora::gx::finalize_frame_interpolation();
ASSERT_NE(newborn.previous.size, 0u);
EXPECT_TRUE(same_point(drawn_center(newborn, newborn.previous), {100, 0, -500}));
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.vertexMotionDraws, 0u);
EXPECT_EQ(diagnostics.vertexMotionHeld, 1u);
}
TEST_F(GXFifoTest, VrSpeedLineEmitterKeepsEachStreakOnItsOwnPath) {
// A deterministic stand-in for the boost speed lines (rk_koukasen in RKRace.breff):
// 12x300 streaks, each drawn as two crossed quads, two born per frame on a 90-unit
// ring, six-frame life, about 50 units per frame outwards and back, all carried by a
// camera that flies 100 units per frame while turning. Pairing nearest centres swapped
// about half of them and swept every newborn in from a streak that had just died.
ParticleInterpolationReset reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
const aurora::Mat3x4<float> identity{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
gxState().currentPnMtx = 0;
gxState().vtxDesc[GX_VA_POS] = GX_DIRECT;
gxState().pnMtx[0].pos = gxState().pnMtx[0].nrm = identity;
struct Streak {
uint32_t id, birth;
Vec3f origin, velocity;
};
struct Quad {
uint32_t streak, side;
Vec3f center, edge0, edge1;
};
uint32_t seed = 0x5eed1234u;
const auto random = [&seed] {
seed = seed * 1664525u + 1013904223u;
return static_cast<float>(seed >> 8) * (1.0f / 16777216.0f);
};
const auto quads_of = [](const Streak& streak, uint32_t frame) {
const float age = static_cast<float>(frame - streak.birth);
const auto& v = streak.velocity;
const float speed = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
const Vec3f axis{v[0] / speed, v[1] / speed, v[2] / speed};
// Two sides across the axis: the crossed planes of one streak.
const float across = std::sqrt(axis[0] * axis[0] + axis[2] * axis[2]);
const Vec3f side0{axis[2] / across, 0, -axis[0] / across};
const Vec3f side1{axis[1] * side0[2] - axis[2] * side0[1], axis[2] * side0[0] - axis[0] * side0[2],
axis[0] * side0[1] - axis[1] * side0[0]};
Vec3f center{};
for (size_t i = 0; i < 3; ++i) center[i] = streak.origin[i] + v[i] * age - axis[i] * 150;
std::array<Quad, 2> quads{};
for (uint32_t side = 0; side < 2; ++side) {
const auto& s = side == 0 ? side0 : side1;
quads[side] = {streak.id, side, center, {axis[0] * 300, axis[1] * 300, axis[2] * 300},
{s[0] * 12, s[1] * 12, s[2] * 12}};
}
return quads;
};
std::vector<Streak> streaks;
std::vector<Quad> previousQuads;
aurora::Mat3x4<float> previousView = identity;
Vec3f cameraPosition{};
float yaw = 0;
uint32_t nextId = 0, survivors = 0, moved = 0, held = 0, wrong = 0, newborns = 0, newbornsMoved = 0;
for (uint32_t frame = 0; frame < 90; ++frame) {
yaw += 0.01f;
const float c = std::cos(yaw), s = std::sin(yaw);
cameraPosition = {cameraPosition[0] - s * 100, 0, cameraPosition[2] - c * 100};
// Camera looks down -Z, rotated by yaw about +Y; the view is its inverse.
const aurora::Mat3x4<float> view{
{c, 0, -s, -(c * cameraPosition[0] - s * cameraPosition[2])},
{0, 1, 0, 0},
{s, 0, c, -(s * cameraPosition[0] + c * cameraPosition[2])}};
aurora::stereo::SceneCameraMotion motion;
const bool rebase = frame != 0 && motion.prepare(previousView, view, identity, identity);
ASSERT_TRUE(frame == 0 || rebase);
std::erase_if(streaks, [frame](const Streak& streak) { return frame - streak.birth >= 6; });
for (int born = 0; born < 2; ++born) {
const float angle = random() * 6.2831853f;
const float speed = 1.0f + (random() - 0.5f) * 0.46f;
streaks.push_back({nextId++, frame, {90 * std::cos(angle), 90 * std::sin(angle), -300},
{40 * speed * std::cos(angle), 40 * speed * std::sin(angle) + 10 * speed, 30 * speed}});
}
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
std::vector<Quad> quads;
std::vector<aurora::gx::UniformRanges> uniforms;
for (const auto& streak : streaks) {
for (const auto& quad : quads_of(streak, frame)) {
quads.push_back(quad);
uniforms.push_back(record_particle_quad(info, 1000 + quads.size() + frame * 100, quad.center,
quad.edge0, quad.edge1));
}
}
if (rebase)
aurora::gx::set_frame_interpolation_view_rebase(&motion.currentFromPrevious, &motion.previousFromCurrent);
aurora::gx::finalize_frame_interpolation();
if (rebase) {
for (size_t index = 0; index < quads.size(); ++index) {
const auto& quad = quads[index];
if (uniforms[index].previous.size == 0) continue;
const auto start = drawn_center(uniforms[index], uniforms[index].previous);
const auto before = std::find_if(previousQuads.begin(), previousQuads.end(), [&](const Quad& old) {
return old.streak == quad.streak && old.side == quad.side;
});
const bool heldHere = same_point(start, quad.center) ||
same_point(start, transform_point(motion.currentFromPrevious, quad.center));
if (before == previousQuads.end()) {
++newborns;
newbornsMoved += !heldHere;
continue;
}
++survivors;
if (heldHere) {
++held;
} else if (same_point(start, transform_point(motion.currentFromPrevious, before->center))) {
++moved;
} else {
++wrong;
}
}
}
previousQuads = std::move(quads);
previousView = view;
}
ASSERT_GT(survivors, 800u);
// Most streak quads keep moving along their own path...
EXPECT_GT(moved, survivors / 2);
// ...and almost none borrows another streak's (it was about half, plus every newborn).
EXPECT_LE(wrong * 50, survivors);
EXPECT_LE(newbornsMoved * 20, newborns);
std::printf("speed lines: %u survivor quads: %u moved, %u held, %u wrong; %u of %u newborn quads moved\n",
survivors, moved, held, wrong, newbornsMoved, newborns);
}
TEST_F(GXFifoTest, VrCyclicRigidMotionDoesNotBlendBackwardsAcrossReset) {
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
const auto info = aurora::gx::build_shader_info({});
const aurora::Mat3x4<float> identity{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
gxState().vtxDesc[GX_VA_POS] = GX_INDEX16;
auto previousView = identity;
const auto record = [&](float phase, float yaw) {
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
const aurora::Mat3x4<float> view{{std::cos(yaw), 0, std::sin(yaw), 0}, {0, 1, 0, 0},
{-std::sin(yaw), 0, std::cos(yaw), 0}};
aurora::stereo::SceneCameraMotion camera;
EXPECT_TRUE(camera.prepare(previousView, view, identity, identity));
gxState().pnMtx[0].pos = aurora::gx::offset_transform_origin(view, {phase, 0, -5000});
gxState().pnMtx[0].nrm = view;
const auto result = aurora::gx::build_uniform(info, 0, {}, {11, 1, 2, 0, 11}, true);
aurora::gx::set_frame_interpolation_view_rebase(&camera.currentFromPrevious, &camera.previousFromCurrent);
aurora::gx::finalize_frame_interpolation();
previousView = view;
return result;
};
record(16, 0);
record(17, 0.02f);
record(18, 0.04f);
record(19, 0.06f);
AuroraFrameInterpolationDiagnostics before{}, after{};
aurora::gx::get_frame_interpolation_diagnostics(before);
const auto resetFrame = record(0, 0.08f);
aurora::gx::get_frame_interpolation_diagnostics(after);
EXPECT_EQ(after.animationWrapCuts, before.animationWrapCuts + 1);
const auto& current = aurora::gfx::testing::uniform_allocation(resetFrame.current.offset);
const auto& previous = aurora::gfx::testing::uniform_allocation(resetFrame.previous.offset);
EXPECT_EQ(current, previous); // Hold the new phase instead of reverse sweeping.
record(1, 0.10f);
record(0, 0.12f); // A normal same-speed direction change must still interpolate.
aurora::gx::get_frame_interpolation_diagnostics(after);
EXPECT_EQ(after.animationWrapCuts, before.animationWrapCuts + 1);
EXPECT_EQ(after.preparedDraws, 1u);
}
TEST(FrameInterpolationContract, RequiresStablePerspectiveDrawSequence) {
const auto resetInterpolation = [] {
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
};
const auto buildFrame = [](const aurora::gx::ShaderInfo& info, aurora::HashType signatureBase, bool perspective,
bool reverse = false) {
aurora::gx::begin_frame_interpolation();
aurora::gx::BindGroupRanges ranges{};
uint32_t interpolatedUniforms = 0;
for (uint32_t i = 0; i < 8; ++i) {
const uint32_t signatureOffset = reverse ? 7 - i : i;
const aurora::gx::FrameInterpolationDrawIdentity identity{
.combined = signatureBase + signatureOffset,
.pipeline = signatureBase,
.texture = 1,
};
const auto uniforms = aurora::gx::build_uniform(info, 0, ranges, identity, perspective);
interpolatedUniforms +=
static_cast<uint32_t>(std::count_if(uniforms.interpolated.begin(), uniforms.interpolated.end(),
[](const aurora::gfx::Range& range) { return range.size != 0; }));
}
aurora::gx::finalize_frame_interpolation();
return interpolatedUniforms;
};
resetInterpolation();
aurora::gx::set_frame_interpolation_fps(120);
const auto info = aurora::gx::build_shader_info({});
// Slots are inserted whenever interpolation is configured, so a frame with no matches just fills
// them with duplicates. The staged uniform counts below are what verify the matching.
EXPECT_EQ(buildFrame(info, 100, true), 0u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
EXPECT_EQ(buildFrame(info, 100, true), 8u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
EXPECT_EQ(aurora::gx::interpolated_frame_count(), 1u);
EXPECT_TRUE(aurora::gx::frame_interpolation_replay_safe());
EXPECT_EQ(buildFrame(info, 100, true, true), 8u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
aurora::gx::mark_frame_interpolation_replay_unsafe();
EXPECT_FALSE(aurora::gx::frame_interpolation_replay_safe());
EXPECT_EQ(aurora::gx::interpolated_frame_count(), 1u);
aurora::gx::set_frame_interpolation_fps(180);
EXPECT_EQ(buildFrame(info, 250, true), 0u);
EXPECT_TRUE(aurora::gx::frame_interpolation_replay_safe());
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
EXPECT_EQ(buildFrame(info, 250, true), 16u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
EXPECT_EQ(aurora::gx::interpolated_frame_count(), 2u);
aurora::gx::set_frame_interpolation_fps(120);
EXPECT_EQ(buildFrame(info, 200, true), 0u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
EXPECT_EQ(buildFrame(info, 200, false), 0u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
aurora::gx::set_frame_interpolation_fps(240);
EXPECT_EQ(buildFrame(info, 300, true), 0u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
EXPECT_EQ(buildFrame(info, 300, true), 24u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
EXPECT_EQ(aurora::gx::interpolated_frame_count(), 3u);
// CPU-deformed draws change their vertex bytes every frame, so their stable pipeline and texture
// identity still has to retain transform history for the camera and model matrices.
aurora::gx::set_frame_interpolation_fps(120);
const auto buildDeformedFrame = [&](aurora::HashType geometryEpoch) {
aurora::gx::begin_frame_interpolation();
aurora::gx::BindGroupRanges ranges{};
uint32_t interpolatedUniforms = 0;
for (uint32_t i = 0; i < 8; ++i) {
const aurora::gx::FrameInterpolationDrawIdentity identity{
.combined = geometryEpoch + i,
.pipeline = 42,
.texture = 7,
};
const auto uniforms = aurora::gx::build_uniform(info, 0, ranges, identity, true);
interpolatedUniforms +=
static_cast<uint32_t>(std::count_if(uniforms.interpolated.begin(), uniforms.interpolated.end(),
[](const aurora::gfx::Range& range) { return range.size != 0; }));
}
aurora::gx::finalize_frame_interpolation();
return interpolatedUniforms;
};
EXPECT_EQ(buildDeformedFrame(400), 0u);
EXPECT_EQ(buildDeformedFrame(500), 8u);
EXPECT_TRUE(aurora::gx::has_interpolated_frame());
resetInterpolation();
}
TEST(FrameInterpolationContract, DecomposesRotationScaleAndTranslation) {
const aurora::Mat3x4<float> previous{
{1.0f, 0.0f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
const aurora::Mat3x4<float> current{
{0.0f, -2.0f, 0.0f, 10.0f},
{2.0f, 0.0f, 0.0f, 20.0f},
{0.0f, 0.0f, 2.0f, 30.0f},
};
aurora::Mat3x4<float> midpoint{};
ASSERT_TRUE(aurora::gx::interpolate_transform_midpoint(previous, current, midpoint));
const float expectedAxis = std::sqrt(0.5f) * 1.5f;
EXPECT_NEAR(midpoint.m0.x(), expectedAxis, 1.0e-5f);
EXPECT_NEAR(midpoint.m0.y(), -expectedAxis, 1.0e-5f);
EXPECT_NEAR(midpoint.m1.x(), expectedAxis, 1.0e-5f);
EXPECT_NEAR(midpoint.m1.y(), expectedAxis, 1.0e-5f);
EXPECT_NEAR(midpoint.m2.z(), 1.5f, 1.0e-5f);
EXPECT_FLOAT_EQ(midpoint.m0.w(), 5.0f);
EXPECT_FLOAT_EQ(midpoint.m1.w(), 10.0f);
EXPECT_FLOAT_EQ(midpoint.m2.w(), 15.0f);
aurora::Mat3x4<float> quarter{};
aurora::Mat3x4<float> threeQuarter{};
ASSERT_TRUE(aurora::gx::interpolate_transform(previous, current, 0.25f, quarter));
ASSERT_TRUE(aurora::gx::interpolate_transform(previous, current, 0.75f, threeQuarter));
EXPECT_FLOAT_EQ(quarter.m0.w(), 2.5f);
EXPECT_FLOAT_EQ(quarter.m1.w(), 5.0f);
EXPECT_FLOAT_EQ(quarter.m2.w(), 7.5f);
EXPECT_FLOAT_EQ(threeQuarter.m0.w(), 7.5f);
EXPECT_FLOAT_EQ(threeQuarter.m1.w(), 15.0f);
EXPECT_FLOAT_EQ(threeQuarter.m2.w(), 22.5f);
}
TEST(FrameInterpolationContract, IndexedPaletteInterpolationPreservesSharedSeams) {
const aurora::Mat3x4<float> identity{
{1.0f, 0.0f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
const aurora::Mat3x4<float> quarterTurn{
{0.0f, -1.0f, 0.0f, 0.0f},
{1.0f, 0.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
const aurora::Mat3x4<float> translatedPrevious{
{1.0f, 0.0f, 0.0f, 1.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
const aurora::Mat3x4<float> translatedCurrent{
{1.0f, 0.0f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f, 1.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
aurora::Mat3x4<float> rotatedMidpoint{};
aurora::Mat3x4<float> translatedMidpoint{};
ASSERT_TRUE(aurora::gx::interpolate_indexed_transform(identity, quarterTurn, 0.5f, rotatedMidpoint));
ASSERT_TRUE(
aurora::gx::interpolate_indexed_transform(translatedPrevious, translatedCurrent, 0.5f, translatedMidpoint));
const auto transformPoint = [](const aurora::Mat3x4<float>& matrix, const std::array<float, 3>& point) {
return std::array<float, 3>{
matrix.m0.x() * point[0] + matrix.m0.y() * point[1] + matrix.m0.z() * point[2] + matrix.m0.w(),
matrix.m1.x() * point[0] + matrix.m1.y() * point[1] + matrix.m1.z() * point[2] + matrix.m1.w(),
matrix.m2.x() * point[0] + matrix.m2.y() * point[1] + matrix.m2.z() * point[2] + matrix.m2.w(),
};
};
// Two representations of the same seam point that agree at both real frames, so coefficient
// interpolation has to keep them coincident; a rigid decompose would pull them apart.
const auto rotatedSeam = transformPoint(rotatedMidpoint, {1.0f, 0.0f, 0.0f});
const auto translatedSeam = transformPoint(translatedMidpoint, {0.0f, 0.0f, 0.0f});
for (size_t component = 0; component < rotatedSeam.size(); ++component) {
EXPECT_FLOAT_EQ(rotatedSeam[component], translatedSeam[component]);
}
EXPECT_FLOAT_EQ(rotatedSeam[0], 0.5f);
EXPECT_FLOAT_EQ(rotatedSeam[1], 0.5f);
// Composed palette matrices may legitimately contain shear, so the indexed path interpolates it
// instead of rejecting the matrix and snapping those vertices to the new frame.
const aurora::Mat3x4<float> sheared{
{1.0f, 0.5f, 0.0f, 4.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
aurora::Mat3x4<float> shearedMidpoint{};
ASSERT_TRUE(aurora::gx::interpolate_indexed_transform(identity, sheared, 0.5f, shearedMidpoint));
EXPECT_FLOAT_EQ(shearedMidpoint.m0.y(), 0.25f);
EXPECT_FLOAT_EQ(shearedMidpoint.m0.w(), 2.0f);
}
TEST(FrameInterpolationContract, IndexedPaletteHistoryKeepsAbsoluteVertexSlots) {
constexpr uint16_t usedMask = (1u << 0) | (1u << 1);
constexpr size_t projectionOffset = 0;
constexpr size_t positionOffset = sizeof(aurora::Mat4x4<float>);
constexpr size_t normalOffset = positionOffset + aurora::gx::MaxPnMtx * sizeof(aurora::Mat3x4<float>);
constexpr size_t uniformSize = normalOffset + aurora::gx::MaxPnMtx * sizeof(aurora::Mat3x4<float>);
const aurora::gx::FrameInterpolationDrawIdentity identity{
.combined = 0x1234,
.pipeline = 0x5678,
.texture = 0x9abc,
.matrixTopology = 0xdef0,
};
const aurora::Mat4x4<float> projection{};
const auto matrixAt = [](float x) {
return aurora::Mat3x4<float>{
{1.0f, 0.0f, 0.0f, x},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
};
const auto recordFrame = [&](const aurora::gx::FrameInterpolationDrawIdentity& drawIdentity, float slot0X,
float slot1X, std::array<uint8_t, uniformSize>& source) {
g_gxState.pnMtx[0].pos = matrixAt(slot0X);
g_gxState.pnMtx[1].pos = matrixAt(slot1X);
g_gxState.pnMtx[0].nrm = matrixAt(0.0f);
g_gxState.pnMtx[1].nrm = matrixAt(0.0f);
std::memcpy(source.data() + positionOffset, &g_gxState.pnMtx[0].pos, sizeof(aurora::Mat3x4<float>));
std::memcpy(source.data() + positionOffset + sizeof(aurora::Mat3x4<float>), &g_gxState.pnMtx[1].pos,
sizeof(aurora::Mat3x4<float>));
std::memcpy(source.data() + normalOffset, &g_gxState.pnMtx[0].nrm, sizeof(aurora::Mat3x4<float>));
std::memcpy(source.data() + normalOffset + sizeof(aurora::Mat3x4<float>), &g_gxState.pnMtx[1].nrm,
sizeof(aurora::Mat3x4<float>));
return aurora::gx::record_interpolation_draw(drawIdentity, projection, usedMask,
aurora::gx::InterpolatedUniformLayout{
.sourceUniformData = source.data(),
.uniformSize = source.size(),
.projectionOffset = projectionOffset,
.positionOffset = positionOffset,
.normalOffset = normalOffset,
.currentMatrix = 0,
.indexedMatrices = true,
});
};
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
aurora::gx::set_frame_interpolation_fps(120);
std::array<uint8_t, uniformSize> previousSource{};
aurora::gx::begin_frame_interpolation();
recordFrame(identity, 0.0f, 100.0f, previousSource);
aurora::gx::finalize_frame_interpolation();
// Current slot 0 is spatially nearest previous slot 1 and vice versa. The old nearest-unused
// heuristic crossed them even though vertex PNMTXIDX bytes address absolute slots 0 and 1.
aurora::gfx::testing::reset_uniform_allocations();
std::array<uint8_t, uniformSize> currentSource{};
aurora::gx::begin_frame_interpolation();
const auto ranges = recordFrame(identity, 90.0f, 10.0f, currentSource);
ASSERT_NE(ranges[0].size, 0u);
aurora::gx::finalize_frame_interpolation();
const auto& interpolated = aurora::gfx::testing::uniform_allocation(0);
ASSERT_EQ(interpolated.size(), uniformSize);
aurora::Mat3x4<float> slot0Midpoint{};
aurora::Mat3x4<float> slot1Midpoint{};
std::memcpy(static_cast<void*>(&slot0Midpoint), interpolated.data() + positionOffset, sizeof(slot0Midpoint));
std::memcpy(static_cast<void*>(&slot1Midpoint), interpolated.data() + positionOffset + sizeof(slot0Midpoint),
sizeof(slot1Midpoint));
EXPECT_FLOAT_EQ(slot0Midpoint.m0.w(), 45.0f);
EXPECT_FLOAT_EQ(slot1Midpoint.m0.w(), 55.0f);
// A changed vertex-to-slot topology is not recoverable from matrix values, so it must not fall
// into the coarse material-only bucket even when the used mask is unchanged.
auto changedTopology = identity;
++changedTopology.combined;
++changedTopology.matrixTopology;
aurora::gfx::testing::reset_uniform_allocations();
std::array<uint8_t, uniformSize> changedSource{};
aurora::gx::begin_frame_interpolation();
const auto changedRanges = recordFrame(changedTopology, 91.0f, 9.0f, changedSource);
aurora::gx::finalize_frame_interpolation();
// Palette borrowing may reserve a range before matching is resolved. The
// safety contract is that a changed topology never reuses the old transforms.
if (changedRanges[0].size != 0) {
const auto& unchanged = aurora::gfx::testing::uniform_allocation(changedRanges[0].offset);
EXPECT_EQ(std::memcmp(unchanged.data(), changedSource.data(), uniformSize), 0);
}
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::reset_uniform_allocations();
}
TEST(FrameInterpolationContract, RepeatedDrawsMatchByTransformInsteadOfDrawOrder) {
const aurora::Mat3x4<float> left{
{1.0f, 0.0f, 0.0f, -500.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 1000.0f},
};
const aurora::Mat3x4<float> right{
{1.0f, 0.0f, 0.0f, 500.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 1000.0f},
};
const aurora::Mat3x4<float> currentRight{
{0.9998f, 0.0f, 0.02f, 496.0f},
{0.0f, 1.0f, 0.0f, 1.0f},
{-0.02f, 0.0f, 0.9998f, 997.0f},
};
// A transparent sorter may submit right then left this frame after left then right last frame.
// The signatures are identical, so spatial continuity has to pick the historical transform.
EXPECT_LT(aurora::gx::transform_match_distance_squared(right, currentRight),
aurora::gx::transform_match_distance_squared(left, currentRight));
}
TEST(VertexColorContract, MissingAndSparseColorsFollowGxRasterDefaults) {
aurora::gx::ShaderConfig config{};
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), -1);
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), -1);
config.attrs[GX_VA_CLR1].attrType = GX_DIRECT;
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), GX_VA_CLR1);
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), -1);
config.attrs[GX_VA_CLR0].attrType = GX_DIRECT;
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), GX_VA_CLR0);
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), GX_VA_CLR1);
config.attrs[GX_VA_CLR1].attrType = GX_NONE;
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 0), GX_VA_CLR0);
EXPECT_EQ(aurora::gx::shader_vertex_color_attr(config, 1), -1);
}
TEST(TevTexcoordStateContract, DirectStageFeedsFollowingAddPrevInFixedPoint) {
aurora::gx::ShaderConfig config{};
config.numTexGens = 2;
config.numIndStages = 1;
config.tevStageCount = 2;
config.indStages[0].texCoordId = GX_TEXCOORD0;
config.indStages[0].texMapId = GX_TEXMAP0;
config.indStages[0].scaleS = GX_ITS_1;
config.indStages[0].scaleT = GX_ITS_1;
config.tevStages[0].texCoordId = GX_TEXCOORD1;
config.tevStages[0].texMapId = GX_TEXMAP_NULL;
config.tevStages[0].indTexMtxId = GX_ITM_OFF;
config.tevStages[1].indTexMtxId = GX_ITM_1;
config.tevStages[1].indTexAddPrev = true;
config.tevStages[1].indTexWrapS = GX_ITW_0;
config.tevStages[1].indTexWrapT = GX_ITW_0;
config.tevStages[1].texCoordId = GX_TEXCOORD_NULL;
config.tevStages[1].texMapId = GX_TEXMAP1;
config.tevStages[1].colorPass.d = GX_CC_TEXC;
// Stage 0 is direct and stage 1 consumes the coordinate it establishes. Disabling stage 1's
// texture map does not stop that update, so its GX_TEXCOORD_NULL resolves to coord 0.
const auto directDependency = aurora::gx::tev_stage_texture_dependency(config, 0);
const auto indirectDependency = aurora::gx::tev_stage_texture_dependency(config, 1);
EXPECT_EQ(directDependency.texCoordId, 1);
EXPECT_EQ(directDependency.texMapId, -1);
EXPECT_TRUE(directDependency.needsFixedTexcoordState);
EXPECT_FALSE(directDependency.canSampleTexture);
EXPECT_EQ(indirectDependency.texCoordId, 0);
EXPECT_EQ(indirectDependency.texMapId, 1);
EXPECT_TRUE(indirectDependency.needsFixedTexcoordState);
EXPECT_TRUE(indirectDependency.combinerUsesTexture);
EXPECT_TRUE(indirectDependency.canSampleTexture);
EXPECT_TRUE(aurora::gx::shader_uses_fixed_texcoord_state(config));
// The production ShaderInfo implementation, not the old empty stub: both effective coords, the
// regular texture and the indirect lookup have to appear in the binding contract.
const auto info = aurora::gx::build_shader_info(config);
EXPECT_TRUE(info.sampledTexCoords.test(0));
EXPECT_TRUE(info.sampledTexCoords.test(1));
EXPECT_TRUE(info.sampledTextures.test(0));
EXPECT_TRUE(info.sampledTextures.test(1));
EXPECT_TRUE(info.usedIndStages.test(0));
EXPECT_TRUE(info.usedIndTexMtxs.test(GX_ITM_1 - GX_ITM_0));
EXPECT_EQ(aurora::gx::tev_indirect_wrap_mask(GX_ITW_256), 0x7fff);
EXPECT_EQ(aurora::gx::tev_indirect_wrap_mask(static_cast<GXIndTexWrap>(GX_MAX_ITWRAP)), 0);
EXPECT_EQ(aurora::gx::indirect_matrix_mantissa(0.5f), 512);
EXPECT_EQ(aurora::gx::indirect_matrix_mantissa(-0.5f), -512);
EXPECT_EQ(aurora::gx::indirect_matrix_shift(3), -3);
EXPECT_EQ(aurora::gx::indirect_matrix_shift(-5), 5);
EXPECT_EQ(aurora::gx::tev_s24_wrap(0x007fffff), 0x007fffff);
EXPECT_EQ(aurora::gx::tev_s24_wrap(0x00800000), -0x00800000);
EXPECT_EQ(aurora::gx::tev_s24_wrap(-0x00800000), -0x00800000);
EXPECT_EQ(aurora::gx::tev_s24_wrap(-0x00800001), 0x007fffff);
config.tevStages[1].texMapId = GX_TEXMAP_NULL;
EXPECT_FALSE(aurora::gx::tev_stage_texture_dependency(config, 1).canSampleTexture);
config.tevStages[1].texMapId = GX_TEXMAP1;
// No regular texture lookup occurs with zero texgens, but an enabled
// indirect stage still samples its texture at the literal zero coordinate.
config.numTexGens = 0;
EXPECT_EQ(aurora::gx::tev_stage_texture_dependency(config, 1).texCoordId, -1);
EXPECT_FALSE(aurora::gx::tev_stage_texture_dependency(config, 1).canSampleTexture);
const auto zeroTexgenInfo = aurora::gx::build_shader_info(config);
EXPECT_FALSE(zeroTexgenInfo.sampledTexCoords.any());
EXPECT_TRUE(zeroTexgenInfo.sampledTextures.test(0));
EXPECT_FALSE(zeroTexgenInfo.sampledTextures.test(1));
// WGSL must likewise avoid producing a regular-texture UV expression for
// stage 1: tex1_size_bias is intentionally absent from this uniform layout.
const auto zeroTexgenDependency = aurora::gx::tev_stage_texture_dependency(config, 1);
EXPECT_FALSE(aurora::gx::tev_texture_sample_enabled(zeroTexgenDependency, zeroTexgenDependency.combinerUsesTexture));
// A standalone direct stage still uses the normalized fast path.
config.numTexGens = 2;
config.tevStageCount = 1;
config.tevStages[1] = {};
config.tevStages[0].texMapId = GX_TEXMAP1;
config.tevStages[0].colorPass.d = GX_CC_TEXC;
const auto directSampleDependency = aurora::gx::tev_stage_texture_dependency(config, 0);
EXPECT_FALSE(directSampleDependency.needsFixedTexcoordState);
EXPECT_TRUE(directSampleDependency.combinerUsesTexture);
EXPECT_TRUE(directSampleDependency.canSampleTexture);
const auto directInfo = aurora::gx::build_shader_info(config);
EXPECT_TRUE(directInfo.sampledTexCoords.test(1));
EXPECT_TRUE(directInfo.sampledTextures.test(1));
// Z-texture remains enabled even without a regular source stage; the shader
// applies its bias/op to a zero raw texture value in that case.
config.tevStages[0].texMapId = GX_TEXMAP_NULL;
config.tevStages[0].colorPass.d = GX_CC_ZERO;
config.zTexture = static_cast<u32>(GX_ZT_REPLACE) << 26;
EXPECT_TRUE(aurora::gx::tev_z_texture_enabled(config));
EXPECT_EQ(aurora::gx::tev_z_texture_stage(config), -1);
}
TEST(TevRegisterLivenessContract, RgbWriteDoesNotHideSameStageOldAlphaRead) {
aurora::gx::ShaderConfig config{};
config.tevStageCount = 1;
config.tevStages[0].colorOp.outReg = GX_TEVREG0;
config.tevStages[0].alphaPass.d = GX_CA_A0;
const auto info = aurora::gx::build_shader_info(config);
EXPECT_TRUE(info.writesTevRegRgb.test(GX_TEVREG0));
EXPECT_FALSE(info.writesTevRegAlpha.test(GX_TEVREG0));
EXPECT_FALSE(info.loadsTevRegRgb.test(GX_TEVREG0));
EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0));
}
TEST(TevRegisterLivenessContract, TracksOppositeHalvesIndependentlyAcrossStages) {
aurora::gx::ShaderConfig config{};
config.tevStageCount = 2;
config.tevStages[0].colorOp.outReg = GX_TEVREG0;
config.tevStages[0].alphaOp.outReg = GX_TEVREG1;
config.tevStages[1].colorPass.a = GX_CC_C1;
config.tevStages[1].alphaPass.a = GX_CA_A0;
const auto info = aurora::gx::build_shader_info(config);
EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0));
EXPECT_FALSE(info.loadsTevRegRgb.test(GX_TEVREG0));
EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG1));
EXPECT_FALSE(info.loadsTevRegAlpha.test(GX_TEVREG1));
}
TEST(TevRegisterLivenessContract, PacksOneUniformWhenBothHalvesNeedInitialValue) {
aurora::gx::ShaderConfig baseline{};
baseline.tevStageCount = 1;
auto config = baseline;
config.tevStages[0].colorPass.a = GX_CC_C0;
config.tevStages[0].alphaPass.a = GX_CA_A0;
config.tevStages[0].colorPass.b = GX_CC_KONST;
config.tevStages[0].kcSel = GX_TEV_KCSEL_K0;
const auto info = aurora::gx::build_shader_info(config);
EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG0));
EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0));
// The final allocation is alignment-rounded, so adding one register need
// not increase it. Verify actual packing with a distinct following K color.
const auto savedReg = g_gxState.colorRegs[GX_TEVREG0];
const auto savedKColor = g_gxState.kcolors[GX_KCOLOR0];
g_gxState.colorRegs[GX_TEVREG0] = {11.f, 22.f, 33.f, 44.f};
g_gxState.kcolors[GX_KCOLOR0] = {55.f, 66.f, 77.f, 88.f};
EXPECT_TRUE(info.sampledKColors.test(GX_KCOLOR0));
aurora::gfx::testing::reset_uniform_allocations();
aurora::gx::build_uniform(info, 0, {}, {}, false);
const auto expectedReg = g_gxState.colorRegs[GX_TEVREG0];
const auto expectedKColor = g_gxState.kcolors[GX_KCOLOR0];
g_gxState.colorRegs[GX_TEVREG0] = savedReg;
g_gxState.kcolors[GX_KCOLOR0] = savedKColor;
const auto& bytes = aurora::gfx::testing::uniform_allocation(0);
const auto* reg = reinterpret_cast<const uint8_t*>(&expectedReg);
const auto found = std::search(bytes.begin(), bytes.end(), reg, reg + sizeof(aurora::Vec4<float>));
ASSERT_NE(found, bytes.end());
const size_t offset = static_cast<size_t>(found - bytes.begin());
ASSERT_LE(offset + 2 * sizeof(aurora::Vec4<float>), bytes.size());
EXPECT_EQ(std::memcmp(bytes.data() + offset + sizeof(aurora::Vec4<float>), &expectedKColor,
sizeof(aurora::Vec4<float>)),
0);
aurora::gfx::testing::reset_uniform_allocations();
}
// BP registers (direct FIFO writes, no dirty state flush needed)
// --- GXSetBlendMode (BP 0x41) ---
TEST_F(GXFifoTest, BlendMode_Blend_SrcAlpha) {
GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_NOOP);
auto bytes = capture_fifo();
// Validate encoding: BP opcode 0x61, register ID 0x41
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0x41);
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.blendMode, GX_BM_BLEND);
EXPECT_EQ(g_gxState.blendFacSrc, GX_BL_SRCALPHA);
EXPECT_EQ(g_gxState.blendFacDst, GX_BL_INVSRCALPHA);
}
TEST_F(GXFifoTest, BlendMode_None) {
GXSetBlendMode(GX_BM_NONE, GX_BL_ZERO, GX_BL_ZERO, GX_LO_CLEAR);
auto bytes = capture_fifo();
reset_gx_state();
// Pre-set to something else to prove the decode works
g_gxState.blendMode = GX_BM_BLEND;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.blendMode, GX_BM_NONE);
}
TEST_F(GXFifoTest, BlendMode_Subtract) {
GXSetBlendMode(GX_BM_SUBTRACT, GX_BL_ONE, GX_BL_ONE, GX_LO_NOOP);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.blendMode, GX_BM_SUBTRACT);
}
TEST_F(GXFifoTest, BlendMode_Logic) {
GXSetBlendMode(GX_BM_LOGIC, GX_BL_ONE, GX_BL_ZERO, GX_LO_XOR);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.blendMode, GX_BM_LOGIC);
EXPECT_EQ(g_gxState.blendOp, GX_LO_XOR);
}
TEST_F(GXFifoTest, GenMode_FirstZeroWriteDecodesAndRepeatDeduplicates) {
reset_gx_state();
const auto before = g_gxState.pipelineStateGeneration;
decode_fifo(bp_cmd(0, 0));
EXPECT_EQ(g_gxState.numTevStages, 1u);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
EXPECT_EQ(g_gxState.numChans, 0u);
EXPECT_EQ(g_gxState.numTexGens, 0u);
EXPECT_EQ(g_gxState.numIndStages, 0u);
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
const auto decoded = g_gxState.pipelineStateGeneration;
decode_fifo(bp_cmd(0, 0));
EXPECT_EQ(g_gxState.pipelineStateGeneration, decoded);
}
TEST_F(GXFifoTest, GenMode_FirstMaskedWritePreservesZeroResetBits) {
for (const u32 mask : {0u, 1u << 10}) {
reset_gx_state();
const auto before = g_gxState.pipelineStateGeneration;
decode_fifo(bp_cmd(0xFE, mask));
decode_fifo(bp_cmd(0, 0xFFFFFF));
EXPECT_EQ(g_gxState.bpRegCache[0], mask);
EXPECT_EQ(g_gxState.bpRegCache[0xFE], 0xFFFFFFu);
EXPECT_EQ(g_gxState.numTevStages, mask ? 2u : 1u);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
decode_fifo(bp_cmd(0, 0));
EXPECT_EQ(g_gxState.numTevStages, 1u);
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
}
}
TEST_F(GXFifoTest, GenMode_ColdSingleStageApiSetupDecodes) {
reset_gx_state();
GXSetNumTevStages(1);
GXSetNumTexGens(0);
GXSetNumChans(0);
GXSetCullMode(GX_CULL_NONE);
const auto bytes = flush_and_capture();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.numTevStages, 1u);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
}
TEST_F(GXFifoTest, BpMask_AppliesOnlyToNextWrite) {
std::vector<u8> bytes;
auto mask = bp_cmd(0xFE, 1u << 19);
auto genMode = bp_cmd(0x00, 1u << 19);
auto cullAndInd = bp_cmd(0x00, (2u << 14) | (3u << 16));
bytes.insert(bytes.end(), mask.begin(), mask.end());
bytes.insert(bytes.end(), genMode.begin(), genMode.end());
bytes.insert(bytes.end(), cullAndInd.begin(), cullAndInd.end());
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.bpRegCache[0x00] & (1u << 19), 0u);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_FRONT);
EXPECT_EQ(g_gxState.numIndStages, 3u);
}
TEST_F(GXFifoTest, IndirectTextureMask_DecodesWithoutChangingBpWriteMask) {
auto bytes = bp_cmd(0x0F, 0x5A);
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.indTexMask, 0x5Au);
EXPECT_EQ(g_gxState.bpRegCache[0x0F], 0x0F00005Au);
EXPECT_EQ(g_gxState.bpRegCache[0xFE], 0x00FFFFFFu);
}
// --- GXSetColorUpdate / GXSetAlphaUpdate (BP 0x41 cmode0) ---
TEST_F(GXFifoTest, ColorUpdate_Disabled) {
GXSetColorUpdate(GX_FALSE);
auto bytes = capture_fifo();
reset_gx_state();
g_gxState.colorUpdate = true;
decode_fifo(bytes);
EXPECT_FALSE(g_gxState.colorUpdate);
}
TEST_F(GXFifoTest, AlphaUpdate_Disabled) {
GXSetAlphaUpdate(false);
auto bytes = capture_fifo();
reset_gx_state();
g_gxState.alphaUpdate = true;
decode_fifo(bytes);
EXPECT_FALSE(g_gxState.alphaUpdate);
}
// --- GXSetZMode (BP 0x40) ---
TEST_F(GXFifoTest, ZMode_LessNoUpdate) {
GXSetZMode(true, GX_LESS, false);
auto bytes = capture_fifo();
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0x40);
reset_gx_state();
decode_fifo(bytes);
EXPECT_TRUE(g_gxState.depthCompare);
EXPECT_EQ(g_gxState.depthFunc, GX_LESS);
EXPECT_FALSE(g_gxState.depthUpdate);
}
TEST_F(GXFifoTest, ZMode_AlwaysUpdate) {
GXSetZMode(true, GX_ALWAYS, true);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_TRUE(g_gxState.depthCompare);
EXPECT_EQ(g_gxState.depthFunc, GX_ALWAYS);
EXPECT_TRUE(g_gxState.depthUpdate);
}
TEST_F(GXFifoTest, ZMode_Disabled) {
GXSetZMode(false, GX_NEVER, false);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_FALSE(g_gxState.depthCompare);
EXPECT_EQ(g_gxState.depthFunc, GX_NEVER);
EXPECT_FALSE(g_gxState.depthUpdate);
}
TEST_F(GXFifoTest, ZTexture_ReplaceZ24X8) {
GXSetZTexture(GX_ZT_REPLACE, GX_TF_Z24X8, 0x123456);
auto bytes = capture_fifo();
EXPECT_TRUE(has_bp_write(bytes, 0xF4));
EXPECT_TRUE(has_bp_write(bytes, 0xF5));
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.zTextureBias, 0x123456u);
EXPECT_EQ(g_gxState.zTextureFmt, 2u);
EXPECT_EQ(g_gxState.zTextureOp, GX_ZT_REPLACE);
}
// --- GXSetAlphaCompare (BP 0xF3) ---
TEST_F(GXFifoTest, AlphaCompare_GreaterThan128) {
GXSetAlphaCompare(GX_GREATER, 128, GX_AOP_AND, GX_ALWAYS, 0);
auto bytes = capture_fifo();
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0xF3);
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.alphaCompare.comp0, GX_GREATER);
EXPECT_EQ(g_gxState.alphaCompare.ref0, 128u);
EXPECT_EQ(g_gxState.alphaCompare.op, GX_AOP_AND);
EXPECT_EQ(g_gxState.alphaCompare.comp1, GX_ALWAYS);
EXPECT_EQ(g_gxState.alphaCompare.ref1, 0u);
}
TEST_F(GXFifoTest, AlphaCompare_OrGequal) {
GXSetAlphaCompare(GX_GEQUAL, 64, GX_AOP_OR, GX_LEQUAL, 200);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.alphaCompare.comp0, GX_GEQUAL);
EXPECT_EQ(g_gxState.alphaCompare.ref0, 64u);
EXPECT_EQ(g_gxState.alphaCompare.op, GX_AOP_OR);
EXPECT_EQ(g_gxState.alphaCompare.comp1, GX_LEQUAL);
EXPECT_EQ(g_gxState.alphaCompare.ref1, 200u);
}
// --- GXSetDstAlpha (BP 0x42) ---
TEST_F(GXFifoTest, DstAlpha_Enabled) {
GXSetDstAlpha(true, 0x80);
auto bytes = capture_fifo();
reset_gx_state();
g_gxState.dstAlpha = UINT32_MAX;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.dstAlpha, 0x80u);
}
TEST_F(GXFifoTest, DstAlpha_Disabled) {
GXSetDstAlpha(false, 0);
auto bytes = capture_fifo();
reset_gx_state();
g_gxState.dstAlpha = 0x80;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.dstAlpha, UINT32_MAX);
}
// --- GXSetPixelFmt (BP 0x43, 0x42 + genMode flush) ---
TEST_F(GXFifoTest, PixelFmt_Rgb565Z16_Decode) {
GXSetPixelFmt(GX_PF_RGB565_Z16, GX_ZC_FAR);
auto bytes = flush_and_capture();
EXPECT_TRUE(has_bp_write(bytes, 0x43));
EXPECT_TRUE(has_bp_write(bytes, 0x00));
reset_gx_state();
g_gxState.pixelFmt = GX_PF_RGB8_Z24;
g_gxState.zFmt = GX_ZC_LINEAR;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.pixelFmt, GX_PF_RGB565_Z16);
EXPECT_EQ(g_gxState.zFmt, GX_ZC_FAR);
EXPECT_TRUE(g_gxState.zCompLocBeforeTex);
}
TEST_F(GXFifoTest, PixelFmt_U8_Decode) {
GXSetPixelFmt(GX_PF_U8, GX_ZC_MID);
auto bytes = flush_and_capture();
EXPECT_TRUE(has_bp_write(bytes, 0x43));
EXPECT_TRUE(has_bp_write(bytes, 0x42));
EXPECT_TRUE(has_bp_write(bytes, 0x00));
reset_gx_state();
g_gxState.pixelFmt = GX_PF_RGB8_Z24;
g_gxState.zFmt = GX_ZC_LINEAR;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.pixelFmt, GX_PF_U8);
EXPECT_EQ(g_gxState.zFmt, GX_ZC_MID);
EXPECT_EQ(g_gxState.dstAlpha, UINT32_MAX);
EXPECT_TRUE(g_gxState.zCompLocBeforeTex);
}
// TEV registers (direct FIFO writes)
// --- GXSetTevColorIn / GXSetTevAlphaIn ---
TEST_F(GXFifoTest, TevColorIn_Stage0) {
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXC, GX_CC_RASC, GX_CC_ZERO);
auto bytes = capture_fifo();
// BP opcode 0x61, register 0xC0 (stage 0 color)
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0xC0);
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.colorPass.a, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.b, GX_CC_TEXC);
EXPECT_EQ(s.colorPass.c, GX_CC_RASC);
EXPECT_EQ(s.colorPass.d, GX_CC_ZERO);
}
TEST_F(GXFifoTest, TevAlphaIn_Stage0) {
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_TEXA, GX_CA_RASA, GX_CA_ZERO);
auto bytes = capture_fifo();
// BP opcode 0x61, register 0xC1 (stage 0 alpha)
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0xC1);
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA);
EXPECT_EQ(s.alphaPass.c, GX_CA_RASA);
EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO);
}
TEST_F(GXFifoTest, TevAlphaIn_Stage5) {
GXSetTevAlphaIn(GX_TEVSTAGE5, GX_CA_APREV, GX_CA_A0, GX_CA_KONST, GX_CA_ZERO);
auto bytes = capture_fifo();
// Stage 5 alpha register = 0xC1 + 5*2 = 0xCB
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0xCB);
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[5];
EXPECT_EQ(s.alphaPass.a, GX_CA_APREV);
EXPECT_EQ(s.alphaPass.b, GX_CA_A0);
EXPECT_EQ(s.alphaPass.c, GX_CA_KONST);
EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO);
}
TEST_F(GXFifoTest, TevColorIn_Stage7) {
GXSetTevColorIn(GX_TEVSTAGE7, GX_CC_C0, GX_CC_A0, GX_CC_KONST, GX_CC_CPREV);
auto bytes = capture_fifo();
// Stage 7 color register = 0xC0 + 7*2 = 0xCE
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0xCE);
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[7];
EXPECT_EQ(s.colorPass.a, GX_CC_C0);
EXPECT_EQ(s.colorPass.b, GX_CC_A0);
EXPECT_EQ(s.colorPass.c, GX_CC_KONST);
EXPECT_EQ(s.colorPass.d, GX_CC_CPREV);
}
// --- GXSetTevOp (convenience wrapper over ColorIn/AlphaIn/ColorOp/AlphaOp) ---
// GXSetTevOp emits 4 BP writes: tevc (colorIn+colorOp) and teva (alphaIn+alphaOp).
TEST_F(GXFifoTest, TevOp_Modulate_Stage0) {
GXSetTevOp(GX_TEVSTAGE0, GX_MODULATE);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
// Modulate: color = ZERO, TEXC, RASC, ZERO (stage 0 uses RASC/RASA)
EXPECT_EQ(s.colorPass.a, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.b, GX_CC_TEXC);
EXPECT_EQ(s.colorPass.c, GX_CC_RASC);
EXPECT_EQ(s.colorPass.d, GX_CC_ZERO);
// Modulate: alpha = ZERO, TEXA, RASA, ZERO
EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA);
EXPECT_EQ(s.alphaPass.c, GX_CA_RASA);
EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO);
// Op = ADD, bias = ZERO, scale = 1, clamp = true, outReg = TEVPREV
EXPECT_EQ(s.colorOp.op, GX_TEV_ADD);
EXPECT_EQ(s.colorOp.bias, GX_TB_ZERO);
EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_1);
EXPECT_TRUE(s.colorOp.clamp);
EXPECT_EQ(s.colorOp.outReg, GX_TEVPREV);
EXPECT_EQ(s.alphaOp.op, GX_TEV_ADD);
EXPECT_EQ(s.alphaOp.bias, GX_TB_ZERO);
EXPECT_EQ(s.alphaOp.scale, GX_CS_SCALE_1);
EXPECT_TRUE(s.alphaOp.clamp);
EXPECT_EQ(s.alphaOp.outReg, GX_TEVPREV);
}
TEST_F(GXFifoTest, TevOp_Modulate_Stage1) {
// Non-stage-0 uses CPREV/APREV instead of RASC/RASA
GXSetTevOp(GX_TEVSTAGE1, GX_MODULATE);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[1];
EXPECT_EQ(s.colorPass.a, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.b, GX_CC_TEXC);
EXPECT_EQ(s.colorPass.c, GX_CC_CPREV);
EXPECT_EQ(s.colorPass.d, GX_CC_ZERO);
EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA);
EXPECT_EQ(s.alphaPass.c, GX_CA_APREV);
EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO);
}
TEST_F(GXFifoTest, TevOp_Replace) {
GXSetTevOp(GX_TEVSTAGE0, GX_REPLACE);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
// Replace: color = ZERO, ZERO, ZERO, TEXC
EXPECT_EQ(s.colorPass.a, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.b, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.c, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.d, GX_CC_TEXC);
// Replace: alpha = ZERO, ZERO, ZERO, TEXA
EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.b, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.c, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.d, GX_CA_TEXA);
}
TEST_F(GXFifoTest, TevOp_Decal) {
GXSetTevOp(GX_TEVSTAGE0, GX_DECAL);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
// Decal: color = RASC, TEXC, TEXA, ZERO
EXPECT_EQ(s.colorPass.a, GX_CC_RASC);
EXPECT_EQ(s.colorPass.b, GX_CC_TEXC);
EXPECT_EQ(s.colorPass.c, GX_CC_TEXA);
EXPECT_EQ(s.colorPass.d, GX_CC_ZERO);
// Decal: alpha = ZERO, ZERO, ZERO, RASA
EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.b, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.c, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.d, GX_CA_RASA);
}
TEST_F(GXFifoTest, TevOp_Blend) {
GXSetTevOp(GX_TEVSTAGE0, GX_BLEND);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
// Blend: color = RASC, ONE, TEXC, ZERO
EXPECT_EQ(s.colorPass.a, GX_CC_RASC);
EXPECT_EQ(s.colorPass.b, GX_CC_ONE);
EXPECT_EQ(s.colorPass.c, GX_CC_TEXC);
EXPECT_EQ(s.colorPass.d, GX_CC_ZERO);
// Blend: alpha = ZERO, TEXA, RASA, ZERO
EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.b, GX_CA_TEXA);
EXPECT_EQ(s.alphaPass.c, GX_CA_RASA);
EXPECT_EQ(s.alphaPass.d, GX_CA_ZERO);
}
TEST_F(GXFifoTest, TevOp_PassClr) {
GXSetTevOp(GX_TEVSTAGE0, GX_PASSCLR);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
// PassClr: color = ZERO, ZERO, ZERO, RASC
EXPECT_EQ(s.colorPass.a, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.b, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.c, GX_CC_ZERO);
EXPECT_EQ(s.colorPass.d, GX_CC_RASC);
// PassClr: alpha = ZERO, ZERO, ZERO, RASA
EXPECT_EQ(s.alphaPass.a, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.b, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.c, GX_CA_ZERO);
EXPECT_EQ(s.alphaPass.d, GX_CA_RASA);
}
// --- GXSetTevColorOp / GXSetTevAlphaOp ---
TEST_F(GXFifoTest, TevColorOp_Sub_Scale2_Reg1) {
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_SUB, GX_TB_ADDHALF, GX_CS_SCALE_2, GX_TRUE, GX_TEVREG1);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.colorOp.op, GX_TEV_SUB);
EXPECT_EQ(s.colorOp.bias, GX_TB_ADDHALF);
EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_2);
EXPECT_TRUE(s.colorOp.clamp);
EXPECT_EQ(s.colorOp.outReg, GX_TEVREG1);
}
TEST_F(GXFifoTest, TevAlphaOp_Add_NoClamp_Reg2) {
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_SUBHALF, GX_CS_DIVIDE_2, GX_FALSE, GX_TEVREG2);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.alphaOp.op, GX_TEV_ADD);
EXPECT_EQ(s.alphaOp.bias, GX_TB_SUBHALF);
EXPECT_EQ(s.alphaOp.scale, GX_CS_DIVIDE_2);
EXPECT_FALSE(s.alphaOp.clamp);
EXPECT_EQ(s.alphaOp.outReg, GX_TEVREG2);
}
TEST_F(GXFifoTest, TevColorOp_CompareR8GT) {
// Compare ops (op > 1) use a different encoding: bias=3, scale encodes compare mode
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_COMP_R8_GT, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_R8_GT);
// Decoder normalizes compare mode: bias=ZERO, scale=SCALE_1
EXPECT_EQ(s.colorOp.bias, GX_TB_ZERO);
EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_1);
EXPECT_EQ(s.colorOp.outReg, GX_TEVPREV);
}
TEST_F(GXFifoTest, TevColorOp_CompareGR16EQ) {
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_COMP_GR16_EQ, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVREG0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_GR16_EQ);
EXPECT_EQ(s.colorOp.outReg, GX_TEVREG0);
}
TEST_F(GXFifoTest, TevAlphaOp_CompareRGB8GT) {
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_COMP_RGB8_GT, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
// GX_TEV_COMP_RGB8_GT is the same enum value for alpha as GX_TEV_COMP_A8_GT
EXPECT_EQ(s.alphaOp.op, GX_TEV_COMP_RGB8_GT);
EXPECT_EQ(s.alphaOp.bias, GX_TB_ZERO);
EXPECT_EQ(s.alphaOp.scale, GX_CS_SCALE_1);
}
TEST_F(GXFifoTest, TevColorOp_CompareBGR24GT) {
GXSetTevColorOp(GX_TEVSTAGE2, GX_TEV_COMP_BGR24_GT, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVREG1);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[2];
EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_BGR24_GT);
EXPECT_EQ(s.colorOp.bias, GX_TB_ZERO);
EXPECT_EQ(s.colorOp.scale, GX_CS_SCALE_1);
EXPECT_EQ(s.colorOp.outReg, GX_TEVREG1);
}
TEST_F(GXFifoTest, TevColorOp_CompareRGB8EQ) {
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_COMP_RGB8_EQ, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.colorOp.op, GX_TEV_COMP_RGB8_EQ);
EXPECT_EQ(s.colorOp.outReg, GX_TEVPREV);
}
TEST_F(GXFifoTest, TevAlphaOp_CompareA8EQ) {
GXSetTevAlphaOp(GX_TEVSTAGE1, GX_TEV_COMP_RGB8_EQ, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVREG2);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[1];
// For alpha, GX_TEV_COMP_RGB8_EQ maps to A8_EQ
EXPECT_EQ(s.alphaOp.op, GX_TEV_COMP_RGB8_EQ);
EXPECT_EQ(s.alphaOp.outReg, GX_TEVREG2);
}
// --- GXSetTevColorS10 ---
TEST_F(GXFifoTest, TevColorS10_Positive) {
GXColorS10 col = {511, 256, 100, 0};
GXSetTevColorS10(GX_TEVREG0, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// S10 values are encoded as 11-bit signed and decoded to float/255
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][0], 511.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][1], 256.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][2], 100.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][3], 0.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, TevColorS10_Negative) {
GXColorS10 col = {-128, -1, 0, 255};
GXSetTevColorS10(GX_TEVPREV, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][0], -128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][1], -1.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][2], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][3], 255.f / 255.f, 1.f / 255.f);
}
// --- GXSetTevKColorSel / GXSetTevKAlphaSel ---
TEST_F(GXFifoTest, TevKColorSel_Stage0_K0) {
GXSetTevKColorSel(GX_TEVSTAGE0, GX_TEV_KCSEL_K0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevStages[0].kcSel, GX_TEV_KCSEL_K0);
}
TEST_F(GXFifoTest, TevKColorSel_Stage1_K2_R) {
GXSetTevKColorSel(GX_TEVSTAGE1, GX_TEV_KCSEL_K2_R);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevStages[1].kcSel, GX_TEV_KCSEL_K2_R);
}
TEST_F(GXFifoTest, TevKAlphaSel_Stage0_K1_A) {
GXSetTevKAlphaSel(GX_TEVSTAGE0, GX_TEV_KASEL_K1_A);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevStages[0].kaSel, GX_TEV_KASEL_K1_A);
}
TEST_F(GXFifoTest, TevKAlphaSel_Stage3_K3_B) {
GXSetTevKAlphaSel(GX_TEVSTAGE3, GX_TEV_KASEL_K3_B);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevStages[3].kaSel, GX_TEV_KASEL_K3_B);
}
TEST_F(GXFifoTest, TevKColorSel_DoesNotCorruptSwapTable) {
// Regression: tevKsel shadow registers share bits with swap table entries.
// Setting K color selection must not zero out the swap table bits.
GXSetTevKColorSel(GX_TEVSTAGE0, GX_TEV_KCSEL_K0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// Swap table 0 should remain identity (initialized in GXInit)
EXPECT_EQ(g_gxState.tevSwapTable[0].red, GX_CH_RED);
EXPECT_EQ(g_gxState.tevSwapTable[0].green, GX_CH_GREEN);
EXPECT_EQ(g_gxState.tevSwapTable[0].blue, GX_CH_BLUE);
EXPECT_EQ(g_gxState.tevSwapTable[0].alpha, GX_CH_ALPHA);
// K color selection should still be set
EXPECT_EQ(g_gxState.tevStages[0].kcSel, GX_TEV_KCSEL_K0);
}
// --- GXSetTevSwapMode ---
TEST_F(GXFifoTest, TevSwapMode_Stage0) {
GXSetTevSwapMode(GX_TEVSTAGE0, GX_TEV_SWAP1, GX_TEV_SWAP2);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevStages[0].tevSwapRas, GX_TEV_SWAP1);
EXPECT_EQ(g_gxState.tevStages[0].tevSwapTex, GX_TEV_SWAP2);
}
TEST_F(GXFifoTest, TevSwapMode_Stage3) {
GXSetTevSwapMode(GX_TEVSTAGE3, GX_TEV_SWAP3, GX_TEV_SWAP0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevStages[3].tevSwapRas, GX_TEV_SWAP3);
EXPECT_EQ(g_gxState.tevStages[3].tevSwapTex, GX_TEV_SWAP0);
}
// --- GXSetTevSwapModeTable ---
TEST_F(GXFifoTest, TevSwapModeTable_Swap1_AllRed) {
GXSetTevSwapModeTable(GX_TEV_SWAP1, GX_CH_RED, GX_CH_RED, GX_CH_RED, GX_CH_ALPHA);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].red, GX_CH_RED);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].green, GX_CH_RED);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].blue, GX_CH_RED);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP1].alpha, GX_CH_ALPHA);
}
TEST_F(GXFifoTest, TevSwapModeTable_Swap2_Swizzle) {
GXSetTevSwapModeTable(GX_TEV_SWAP2, GX_CH_BLUE, GX_CH_GREEN, GX_CH_RED, GX_CH_ALPHA);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].red, GX_CH_BLUE);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].green, GX_CH_GREEN);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].blue, GX_CH_RED);
EXPECT_EQ(g_gxState.tevSwapTable[GX_TEV_SWAP2].alpha, GX_CH_ALPHA);
}
// --- GXSetTevOrder (BP 0x28-0x2F) ---
TEST_F(GXFifoTest, TevOrder_Stage0) {
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.texMapId, GX_TEXMAP0);
EXPECT_EQ(s.texCoordId, GX_TEXCOORD0);
EXPECT_EQ(s.channelId, GX_COLOR0A0);
}
TEST_F(GXFifoTest, TevOrder_Stage1_OddStage) {
// Odd stages use different bit positions within the tref register
GXSetTevOrder(GX_TEVSTAGE1, GX_TEXCOORD2, GX_TEXMAP3, GX_COLOR1A1);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[1];
EXPECT_EQ(s.texMapId, GX_TEXMAP3);
EXPECT_EQ(s.texCoordId, GX_TEXCOORD2);
EXPECT_EQ(s.channelId, GX_COLOR1A1);
}
TEST_F(GXFifoTest, TevOrder_Stage0_TexNull) {
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& s = g_gxState.tevStages[0];
EXPECT_EQ(s.texMapId, GX_TEXMAP_NULL);
EXPECT_EQ(s.channelId, GX_COLOR0A0);
}
// --- GXSetTevKColor (BP 0xE0-0xE7, K color flag) ---
TEST_F(GXFifoTest, TevKColor_K0) {
GXColor kc = {255, 128, 64, 32};
GXSetTevKColor(GX_KCOLOR0, kc);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// K colors are stored as float (0-1 range), 8-bit precision
EXPECT_NEAR(g_gxState.kcolors[0][0], 255.f / 255.f, 1.f / 255.f); // R
EXPECT_NEAR(g_gxState.kcolors[0][1], 128.f / 255.f, 1.f / 255.f); // G
EXPECT_NEAR(g_gxState.kcolors[0][2], 64.f / 255.f, 1.f / 255.f); // B
EXPECT_NEAR(g_gxState.kcolors[0][3], 32.f / 255.f, 1.f / 255.f); // A
}
TEST_F(GXFifoTest, TevKColor_K1) {
GXColor kc = {0, 255, 0, 128};
GXSetTevKColor(GX_KCOLOR1, kc);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.kcolors[1][0], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[1][1], 255.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[1][2], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[1][3], 128.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, TevKColor_K2) {
GXColor kc = {10, 20, 30, 40};
GXSetTevKColor(GX_KCOLOR2, kc);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.kcolors[2][0], 10.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[2][1], 20.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[2][2], 30.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[2][3], 40.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, TevKColor_K3) {
GXColor kc = {200, 150, 100, 50};
GXSetTevKColor(GX_KCOLOR3, kc);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.kcolors[3][0], 200.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[3][1], 150.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[3][2], 100.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.kcolors[3][3], 50.f / 255.f, 1.f / 255.f);
}
// GXSetTevColor (BP 0xE0-0xE7): the side channel stores float while the FIFO encodes 11-bit
// signed, so the decoded value comes back with reduced precision.
TEST_F(GXFifoTest, TevColor_Reg0) {
GXColor col = {200, 100, 50, 255};
GXSetTevColor(GX_TEVREG0, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// 11-bit signed encoding, so values should round-trip within 8-bit range
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][0], 200.f / 255.f, 1.f / 255.f); // R
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][1], 100.f / 255.f, 1.f / 255.f); // G
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][2], 50.f / 255.f, 1.f / 255.f); // B
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][3], 255.f / 255.f, 1.f / 255.f); // A
}
TEST_F(GXFifoTest, TevColor_Prev) {
GXColor col = {128, 64, 32, 16};
GXSetTevColor(GX_TEVPREV, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][0], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][1], 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][2], 32.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVPREV][3], 16.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, TevColor_Reg1) {
GXColor col = {0, 128, 255, 192};
GXSetTevColor(GX_TEVREG1, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][0], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][1], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][2], 255.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][3], 192.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, TevColor_Reg2) {
GXColor col = {1, 2, 3, 4};
GXSetTevColor(GX_TEVREG2, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][0], 1.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][1], 2.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][2], 3.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][3], 4.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, TevColorS10_Reg1) {
GXColorS10 col = {300, -50, 0, 255};
GXSetTevColorS10(GX_TEVREG1, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][0], 300.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][1], -50.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][2], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG1][3], 255.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, TevColorS10_Reg2) {
GXColorS10 col = {-1024, 1023, 128, -256};
GXSetTevColorS10(GX_TEVREG2, col);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][0], -1024.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][1], 1023.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][2], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG2][3], -256.f / 255.f, 1.f / 255.f);
}
// CP registers (require __GXSetDirtyState() flush)
// --- GXClearVtxDesc ---
TEST_F(GXFifoTest, ClearVtxDesc_ClearsAll) {
// Set every attribute to something non-default
GXSetVtxDesc(GX_VA_PNMTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX0MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX1MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX2MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX3MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX4MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX5MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX6MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX7MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_POS, GX_INDEX16);
GXSetVtxDesc(GX_VA_NRM, GX_INDEX8);
GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
GXSetVtxDesc(GX_VA_CLR1, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX0, GX_INDEX16);
GXSetVtxDesc(GX_VA_TEX1, GX_INDEX8);
GXSetVtxDesc(GX_VA_TEX2, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX3, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX4, GX_INDEX16);
GXSetVtxDesc(GX_VA_TEX5, GX_INDEX8);
GXSetVtxDesc(GX_VA_TEX6, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX7, GX_DIRECT);
// Discard the dirty state from above
aurora::gx::fifo::clear_buffer();
// Now clear and flush
GXClearVtxDesc();
auto bytes = flush_and_capture();
reset_gx_state();
// Pre-fill g_gxState with non-zero to prove decode clears them
for (int i = 0; i < GX_VA_MAX_ATTR; ++i) {
g_gxState.vtxDesc[i] = GX_INDEX16;
}
decode_fifo(bytes);
// After GXClearVtxDesc: POS = GX_DIRECT, everything else = GX_NONE
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_PNMTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX1MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX2MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX3MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX4MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX5MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX6MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX7MTXIDX], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_NRM], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR0], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR1], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX1], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX2], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX3], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX4], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX5], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX6], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX7], GX_NONE);
}
// --- GXSetVtxDesc / GXClearVtxDesc ---
TEST_F(GXFifoTest, VtxDesc_PosAndNrm_Direct) {
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
GXSetVtxDesc(GX_VA_NRM, GX_DIRECT);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_NRM], GX_DIRECT);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR0], GX_NONE);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0], GX_NONE);
}
TEST_F(GXFifoTest, VtxDesc_Indexed) {
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_INDEX16);
GXSetVtxDesc(GX_VA_NRM, GX_INDEX16);
GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX0, GX_INDEX8);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_INDEX16);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_NRM], GX_INDEX16);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_CLR0], GX_DIRECT);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0], GX_INDEX8);
EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_POS], GX_INDEX16);
EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_NRM], GX_INDEX16);
EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_CLR0], GX_DIRECT);
EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_TEX0], GX_INDEX8);
}
TEST_F(GXFifoTest, SourceVtxDesc_DoesNotChangeEmittedLayout) {
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
GXSetSourceVtxDesc(GX_VA_POS, GX_INDEX16);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT);
EXPECT_EQ(g_gxState.sourceVtxDesc[GX_VA_POS], GX_INDEX16);
}
TEST_F(GXFifoTest, VtxDesc_MtxIdx) {
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_PNMTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX0MTXIDX, GX_DIRECT);
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_PNMTXIDX], GX_DIRECT);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_TEX0MTXIDX], GX_DIRECT);
EXPECT_EQ(g_gxState.vtxDesc[GX_VA_POS], GX_DIRECT);
}
TEST_F(GXFifoTest, GetVtxDesc_UsesShadowState) {
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_INDEX16);
GXSetVtxDesc(GX_VA_NBT, GX_DIRECT);
GXAttrType posType = GX_NONE;
GXAttrType nbtType = GX_NONE;
GXVtxDescList vcd[24]{};
GXGetVtxDesc(GX_VA_POS, &posType);
GXGetVtxDesc(GX_VA_NBT, &nbtType);
GXGetVtxDescv(vcd);
EXPECT_EQ(posType, GX_INDEX16);
EXPECT_EQ(nbtType, GX_DIRECT);
EXPECT_EQ(vcd[GX_VA_POS].attr, GX_VA_POS);
EXPECT_EQ(vcd[GX_VA_POS].type, GX_INDEX16);
EXPECT_EQ(vcd[GX_VA_TEX7 + 1].attr, GX_VA_NBT);
EXPECT_EQ(vcd[GX_VA_TEX7 + 1].type, GX_DIRECT);
EXPECT_EQ(vcd[GX_VA_TEX7 + 2].attr, GX_VA_NULL);
}
// --- GXSetVtxAttrFmt ---
TEST_F(GXFifoTest, VtxAttrFmt_PosF32) {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
auto& vf = g_gxState.vtxFmts[GX_VTXFMT0];
EXPECT_EQ(vf.attrs[GX_VA_POS].cnt, GX_POS_XYZ);
EXPECT_EQ(vf.attrs[GX_VA_POS].type, GX_F32);
EXPECT_EQ(vf.attrs[GX_VA_POS].frac, 0);
}
TEST_F(GXFifoTest, VtxAttrFmt_NrmS16) {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_S16, 0);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
auto& vf = g_gxState.vtxFmts[GX_VTXFMT0];
EXPECT_EQ(vf.attrs[GX_VA_NRM].cnt, GX_NRM_XYZ);
EXPECT_EQ(vf.attrs[GX_VA_NRM].type, GX_S16);
EXPECT_EQ(vf.attrs[GX_VA_NRM].frac, 14);
}
TEST_F(GXFifoTest, VtxAttrFmt_NrmU8UsesUnsignedScale) {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_S16, 0);
(void)flush_and_capture();
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_U8, 0);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
auto& vf = g_gxState.vtxFmts[GX_VTXFMT0];
EXPECT_EQ(vf.attrs[GX_VA_NRM].cnt, GX_NRM_XYZ);
EXPECT_EQ(vf.attrs[GX_VA_NRM].type, GX_U8);
EXPECT_EQ(vf.attrs[GX_VA_NRM].frac, 7);
}
TEST_F(GXFifoTest, NormalU8DirectPreservesUnsignedRawBytes) {
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_NRM, GX_DIRECT);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_U8, 0);
aurora::gx::fifo::clear_buffer();
GXNormal3u8(0xFF, 0x80, 0x01);
const auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 3u);
EXPECT_EQ(bytes[0], 0xFF);
EXPECT_EQ(bytes[1], 0x80);
EXPECT_EQ(bytes[2], 0x01);
}
TEST_F(GXFifoTest, VtxAttrFmt_NrmNBT3PreservesIndex3Bit) {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_NBT3, GX_S16, 0);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
auto& vf = g_gxState.vtxFmts[GX_VTXFMT0];
EXPECT_EQ(vf.attrs[GX_VA_NRM].cnt, GX_NRM_NBT3);
EXPECT_EQ(vf.attrs[GX_VA_NRM].type, GX_S16);
}
TEST_F(GXFifoTest, VtxAttrFmt_Tex0_S16_Frac8) {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_S16, 8);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
auto& vf = g_gxState.vtxFmts[GX_VTXFMT0];
EXPECT_EQ(vf.attrs[GX_VA_TEX0].cnt, GX_TEX_ST);
EXPECT_EQ(vf.attrs[GX_VA_TEX0].type, GX_S16);
EXPECT_EQ(vf.attrs[GX_VA_TEX0].frac, 8);
}
TEST_F(GXFifoTest, VtxAttrFmt_Clr0_RGBA8) {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
auto& vf = g_gxState.vtxFmts[GX_VTXFMT0];
EXPECT_EQ(vf.attrs[GX_VA_CLR0].cnt, GX_CLR_RGBA);
EXPECT_EQ(vf.attrs[GX_VA_CLR0].type, GX_RGBA8);
}
TEST_F(GXFifoTest, VtxAttrFmt_MultipleTexCoords) {
GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0);
GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX1, GX_TEX_ST, GX_U16, 15);
GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX2, GX_TEX_ST, GX_S16, 8);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
auto& vf = g_gxState.vtxFmts[GX_VTXFMT1];
EXPECT_EQ(vf.attrs[GX_VA_TEX0].type, GX_F32);
EXPECT_EQ(vf.attrs[GX_VA_TEX1].type, GX_U16);
EXPECT_EQ(vf.attrs[GX_VA_TEX1].frac, 15);
EXPECT_EQ(vf.attrs[GX_VA_TEX2].type, GX_S16);
EXPECT_EQ(vf.attrs[GX_VA_TEX2].frac, 8);
}
TEST_F(GXFifoTest, GetVtxAttrFmt_UsesShadowState) {
GXSetVtxAttrFmt(GX_VTXFMT2, GX_VA_NRM, GX_NRM_NBT3, GX_S16, 0);
GXSetVtxAttrFmt(GX_VTXFMT2, GX_VA_TEX4, GX_TEX_ST, GX_U16, 11);
GXCompCnt cnt = GX_POS_XY;
GXCompType type = GX_U8;
u8 frac = 0;
GXVtxAttrFmtList vat[13]{};
GXGetVtxAttrFmt(GX_VTXFMT2, GX_VA_NRM, &cnt, &type, &frac);
EXPECT_EQ(cnt, GX_NRM_NBT3);
EXPECT_EQ(type, GX_S16);
EXPECT_EQ(frac, 14);
GXGetVtxAttrFmtv(GX_VTXFMT2, vat);
EXPECT_EQ(vat[GX_VA_NRM - GX_VA_POS].attr, GX_VA_NRM);
EXPECT_EQ(vat[GX_VA_NRM - GX_VA_POS].cnt, GX_NRM_NBT3);
EXPECT_EQ(vat[GX_VA_NRM - GX_VA_POS].type, GX_S16);
EXPECT_EQ(vat[GX_VA_TEX4 - GX_VA_POS].attr, GX_VA_TEX4);
EXPECT_EQ(vat[GX_VA_TEX4 - GX_VA_POS].type, GX_U16);
EXPECT_EQ(vat[GX_VA_TEX4 - GX_VA_POS].frac, 11);
EXPECT_EQ(vat[12].attr, GX_VA_NULL);
}
// --- GXSetArray (Aurora array-base command + CP stride command) ---
TEST_F(GXFifoTest, SetArray_Pos_EncodesAuroraArrayBaseAndStride) {
u8 posData[32]{};
u8 oldData[8]{};
GXSetArray(GX_VA_POS, posData, sizeof(posData), 12, false);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 22u);
EXPECT_EQ(bytes[0], GX_LOAD_AURORA);
EXPECT_EQ(bytes[1], 0x00);
EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE);
const auto expect_be64 = [&](size_t offset, u64 value) {
for (size_t i = 0; i < 8; ++i) {
EXPECT_EQ(bytes[offset + i], static_cast<u8>((value >> (56 - i * 8)) & 0xFF));
}
};
const auto expect_be32 = [&](size_t offset, u32 value) {
for (size_t i = 0; i < 4; ++i) {
EXPECT_EQ(bytes[offset + i], static_cast<u8>((value >> (24 - i * 8)) & 0xFF));
}
};
expect_be64(3, static_cast<u64>(reinterpret_cast<uintptr_t>(posData)));
expect_be32(11, sizeof(posData));
EXPECT_EQ(bytes[15], 0);
EXPECT_EQ(bytes[16], GX_LOAD_CP_REG);
EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE);
expect_be32(18, 12);
reset_gx_state();
gxState().arrays[GX_VA_POS].data = oldData;
gxState().arrays[GX_VA_POS].size = sizeof(oldData);
gxState().arrays[GX_VA_POS].stride = 2;
gxState().arrays[GX_VA_POS].cachedRange.offset = 4;
gxState().arrays[GX_VA_POS].cachedRange.size = 8;
gxState().stateDirty = false;
decode_fifo(bytes);
EXPECT_EQ(gxState().arrays[GX_VA_POS].data, posData);
EXPECT_EQ(gxState().arrays[GX_VA_POS].size, sizeof(posData));
EXPECT_EQ(gxState().arrays[GX_VA_POS].stride, 12);
EXPECT_FALSE(gxState().arrays[GX_VA_POS].le);
EXPECT_EQ(gxState().arrays[GX_VA_POS].cachedRange.offset, 0u);
EXPECT_EQ(gxState().arrays[GX_VA_POS].cachedRange.size, 0u);
EXPECT_TRUE(gxState().stateDirty);
}
TEST_F(GXFifoTest, SetArray_Nbt_UsesNrmCommandSlotAndState) {
u8 nbtData[96]{};
u8 untouchedData[24]{};
GXSetArray(GX_VA_NBT, nbtData, sizeof(nbtData), 36, false);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 22u);
EXPECT_EQ(bytes[0], GX_LOAD_AURORA);
EXPECT_EQ(bytes[1], 0x00);
EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE | 0x01);
EXPECT_EQ(bytes[15], 0);
EXPECT_EQ(bytes[16], GX_LOAD_CP_REG);
EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE | 0x01);
reset_gx_state();
gxState().arrays[GX_VA_NRM].cachedRange.offset = 12;
gxState().arrays[GX_VA_NRM].cachedRange.size = 48;
gxState().arrays[GX_VA_NBT].data = untouchedData;
gxState().arrays[GX_VA_NBT].size = sizeof(untouchedData);
gxState().arrays[GX_VA_NBT].stride = 24;
gxState().stateDirty = false;
decode_fifo(bytes);
EXPECT_EQ(gxState().arrays[GX_VA_NRM].data, nbtData);
EXPECT_EQ(gxState().arrays[GX_VA_NRM].size, sizeof(nbtData));
EXPECT_EQ(gxState().arrays[GX_VA_NRM].stride, 36);
EXPECT_FALSE(gxState().arrays[GX_VA_NRM].le);
EXPECT_EQ(gxState().arrays[GX_VA_NRM].cachedRange.offset, 0u);
EXPECT_EQ(gxState().arrays[GX_VA_NRM].cachedRange.size, 0u);
EXPECT_TRUE(gxState().stateDirty);
EXPECT_EQ(gxState().arrays[GX_VA_NBT].data, untouchedData);
EXPECT_EQ(gxState().arrays[GX_VA_NBT].size, sizeof(untouchedData));
EXPECT_EQ(gxState().arrays[GX_VA_NBT].stride, 24);
}
TEST_F(GXFifoTest, SetArray_LittleEndianFlag_UpdatesStateAndClearsCachedRange) {
u8 clrData[16]{};
GXSetArray(GX_VA_CLR0, clrData, sizeof(clrData), 4, true);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 22u);
EXPECT_EQ(bytes[0], GX_LOAD_AURORA);
EXPECT_EQ(bytes[1], 0x00);
EXPECT_EQ(bytes[2], GX_LOAD_AURORA_ARRAYBASE | (GX_VA_CLR0 - GX_VA_POS));
EXPECT_EQ(bytes[15], 1);
EXPECT_EQ(bytes[16], GX_LOAD_CP_REG);
EXPECT_EQ(bytes[17], GX_CP_REG_ARRAYSTRIDE | (GX_VA_CLR0 - GX_VA_POS));
reset_gx_state();
gxState().arrays[GX_VA_CLR0].data = clrData;
gxState().arrays[GX_VA_CLR0].size = sizeof(clrData);
gxState().arrays[GX_VA_CLR0].stride = 4;
gxState().arrays[GX_VA_CLR0].le = false;
gxState().arrays[GX_VA_CLR0].cachedRange.offset = 3;
gxState().arrays[GX_VA_CLR0].cachedRange.size = 9;
gxState().stateDirty = false;
decode_fifo(bytes);
EXPECT_EQ(gxState().arrays[GX_VA_CLR0].data, clrData);
EXPECT_EQ(gxState().arrays[GX_VA_CLR0].size, sizeof(clrData));
EXPECT_EQ(gxState().arrays[GX_VA_CLR0].stride, 4);
EXPECT_TRUE(gxState().arrays[GX_VA_CLR0].le);
EXPECT_EQ(gxState().arrays[GX_VA_CLR0].cachedRange.offset, 0u);
EXPECT_EQ(gxState().arrays[GX_VA_CLR0].cachedRange.size, 0u);
EXPECT_TRUE(gxState().stateDirty);
}
TEST_F(GXFifoTest, InvalidateVtxCache_DropsAllIndexedArrayUploads) {
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
gxState().arrays[i].cachedRange.offset = static_cast<u32>(i * 32);
gxState().arrays[i].cachedRange.size = static_cast<u32>(i + 1);
}
const std::vector<u8> command{GX_CMD_INVL_VC};
decode_fifo(command);
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
EXPECT_EQ(gxState().arrays[i].cachedRange.offset, 0u);
EXPECT_EQ(gxState().arrays[i].cachedRange.size, 0u);
}
}
TEST_F(GXFifoTest, LoadTexObj_EncodesSdkBpBurstAndAuroraMetadata) {
alignas(32) u8 image[64]{};
GXTexObj obj{};
GXInitTexObj(&obj, image, 8, 8, GX_TF_RGB5A3, GX_REPEAT, GX_MIRROR, GX_FALSE);
GXLoadTexObj(&obj, GX_TEXMAP2);
auto bytes = capture_fifo();
EXPECT_TRUE(has_bp_write(bytes, 0x82));
EXPECT_TRUE(has_bp_write(bytes, 0x86));
EXPECT_TRUE(has_bp_write(bytes, 0x8A));
EXPECT_TRUE(has_bp_write(bytes, 0x8E));
EXPECT_TRUE(has_bp_write(bytes, 0x92));
EXPECT_TRUE(has_bp_write(bytes, 0x96));
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ));
reset_gx_state();
decode_fifo(bytes);
const auto& slot = gxState().loadedTextures[GX_TEXMAP2];
EXPECT_EQ(slot.data, image);
EXPECT_EQ(slot.width(), 8u);
EXPECT_EQ(slot.height(), 8u);
EXPECT_EQ(slot.format(), GX_TF_RGB5A3);
EXPECT_FALSE(slot.has_mips());
EXPECT_EQ(slot.mode0 >> 24, 0x82u);
EXPECT_EQ(slot.mode1 >> 24, 0x86u);
EXPECT_EQ(slot.image0 >> 24, 0x8Au);
EXPECT_EQ(slot.image3 >> 24, 0x96u);
EXPECT_NE(slot.texObjId, 0u);
EXPECT_EQ(slot.texDataVersion, 1u);
}
TEST(GXTextureMipCount, ClampsLodToTheDimensionsFullMipChain) {
alignas(32) u8 image[64]{};
GXTexObj small{};
GXInitTexObj(&small, image, 8, 8, GX_TF_I4, GX_CLAMP, GX_CLAMP, GX_TRUE);
GXInitTexObjMaxLOD(&small, 10.0f);
const auto& smallRef = reinterpret_cast<const GXTexObj_&>(small);
EXPECT_EQ(smallRef.mip_count(), 4u);
GXTexObj large{};
GXInitTexObj(&large, image, 1024, 1024, GX_TF_I4, GX_CLAMP, GX_CLAMP, GX_TRUE);
GXInitTexObjMaxLOD(&large, 10.0f);
const auto& largeRef = reinterpret_cast<const GXTexObj_&>(large);
EXPECT_EQ(largeRef.mip_count(), 11u);
}
TEST_F(GXFifoTest, LoadTexObjPcFormat_PreservesFullFormatMetadata) {
alignas(32) u8 image[64]{};
GXTexObj obj{};
GXInitTexObj(&obj, image, 8, 8, GX_TF_RGBA8_PC, GX_REPEAT, GX_REPEAT, GX_FALSE);
EXPECT_EQ(GXGetTexObjFmt(&obj), GX_TF_RGBA8_PC);
GXLoadTexObj(&obj, GX_TEXMAP3);
auto bytes = capture_fifo();
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ));
reset_gx_state();
decode_fifo(bytes);
const auto& slot = gxState().loadedTextures[GX_TEXMAP3];
EXPECT_EQ(slot.width(), 8u);
EXPECT_EQ(slot.height(), 8u);
EXPECT_EQ(slot.format(), GX_TF_RGBA8_PC);
EXPECT_EQ(slot.raw_format(), static_cast<u32>(GX_TF_RGBA8));
}
TEST_F(GXFifoTest, RawDrawDrainsQueuedMaterialStateWithoutDeferredDirtyBits) {
__GXSetDirtyState();
aurora::gx::fifo::clear_buffer();
constexpr GXColor color = {200, 100, 50, 255};
GXSetTevColor(GX_TEVREG0, color);
// TEV register writes are complete FIFO commands, not deferred SDK state.
// They therefore leave dirtyState clear while still preceding the draw.
ASSERT_EQ(__gx->dirtyState, 0u);
ASSERT_GT(aurora::gx::fifo::get_buffer_size(), 0u);
// The raw bridge receives vertices that are already packed according to the
// active VAT. Seed its size cache so this test can isolate FIFO ordering.
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
const std::array<u8, 4> vertices{};
ASSERT_TRUE(aurora::gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, vertices.data(), 4,
static_cast<uint32_t>(vertices.size())));
EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][0], 200.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][1], 100.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][2], 50.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.colorRegs[GX_TEVREG0][3], 255.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, DisplayListCallWhileRecordingInlinesNestedCommands) {
std::array<u8, 64> outer{};
const std::array<u8, 3> nested{GX_NOP, GX_NOP, GX_NOP};
__GXSetDirtyState();
aurora::gx::fifo::clear_buffer();
GXBeginDisplayList(outer.data(), static_cast<u32>(outer.size()));
GXCallDisplayList(nested.data(), static_cast<u32>(nested.size()));
const u32 bytes = GXEndDisplayList();
EXPECT_EQ(bytes, 32u);
EXPECT_TRUE(std::equal(nested.begin(), nested.end(), outer.begin()));
EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u);
}
TEST_F(GXFifoTest, DirectEfbCopiesDrainQueuedCommands) {
aurora::gx::fifo::write_u8(GX_NOP);
ASSERT_GT(aurora::gx::fifo::get_buffer_size(), 0u);
GXCopyDisp(nullptr, GX_FALSE);
EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u);
std::array<u8, 64> copyDest{};
aurora::gx::fifo::write_u8(GX_NOP);
ASSERT_GT(aurora::gx::fifo::get_buffer_size(), 0u);
GXCopyTex(copyDest.data(), GX_FALSE);
EXPECT_EQ(aurora::gx::fifo::get_buffer_size(), 0u);
}
TEST_F(GXFifoTest, RawDrawPreservesHorizontalXzQuadVertices) {
__GXSetDirtyState();
aurora::gx::fifo::clear_buffer();
aurora::gfx::testing::use_real_vertex_format_helpers(true);
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 14;
g_gxState.vtxDesc[GX_VA_POS] = GX_DIRECT;
g_gxState.vtxDesc[GX_VA_TEX0] = GX_DIRECT;
auto& fmt = g_gxState.vtxFmts[GX_VTXFMT0];
fmt.attrs[GX_VA_POS].cnt = GX_POS_XYZ;
fmt.attrs[GX_VA_POS].type = GX_F32;
fmt.attrs[GX_VA_TEX0].cnt = GX_TEX_ST;
fmt.attrs[GX_VA_TEX0].type = GX_U8;
std::vector<u8> vertices;
const auto append_f32_be = [&](float value) {
u32 bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
vertices.push_back(static_cast<u8>(bits >> 24));
vertices.push_back(static_cast<u8>(bits >> 16));
vertices.push_back(static_cast<u8>(bits >> 8));
vertices.push_back(static_cast<u8>(bits));
};
const auto append_vertex = [&](float x, float y, float z, u8 s, u8 t) {
append_f32_be(x);
append_f32_be(y);
append_f32_be(z);
vertices.push_back(s);
vertices.push_back(t);
};
append_vertex(-53.659431f, 0.27f, -53.659435f, 0, 1);
append_vertex(-53.659431f, 0.27f, 53.659435f, 0, 0);
append_vertex(53.659431f, 0.27f, 53.659435f, 1, 0);
append_vertex(53.659431f, 0.27f, -53.659435f, 1, 1);
const auto expected = vertices;
ASSERT_TRUE(aurora::gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, vertices.data(), 4,
static_cast<uint32_t>(vertices.size())));
EXPECT_EQ(aurora::gfx::testing::last_pushed_vertices(), expected);
}
static void append_test_draw(std::vector<u8>& fifo, GXPrimitive primitive, u16 count) {
fifo.push_back(static_cast<u8>(primitive) | static_cast<u8>(GX_VTXFMT0));
fifo.push_back(static_cast<u8>(count >> 8));
fifo.push_back(static_cast<u8>(count));
for (u16 vertex = 0; vertex < count; ++vertex) {
fifo.push_back(static_cast<u8>(vertex));
}
}
TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
g_gxState.stateDirty = true;
const auto decodeAndReadIndices = [&](GXPrimitive primitive, u16 count) {
std::vector<u8> fifo;
append_test_draw(fifo, primitive, count);
aurora::gfx::testing::reset_vertex_push_record();
decode_fifo(fifo);
return aurora::gfx::testing::last_pushed_indices();
};
EXPECT_EQ(decodeAndReadIndices(GX_QUADS, 8), (std::vector<u16>{0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4}));
g_gxState.stateDirty = true;
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLES, 6), (std::vector<u16>{0, 1, 2, 3, 4, 5}));
g_gxState.stateDirty = true;
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 5), (std::vector<u16>{0, 1, 2, 0, 2, 3, 0, 3, 4}));
g_gxState.stateDirty = true;
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2), (std::vector<u16>{}));
g_gxState.stateDirty = true;
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLESTRIP, 6), (std::vector<u16>{0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5}));
g_gxState.stateDirty = true;
EXPECT_TRUE(decodeAndReadIndices(GX_TRIANGLESTRIP, 0).empty());
g_gxState.stateDirty = true;
EXPECT_EQ(decodeAndReadIndices(GX_LINES, 2), (std::vector<u16>{0, 1, 3, 3, 2, 0}));
}
TEST_F(GXFifoTest, MergedDrawOffsetsCachedTopologyWithoutJoiningPrimitives) {
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
g_gxState.stateDirty = true;
aurora::gfx::testing::use_draw_command_tracking(true);
std::vector<u8> fifo;
append_test_draw(fifo, GX_TRIANGLES, 3);
append_test_draw(fifo, GX_TRIANGLES, 3);
decode_fifo(fifo);
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 1u);
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{3, 4, 5}));
}
TEST_F(GXFifoTest, VrDirectParticleQuadsRetainSeparateCentersInFifoAndRawDraws) {
struct Reset {
~Reset() {
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::begin_frame_interpolation();
}
} reset;
aurora::gx::detail::g_stereoFrameInterpolation.store(true);
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::use_real_vertex_format_helpers(true);
aurora::gfx::testing::use_draw_command_tracking(true);
gxState().projType = GX_PERSPECTIVE;
gxState().vtxDesc[GX_VA_POS] = GX_DIRECT;
gxState().vtxFmts[GX_VTXFMT0].attrs[GX_VA_POS] = {GX_POS_XYZ, GX_F32, 0};
gxState().pnMtx[0].pos = gxState().pnMtx[0].nrm = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
gxState().stateDirty = true;
const auto makeVertices = [](float x) {
std::vector<uint8_t> vertices;
for (const auto& point : {std::array{x - 5, -5.f, -200.f}, std::array{x + 5, -5.f, -200.f},
std::array{x + 5, 5.f, -200.f}, std::array{x - 5, 5.f, -200.f}}) {
for (float component : point) {
uint32_t bits;
std::memcpy(&bits, &component, sizeof(bits));
for (int shift : {24, 16, 8, 0}) vertices.push_back(static_cast<uint8_t>(bits >> shift));
}
}
return vertices;
};
std::vector<uint8_t> commands;
for (float x : {20.f, 100.f}) {
commands.insert(commands.end(), {static_cast<uint8_t>(GX_QUADS), 0, 4});
const auto vertices = makeVertices(x);
commands.insert(commands.end(), vertices.begin(), vertices.end());
}
decode_fifo(commands);
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
auto* draw = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(draw, nullptr);
EXPECT_TRUE(draw->uniformReplayLayout.vertexMotion.enabled);
EXPECT_EQ(draw->uniformReplayLayout.vertexMotion.center, (std::array{100.f, 0.f, -200.f}));
const auto raw = makeVertices(300);
ASSERT_TRUE(aurora::gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, raw.data(), 4, raw.size()));
draw = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(draw, nullptr);
EXPECT_TRUE(draw->uniformReplayLayout.vertexMotion.enabled);
EXPECT_EQ(draw->uniformReplayLayout.vertexMotion.center, (std::array{300.f, 0.f, -200.f}));
// Turning VR interpolation off restores ordinary batching.
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
gxState().stateDirty = true;
decode_fifo(commands);
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 1u);
}
TEST_F(GXFifoTest, InterpolationOffRecordsNothingForParticlesOrMatching) {
// The Quest ships with VR interpolation off. Those frames must not pay for any
// of it: no draw is recorded, no particle quad is tracked, nothing is matched.
struct Reset {
~Reset() { aurora::gx::begin_frame_interpolation(); }
} reset;
aurora::gx::detail::g_stereoFrameInterpolation.store(false);
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
aurora::gfx::testing::use_real_vertex_format_helpers(true);
aurora::gfx::testing::use_draw_command_tracking(true);
gxState().projType = GX_PERSPECTIVE;
gxState().vtxDesc[GX_VA_POS] = GX_DIRECT;
gxState().vtxFmts[GX_VTXFMT0].attrs[GX_VA_POS] = {GX_POS_XYZ, GX_F32, 0};
gxState().pnMtx[0].pos = gxState().pnMtx[0].nrm = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
gxState().stateDirty = true;
std::vector<uint8_t> commands;
for (float x : {20.f, 100.f, 180.f}) {
commands.insert(commands.end(), {static_cast<uint8_t>(GX_QUADS), 0, 4});
for (const auto& point : {std::array{x - 5, -5.f, -200.f}, std::array{x + 5, -5.f, -200.f},
std::array{x + 5, 5.f, -200.f}, std::array{x - 5, 5.f, -200.f}}) {
for (float component : point) {
uint32_t bits;
std::memcpy(&bits, &component, sizeof(bits));
for (int shift : {24, 16, 8, 0}) commands.push_back(static_cast<uint8_t>(bits >> shift));
}
}
}
decode_fifo(commands);
aurora::gx::finalize_frame_interpolation();
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 2u); // Ordinary batching.
auto* draw = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(draw, nullptr);
EXPECT_FALSE(draw->uniformReplayLayout.vertexMotion.enabled);
AuroraFrameInterpolationDiagnostics diagnostics{};
aurora::gx::get_frame_interpolation_diagnostics(diagnostics);
EXPECT_EQ(diagnostics.candidates, 0u);
EXPECT_EQ(diagnostics.matches, 0u);
EXPECT_EQ(diagnostics.preparedDraws, 0u);
EXPECT_EQ(diagnostics.vertexMotionDraws, 0u);
EXPECT_EQ(diagnostics.vertexMotionHeld, 0u);
EXPECT_FALSE(aurora::gx::has_interpolated_frame());
}
TEST_F(GXFifoTest, OrthographicQuadRecordsScreenRectForVrFurniture) {
// MKW draws its split-screen partition with the partition_line layout: a
// one-pixel picture pane sampling a pattern texture in a full-display
// orthographic viewport. VR replay drops it by its recorded screen
// rectangle. This covers the FIFO decode of that rectangle. The harness
// stubs populate_pipeline_config, so whether texture use disqualifies a
// rectangle is exercised by stereo_multiplayer_smoke instead.
aurora::gfx::testing::use_real_vertex_format_helpers(true);
aurora::gfx::testing::use_draw_command_tracking(true);
aurora::Mat4x4<float> proj{};
proj.m0[0] = 2.0f / 640.0f;
proj.m0[3] = -1.0f;
proj.m1[1] = 2.0f / 480.0f;
proj.m1[3] = -1.0f;
proj.m2[2] = -1.0f;
proj.m3[3] = 1.0f;
GXSetProjection(&proj, GX_ORTHOGRAPHIC);
GXSetViewport(0.0f, 0.0f, 640.0f, 480.0f, 0.0f, 1.0f);
GXSetScissor(0, 0, 640, 480);
aurora::Mat3x4<float> identity{};
identity.m0[0] = identity.m1[1] = identity.m2[2] = 1.0f;
GXLoadPosMtxImm(&identity, GX_PNMTX0);
GXSetCurrentMtx(GX_PNMTX0);
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0);
GXSetNumChans(1);
GXSetNumTexGens(1);
GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
GXSetNumTevStages(1);
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0);
GXSetTevOp(GX_TEVSTAGE0, GX_MODULATE);
alignas(32) static const u8 pattern[8 * 8 * 2]{};
GXTexObj obj{};
GXInitTexObj(&obj, pattern, 8, 8, GX_TF_RGB565, GX_CLAMP, GX_CLAMP, GX_FALSE);
GXLoadTexObj(&obj, GX_TEXMAP0);
// yoko_line: full width, one pixel tall, centred vertically.
const float corners[4][2]{{0.0f, 239.5f}, {640.0f, 239.5f}, {640.0f, 240.5f}, {0.0f, 240.5f}};
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
for (const auto& corner : corners) {
GXPosition3f32(corner[0], corner[1], 0.0f);
GXColor4u8(0, 0, 0, 255);
GXTexCoord2f32(corner[0] / 640.0f, corner[1] > 240.0f ? 1.0f : 0.0f);
}
GXEnd();
decode_fifo(flush_and_capture());
const auto* draw = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(draw, nullptr);
ASSERT_TRUE(draw->screenRect.has_value());
EXPECT_NEAR(draw->screenRect->left, 0.0f, 0.01f);
EXPECT_NEAR(draw->screenRect->top, 239.5f, 0.01f);
EXPECT_NEAR(draw->screenRect->width, 640.0f, 0.01f);
EXPECT_NEAR(draw->screenRect->height, 1.0f, 0.01f);
EXPECT_TRUE(
aurora::gfx::stereo_replay::is_split_screen_furniture(*draw->screenRect, {0.0f, 0.0f, 640.0f, 480.0f}, 2));
}
TEST_F(GXFifoTest, TexBufferSize_UsesExactLinearPcFormatSizes) {
EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_R8_PC, GX_FALSE, 0), 32u);
EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_RGBA8_PC, GX_FALSE, 0), 128u);
EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_R8_PC, GX_TRUE, 2), 42u);
EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_RGBA8_PC, GX_TRUE, 2), 168u);
}
TEST_F(GXFifoTest, InvalidateTexAll_EmitsAuroraCacheInvalidationAndDirtiesState) {
GXInvalidateTexAll();
auto bytes = capture_fifo();
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_INVALIDATE_TEX_ALL));
reset_gx_state();
gxState().stateDirty = false;
decode_fifo(bytes);
EXPECT_TRUE(gxState().stateDirty);
}
TEST_F(GXFifoTest, TexObjRawDimensions_WrapAtTenBitBoundary) {
auto& slot = gxState().loadedTextures[GX_TEXMAP0];
slot.image0 = (0x3FFu << 0) | (0x3FFu << 10);
slot.mWidth = 0;
slot.mHeight = 0;
EXPECT_EQ(slot.width(), 0u);
EXPECT_EQ(slot.height(), 0u);
}
TEST_F(GXFifoTest, TexObjExplicitDimensions_DoNotWrapAtTenBitBoundary) {
auto& slot = gxState().loadedTextures[GX_TEXMAP0];
slot.image0 = (0x3FFu << 0) | (0x3FFu << 10);
slot.mWidth = 1024;
slot.mHeight = 1024;
EXPECT_EQ(slot.width(), 1024u);
EXPECT_EQ(slot.height(), 1024u);
}
TEST_F(GXFifoTest, LoadTexObjCiAndTlut_PopulatesTextureAndTlutSlots) {
alignas(32) u8 image[64]{};
alignas(32) u16 palette[16]{};
GXTexObj texObj{};
GXTlutObj tlutObj{};
GXInitTexObjCI(&texObj, image, 8, 8, GX_TF_C4, GX_CLAMP, GX_CLAMP, GX_FALSE, GX_TLUT3);
GXInitTlutObj(&tlutObj, palette, GX_TL_RGB565, 16);
GXLoadTexObj(&texObj, GX_TEXMAP1);
GXLoadTlut(&tlutObj, GX_TLUT3);
auto bytes = capture_fifo();
EXPECT_TRUE(has_bp_write(bytes, 0x81));
EXPECT_TRUE(has_bp_write(bytes, 0x85));
EXPECT_TRUE(has_bp_write(bytes, 0x89));
EXPECT_TRUE(has_bp_write(bytes, 0x8D));
EXPECT_TRUE(has_bp_write(bytes, 0x91));
EXPECT_TRUE(has_bp_write(bytes, 0x95));
EXPECT_TRUE(has_bp_write(bytes, 0x99));
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TEXOBJ));
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_TLUT));
reset_gx_state();
decode_fifo(bytes);
const auto& texSlot = gxState().loadedTextures[GX_TEXMAP1];
EXPECT_EQ(texSlot.data, image);
EXPECT_EQ(texSlot.width(), 8u);
EXPECT_EQ(texSlot.height(), 8u);
EXPECT_EQ(texSlot.format(), GX_TF_C4);
EXPECT_EQ(texSlot.tlut, GX_TLUT3);
const auto& tlutSlot = gxState().loadedTluts[GX_TLUT3];
EXPECT_EQ(tlutSlot.data, palette);
EXPECT_EQ(tlutSlot.format, GX_TL_RGB565);
EXPECT_EQ(tlutSlot.numEntries, 16u);
EXPECT_NE(tlutSlot.tlutObjId, 0u);
EXPECT_EQ(tlutSlot.tlutDataVersion, 1u);
}
TEST_F(GXFifoTest, DestroyTexObj_DoesNotEvictTextureDataAndClearsIdentity) {
alignas(32) u8 image[64]{};
GXTexObj obj{};
GXInitTexObj(&obj, image, 8, 8, GX_TF_RGB5A3, GX_REPEAT, GX_REPEAT, GX_FALSE);
GXDestroyTexObj(&obj);
auto bytes = capture_fifo();
EXPECT_FALSE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_TEXOBJ));
EXPECT_EQ(reinterpret_cast<const GXTexObj_*>(&obj)->texObjId, 0u);
reset_gx_state();
decode_fifo(bytes);
}
TEST_F(GXFifoTest, DestroyTexObj_DoesNotPoisonLoadedSlotCache) {
alignas(32) u8 imageA[64]{};
alignas(32) u8 imageB[64]{};
GXTexObj objA{};
GXTexObj objB{};
GXInitTexObj(&objA, imageA, 8, 8, GX_TF_RGB5A3, GX_REPEAT, GX_REPEAT, GX_FALSE);
GXLoadTexObj(&objA, GX_TEXMAP2);
auto loadABytes = capture_fifo();
const auto destroyedTexObjId = reinterpret_cast<const GXTexObj_*>(&objA)->texObjId;
GXDestroyTexObj(&objA);
auto destroyBytes = capture_fifo();
GXInitTexObj(&objB, imageB, 8, 8, GX_TF_RGB565, GX_CLAMP, GX_CLAMP, GX_FALSE);
GXLoadTexObj(&objB, GX_TEXMAP2);
auto loadBBytes = capture_fifo();
reset_gx_state();
decode_fifo(loadABytes);
auto& slot = gxState().loadedTextures[GX_TEXMAP2];
EXPECT_EQ(slot.texObjId, destroyedTexObjId);
EXPECT_FALSE(slot.no_cache());
decode_fifo(destroyBytes);
EXPECT_EQ(slot.texObjId, destroyedTexObjId);
EXPECT_FALSE(slot.no_cache());
decode_fifo(loadBBytes);
EXPECT_EQ(slot.data, imageB);
EXPECT_EQ(slot.format(), GX_TF_RGB565);
EXPECT_FALSE(slot.no_cache());
}
TEST_F(GXFifoTest, DestroyTlutObj_EmitsAuroraDestroyCommandAndClearsIdentity) {
alignas(32) u16 palette[16]{};
GXTlutObj obj{};
GXInitTlutObj(&obj, palette, GX_TL_RGB565, 16);
GXDestroyTlutObj(&obj);
auto bytes = capture_fifo();
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_TLUT));
EXPECT_EQ(reinterpret_cast<const GXTlutObj_*>(&obj)->tlutObjId, 0u);
reset_gx_state();
decode_fifo(bytes);
}
TEST_F(GXFifoTest, DestroyTlutObj_MarksLoadedSlotNoCacheUntilReloaded) {
alignas(32) u16 paletteA[16]{};
alignas(32) u16 paletteB[16]{};
GXTlutObj objA{};
GXTlutObj objB{};
GXInitTlutObj(&objA, paletteA, GX_TL_RGB565, 16);
GXLoadTlut(&objA, GX_TLUT3);
auto loadABytes = capture_fifo();
const auto destroyedTlutObjId = reinterpret_cast<const GXTlutObj_*>(&objA)->tlutObjId;
GXDestroyTlutObj(&objA);
auto destroyBytes = capture_fifo();
GXInitTlutObj(&objB, paletteB, GX_TL_IA8, 16);
GXLoadTlut(&objB, GX_TLUT3);
auto loadBBytes = capture_fifo();
reset_gx_state();
decode_fifo(loadABytes);
auto& slot = gxState().loadedTluts[GX_TLUT3];
EXPECT_EQ(slot.tlutObjId, destroyedTlutObjId);
EXPECT_FALSE(slot.no_cache());
decode_fifo(destroyBytes);
EXPECT_EQ(slot.tlutObjId, destroyedTlutObjId);
EXPECT_TRUE(slot.no_cache());
decode_fifo(loadBBytes);
EXPECT_EQ(slot.data, paletteB);
EXPECT_EQ(slot.format, GX_TL_IA8);
EXPECT_FALSE(slot.no_cache());
}
TEST_F(GXFifoTest, DestroyCopyTex_EmitsAuroraDestroyCommand) {
alignas(32) u8 image[32]{};
GXDestroyCopyTex(image);
auto bytes = capture_fifo();
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_DESTROY_COPY_TEX));
reset_gx_state();
decode_fifo(bytes);
}
TEST_F(GXFifoTest, DestroyCopyTex_RemovesActiveCopyTextureAndCacheEntriesForPointer) {
alignas(32) u8 imageA[32]{};
alignas(32) u8 imageB[32]{};
const aurora::gx::GXState::CopyTextureRef ref{.revision = 1};
gxState().copyTextures[imageA] = ref;
gxState().copyTextures[imageB] = ref;
gxState().copyTextureCache.emplace(
aurora::gx::GXState::CopyTextureKey{.dest = imageA, .width = 32, .height = 32, .format = GX_TF_I4}, ref);
gxState().copyTextureCache.emplace(
aurora::gx::GXState::CopyTextureKey{.dest = imageA, .width = 64, .height = 64, .format = GX_TF_I8}, ref);
gxState().copyTextureCache.emplace(
aurora::gx::GXState::CopyTextureKey{.dest = imageB, .width = 32, .height = 32, .format = GX_TF_I4}, ref);
GXDestroyCopyTex(imageA);
auto bytes = capture_fifo();
decode_fifo(bytes);
EXPECT_FALSE(gxState().copyTextures.contains(imageA));
EXPECT_TRUE(gxState().copyTextures.contains(imageB));
for (const auto& [key, _] : gxState().copyTextureCache) {
EXPECT_NE(key.dest, imageA);
}
EXPECT_EQ(gxState().copyTextureCache.size(), 1u);
}
// BP genMode (requires __GXSetDirtyState() flush)
// --- GXSetCullMode ---
TEST_F(GXFifoTest, CullMode_Back) {
GXSetCullMode(GX_CULL_BACK);
auto bytes = flush_and_capture();
reset_gx_state();
g_gxState.cullMode = GX_CULL_NONE;
decode_fifo(bytes);
// The encoder swaps front/back for hardware, and decoder swaps back
EXPECT_EQ(g_gxState.cullMode, GX_CULL_BACK);
}
TEST_F(GXFifoTest, CullMode_Front) {
GXSetCullMode(GX_CULL_FRONT);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_FRONT);
}
TEST_F(GXFifoTest, CullMode_None) {
GXSetCullMode(GX_CULL_NONE);
auto bytes = flush_and_capture();
reset_gx_state();
g_gxState.cullMode = GX_CULL_BACK;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
}
TEST_F(GXFifoTest, GetLinePointCullShadowState) {
GXSetLineWidth(12, GX_TO_ZERO);
GXSetPointSize(34, GX_TO_ONE);
GXSetCullMode(GX_CULL_FRONT);
u8 lineWidth = 0;
u8 pointSize = 0;
GXTexOffset lineOffs = GX_TO_ZERO;
GXTexOffset pointOffs = GX_TO_ZERO;
GXCullMode cullMode = GX_CULL_NONE;
GXGetLineWidth(&lineWidth, &lineOffs);
GXGetPointSize(&pointSize, &pointOffs);
GXGetCullMode(&cullMode);
EXPECT_EQ(lineWidth, 12);
EXPECT_EQ(lineOffs, GX_TO_ZERO);
EXPECT_EQ(pointSize, 34);
EXPECT_EQ(pointOffs, GX_TO_ONE);
EXPECT_EQ(cullMode, GX_CULL_FRONT);
}
TEST_F(GXFifoTest, LinePointSize_Decode) {
GXSetLineWidth(12, GX_TO_ZERO);
GXSetPointSize(34, GX_TO_ONE);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 10u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0x22);
EXPECT_EQ(bytes[5], 0x61);
EXPECT_EQ(bytes[6], 0x22);
reset_gx_state();
g_gxState.lineWidth = 1;
g_gxState.pointSize = 2;
g_gxState.lineTexOffset = GX_TO_ONE;
g_gxState.pointTexOffset = GX_TO_ZERO;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.lineWidth, 12u);
EXPECT_EQ(g_gxState.lineTexOffset, GX_TO_ZERO);
EXPECT_EQ(g_gxState.pointSize, 34u);
EXPECT_EQ(g_gxState.pointTexOffset, GX_TO_ONE);
}
// --- GXSetNumTevStages / GXSetNumTexGens / GXSetNumChans ---
TEST_F(GXFifoTest, NumTevStages) {
GXSetNumTevStages(4);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.numTevStages, 4u);
}
TEST_F(GXFifoTest, NumTexGens) {
GXSetNumTexGens(3);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.numTexGens, 3u);
}
TEST_F(GXFifoTest, NumChans) {
GXSetNumChans(2);
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.numChans, 2u);
}
// XF registers (direct FIFO writes)
// --- GXLoadPosMtxImm (XF 0x000-0x077) ---
TEST_F(GXFifoTest, LoadPosMtxImm_Identity) {
// 3x4 identity matrix
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 1.0f;
mtx.m1[1] = 1.0f;
mtx.m2[2] = 1.0f;
GXLoadPosMtxImm(&mtx, GX_PNMTX0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.pnMtx[0].pos;
EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f);
EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f);
}
TEST_F(GXFifoTest, LoadPosMtxImm_Translation) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 1.0f;
mtx.m1[1] = 1.0f;
mtx.m2[2] = 1.0f;
mtx.m0[3] = 10.0f;
mtx.m1[3] = 20.0f;
mtx.m2[3] = 30.0f;
GXLoadPosMtxImm(&mtx, GX_PNMTX3);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.pnMtx[3].pos;
EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 10.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 20.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 30.0f);
}
// --- GXLoadNrmMtxImm (XF 0x400-0x459) ---
TEST_F(GXFifoTest, LoadNrmMtxImm_Identity) {
// 3x4 matrix with 3x3 identity (translation column ignored by encoder)
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 1.0f;
mtx.m1[1] = 1.0f;
mtx.m2[2] = 1.0f;
GXLoadNrmMtxImm(&mtx, GX_PNMTX0);
auto bytes = capture_fifo();
// XF opcode 0x10
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x10);
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.pnMtx[0].nrm;
EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f);
EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f);
}
TEST_F(GXFifoTest, LoadNrmMtxImm_ArbitraryValues) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 0.5f;
mtx.m0[1] = -0.5f;
mtx.m0[2] = 0.7f;
mtx.m0[3] = 999.0f;
mtx.m1[0] = 0.3f;
mtx.m1[1] = 0.8f;
mtx.m1[2] = -0.1f;
mtx.m1[3] = 888.0f;
mtx.m2[0] = -0.6f;
mtx.m2[1] = 0.2f;
mtx.m2[2] = 0.9f;
mtx.m2[3] = 777.0f;
GXLoadNrmMtxImm(&mtx, GX_PNMTX0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.pnMtx[0].nrm;
// 3x3 portion should round-trip
EXPECT_FLOAT_EQ(decoded.m0[0], 0.5f);
EXPECT_FLOAT_EQ(decoded.m0[1], -0.5f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.7f);
EXPECT_FLOAT_EQ(decoded.m1[0], 0.3f);
EXPECT_FLOAT_EQ(decoded.m1[1], 0.8f);
EXPECT_FLOAT_EQ(decoded.m1[2], -0.1f);
EXPECT_FLOAT_EQ(decoded.m2[0], -0.6f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.2f);
EXPECT_FLOAT_EQ(decoded.m2[2], 0.9f);
// Translation column is NOT written by the encoder, so it stays zeroed
EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f);
}
TEST_F(GXFifoTest, LoadNrmMtxImm_DifferentSlot) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 2.0f;
mtx.m1[1] = 3.0f;
mtx.m2[2] = 4.0f;
GXLoadNrmMtxImm(&mtx, GX_PNMTX3);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.pnMtx[3].nrm;
EXPECT_FLOAT_EQ(decoded.m0[0], 2.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 3.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 4.0f);
}
TEST_F(GXFifoTest, LoadNrmMtxImm_Isolation) {
// Loading nrm into slot 0 should not affect slot 1 or the pos matrix
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 11.0f;
mtx.m1[1] = 22.0f;
mtx.m2[2] = 33.0f;
GXLoadNrmMtxImm(&mtx, GX_PNMTX0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// Slot 0 nrm should have our values
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m0[0], 11.0f);
// Slot 0 pos should remain zeroed (nrm write doesn't touch pos)
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m0[0], 0.0f);
// Slot 1 nrm should remain zeroed
EXPECT_FLOAT_EQ(g_gxState.pnMtx[1].nrm.m0[0], 0.0f);
}
TEST_F(GXFifoTest, LoadNrmMtxImm_WithPosMtx) {
// Load both pos and nrm into the same slot, verify both decode correctly
aurora::Mat3x4<float> posMtx{};
posMtx.m0[0] = 1.0f;
posMtx.m1[1] = 1.0f;
posMtx.m2[2] = 1.0f;
posMtx.m0[3] = 5.0f;
posMtx.m1[3] = 10.0f;
posMtx.m2[3] = 15.0f;
aurora::Mat3x4<float> nrmMtx{};
nrmMtx.m0[0] = 0.5f;
nrmMtx.m1[1] = 0.5f;
nrmMtx.m2[2] = 0.5f;
GXLoadPosMtxImm(&posMtx, GX_PNMTX0);
GXLoadNrmMtxImm(&nrmMtx, GX_PNMTX0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// Position matrix
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m0[0], 1.0f);
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m0[3], 5.0f);
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m1[3], 10.0f);
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].pos.m2[3], 15.0f);
// Normal matrix
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m0[0], 0.5f);
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m1[1], 0.5f);
EXPECT_FLOAT_EQ(g_gxState.pnMtx[0].nrm.m2[2], 0.5f);
}
// --- GXLoadTexMtxImm 3x4 (XF 0x078-0x0EF) ---
TEST_F(GXFifoTest, LoadTexMtx3x4_Identity) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 1.0f;
mtx.m1[1] = 1.0f;
mtx.m2[2] = 1.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX3x4);
auto bytes = capture_fifo();
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x10);
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.texMtxs[0];
EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f);
EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f);
}
TEST_F(GXFifoTest, LoadTexMtx3x4_ArbitraryValues) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 2.0f;
mtx.m0[1] = 0.5f;
mtx.m0[2] = 0.0f;
mtx.m0[3] = 10.0f;
mtx.m1[0] = -0.5f;
mtx.m1[1] = 3.0f;
mtx.m1[2] = 0.0f;
mtx.m1[3] = 20.0f;
mtx.m2[0] = 0.0f;
mtx.m2[1] = 0.0f;
mtx.m2[2] = 1.5f;
mtx.m2[3] = -5.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.texMtxs[0];
EXPECT_FLOAT_EQ(decoded.m0[0], 2.0f);
EXPECT_FLOAT_EQ(decoded.m0[1], 0.5f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 10.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], -0.5f);
EXPECT_FLOAT_EQ(decoded.m1[1], 3.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 20.0f);
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 1.5f);
EXPECT_FLOAT_EQ(decoded.m2[3], -5.0f);
}
TEST_F(GXFifoTest, LoadTexMtx3x4_DifferentSlot) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 7.0f;
mtx.m1[1] = 8.0f;
mtx.m2[2] = 9.0f;
mtx.m2[3] = 42.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX5, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// GX_TEXMTX5 = 45, addr = 45*4 = 180 = 0xB4, index = (0xB4 - 0x78) / 12 = 5
auto& decoded = g_gxState.texMtxs[5];
EXPECT_FLOAT_EQ(decoded.m0[0], 7.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 8.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 9.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 42.0f);
}
TEST_F(GXFifoTest, LoadTexMtx3x4_LastSlot) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 11.0f;
mtx.m1[1] = 22.0f;
mtx.m2[2] = 33.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX9, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.texMtxs[9];
EXPECT_FLOAT_EQ(decoded.m0[0], 11.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 22.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 33.0f);
}
TEST_F(GXFifoTest, LoadTexMtx3x4_Isolation) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 50.0f;
mtx.m1[1] = 60.0f;
mtx.m2[2] = 70.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_FLOAT_EQ(g_gxState.texMtxs[0].m0[0], 50.0f);
// Slot 1 should remain zeroed
EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m0[0], 0.0f);
EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m1[1], 0.0f);
}
// --- GXLoadTexMtxImm 2x4 (XF 0x078-0x0EF) ---
TEST_F(GXFifoTest, LoadTexMtx2x4_Identity) {
// 2x4 identity: row0 = [1,0,0,0], row1 = [0,1,0,0]
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 1.0f;
mtx.m1[1] = 1.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX2x4);
auto bytes = capture_fifo();
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x10);
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.texMtxs[0];
// First two rows should round-trip
EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f);
EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f);
// Third row not written by 2x4, should be zeroed
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f);
}
TEST_F(GXFifoTest, LoadTexMtx2x4_ArbitraryValues) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 0.5f;
mtx.m0[1] = -1.0f;
mtx.m0[2] = 0.25f;
mtx.m0[3] = 100.0f;
mtx.m1[0] = 3.0f;
mtx.m1[1] = 0.0f;
mtx.m1[2] = -2.5f;
mtx.m1[3] = -50.0f;
// Row 2 values should be ignored by the encoder
mtx.m2[0] = 999.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX2x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.texMtxs[0];
EXPECT_FLOAT_EQ(decoded.m0[0], 0.5f);
EXPECT_FLOAT_EQ(decoded.m0[1], -1.0f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.25f);
EXPECT_FLOAT_EQ(decoded.m0[3], 100.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], 3.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], -2.5f);
EXPECT_FLOAT_EQ(decoded.m1[3], -50.0f);
// Row 2 should be zeroed (only 8 floats written)
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
}
TEST_F(GXFifoTest, LoadTexMtx2x4_DifferentSlot) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 4.0f;
mtx.m0[3] = 15.0f;
mtx.m1[1] = 5.0f;
mtx.m1[3] = 25.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX3, GX_MTX2x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.texMtxs[3];
EXPECT_FLOAT_EQ(decoded.m0[0], 4.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 15.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 5.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 25.0f);
}
TEST_F(GXFifoTest, LoadTexMtx2x4_Isolation) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 10.0f;
mtx.m1[1] = 20.0f;
GXLoadTexMtxImm(&mtx, GX_TEXMTX0, GX_MTX2x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_FLOAT_EQ(g_gxState.texMtxs[0].m0[0], 10.0f);
EXPECT_FLOAT_EQ(g_gxState.texMtxs[0].m1[1], 20.0f);
// Slot 1 should remain zeroed
EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m0[0], 0.0f);
EXPECT_FLOAT_EQ(g_gxState.texMtxs[1].m1[1], 0.0f);
}
// --- GXSetProjection (XF 0x1020-0x1026) ---
TEST_F(GXFifoTest, Projection_Perspective) {
aurora::Mat4x4<float> proj{};
proj.m0[0] = 1.5f; // near / (right - left) * 2
proj.m0[2] = 0.1f;
proj.m1[1] = 2.0f; // near / (top - bottom) * 2
proj.m1[2] = 0.2f;
proj.m2[2] = -1.002f;
proj.m2[3] = -0.2002f;
proj.m3[2] = -1.0f;
GXSetProjection(&proj, GX_PERSPECTIVE);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.projType, GX_PERSPECTIVE);
EXPECT_FLOAT_EQ(g_gxState.proj.m0[0], 1.5f);
EXPECT_FLOAT_EQ(g_gxState.proj.m0[2], 0.1f);
EXPECT_FLOAT_EQ(g_gxState.proj.m1[1], 2.0f);
EXPECT_FLOAT_EQ(g_gxState.proj.m1[2], 0.2f);
EXPECT_FLOAT_EQ(g_gxState.proj.m2[2], -1.002f);
EXPECT_FLOAT_EQ(g_gxState.proj.m2[3], -0.2002f);
EXPECT_FLOAT_EQ(g_gxState.proj.m3[2], -1.0f);
}
TEST_F(GXFifoTest, Projection_Orthographic) {
aurora::Mat4x4<float> proj{};
proj.m0[0] = 2.0f / 640.0f;
proj.m0[3] = -1.0f;
proj.m1[1] = 2.0f / 480.0f;
proj.m1[3] = -1.0f;
proj.m2[2] = -1.0f / 10000.0f;
proj.m2[3] = 0.0f;
proj.m3[3] = 1.0f;
GXSetProjection(&proj, GX_ORTHOGRAPHIC);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.projType, GX_ORTHOGRAPHIC);
EXPECT_FLOAT_EQ(g_gxState.proj.m0[0], 2.0f / 640.0f);
EXPECT_FLOAT_EQ(g_gxState.proj.m0[3], -1.0f);
EXPECT_FLOAT_EQ(g_gxState.proj.m1[1], 2.0f / 480.0f);
EXPECT_FLOAT_EQ(g_gxState.proj.m1[3], -1.0f);
EXPECT_FLOAT_EQ(g_gxState.proj.m3[3], 1.0f);
}
TEST_F(GXFifoTest, GetProjectionAndScissorShadowState) {
const f32 proj[] = {0.0f, 1.5f, 0.1f, 2.0f, 0.2f, -1.002f, -0.2002f};
f32 outProj[7]{};
u32 left = 0, top = 0, width = 0, height = 0;
GXSetProjectionv(proj);
GXSetScissor(16, 24, 320, 240);
GXGetProjectionv(outProj);
GXGetScissor(&left, &top, &width, &height);
for (size_t i = 0; i < 7; ++i) {
EXPECT_FLOAT_EQ(outProj[i], proj[i]);
}
EXPECT_EQ(left, 16u);
EXPECT_EQ(top, 24u);
EXPECT_EQ(width, 320u);
EXPECT_EQ(height, 240u);
}
TEST_F(GXFifoTest, Scissor_EncodesBpAndDecodesLogicalState) {
GXSetScissor(16, 24, 320, 240);
auto bytes = capture_fifo();
EXPECT_TRUE(has_bp_write(bytes, 0x20));
EXPECT_TRUE(has_bp_write(bytes, 0x21));
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.logicalScissor.x, 16);
EXPECT_EQ(g_gxState.logicalScissor.y, 24);
EXPECT_EQ(g_gxState.logicalScissor.width, 320);
EXPECT_EQ(g_gxState.logicalScissor.height, 240);
}
TEST_F(GXFifoTest, ScissorBoxOffset_EncodesBp59AndDecodesState) {
GXSetScissorBoxOffset(1024, 0);
auto bytes = capture_fifo();
EXPECT_TRUE(has_bp_write(bytes, 0x59));
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.scissorOffsetX, 1024);
EXPECT_EQ(g_gxState.scissorOffsetY, 0);
}
TEST_F(GXFifoTest, GetViewportShadowState) {
f32 vp[6]{};
GXSetViewport(10.0f, 20.0f, 640.0f, 480.0f, 0.1f, 1.0f);
GXGetViewportv(vp);
EXPECT_FLOAT_EQ(vp[0], 10.0f);
EXPECT_FLOAT_EQ(vp[1], 20.0f);
EXPECT_FLOAT_EQ(vp[2], 640.0f);
EXPECT_FLOAT_EQ(vp[3], 480.0f);
EXPECT_FLOAT_EQ(vp[4], 0.1f);
EXPECT_FLOAT_EQ(vp[5], 1.0f);
GXSetViewportJitter(30.0f, 40.0f, 320.0f, 240.0f, 0.2f, 0.9f, 0);
GXGetViewportv(vp);
EXPECT_FLOAT_EQ(vp[0], 30.0f);
EXPECT_FLOAT_EQ(vp[1], 39.5f);
EXPECT_FLOAT_EQ(vp[2], 320.0f);
EXPECT_FLOAT_EQ(vp[3], 240.0f);
EXPECT_FLOAT_EQ(vp[4], 0.2f);
EXPECT_FLOAT_EQ(vp[5], 0.9f);
}
TEST_F(GXFifoTest, Viewport_DecodesLogicalViewportState) {
GXSetViewport(10.0f, 20.0f, 640.0f, 480.0f, 0.1f, 1.0f);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_FLOAT_EQ(g_gxState.logicalViewport.left, 10.0f);
EXPECT_FLOAT_EQ(g_gxState.logicalViewport.top, 20.0f);
EXPECT_FLOAT_EQ(g_gxState.logicalViewport.width, 640.0f);
EXPECT_FLOAT_EQ(g_gxState.logicalViewport.height, 480.0f);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.left, 10.0f);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.top, 20.0f);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.width, 640.0f);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.height, 480.0f);
}
TEST_F(GXFifoTest, ViewportRender_EncodesAuroraOverride) {
GXSetViewportRender(100.0f, 50.0f, 1280.0f, 720.0f, 0.0f, 1.0f);
auto bytes = capture_fifo();
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_VIEWPORT_RENDER));
reset_gx_state();
decode_fifo(bytes);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.left, 100.0f);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.top, 50.0f);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.width, 1280.0f);
EXPECT_FLOAT_EQ(g_gxState.renderViewport.height, 720.0f);
}
TEST_F(GXFifoTest, ScissorRender_EncodesAuroraOverride) {
GXSetScissorRender(100, 40, 800, 600);
auto bytes = capture_fifo();
EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_SCISSOR_RENDER));
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.renderScissor.x, 100);
EXPECT_EQ(g_gxState.renderScissor.y, 40);
EXPECT_EQ(g_gxState.renderScissor.width, 800);
EXPECT_EQ(g_gxState.renderScissor.height, 600);
}
// --- GXLoadLightObjImm (XF 0x600-0x67F) ---
TEST_F(GXFifoTest, LoadLightObjImm_Light0_BasicColor) {
GXLightObj lightObj;
GXInitLightPos(&lightObj, 100.0f, 200.0f, 300.0f);
GXInitLightDir(&lightObj, 0.0f, -1.0f, 0.0f);
GXInitLightColor(&lightObj, {255, 128, 64, 255});
GXInitLightAttn(&lightObj, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f);
GXLoadLightObjImm(&lightObj, GX_LIGHT0);
auto bytes = capture_fifo();
// XF bulk write: opcode 0x10
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x10);
reset_gx_state();
g_gxState.preparedLightsDirty = false;
decode_fifo(bytes);
EXPECT_TRUE(g_gxState.preparedLightsDirty);
auto& light = g_gxState.lights[0];
// Color
EXPECT_NEAR(light.color[0], 255.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[1], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[2], 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[3], 255.f / 255.f, 1.f / 255.f);
// Position
EXPECT_FLOAT_EQ(light.pos[0], 100.0f);
EXPECT_FLOAT_EQ(light.pos[1], 200.0f);
EXPECT_FLOAT_EQ(light.pos[2], 300.0f);
// Direction (GXInitLightDir negates)
EXPECT_FLOAT_EQ(light.dir[0], 0.0f);
EXPECT_FLOAT_EQ(light.dir[1], 1.0f);
EXPECT_FLOAT_EQ(light.dir[2], 0.0f);
// Cosine attenuation
EXPECT_FLOAT_EQ(light.cosAtt[0], 1.0f);
EXPECT_FLOAT_EQ(light.cosAtt[1], 0.0f);
EXPECT_FLOAT_EQ(light.cosAtt[2], 0.0f);
// Distance attenuation
EXPECT_FLOAT_EQ(light.distAtt[0], 1.0f);
EXPECT_FLOAT_EQ(light.distAtt[1], 0.0f);
EXPECT_FLOAT_EQ(light.distAtt[2], 0.0f);
}
TEST_F(GXFifoTest, LoadLightObjImm_Light3_Attenuation) {
GXLightObj lightObj;
GXInitLightPos(&lightObj, -50.0f, 0.0f, 75.0f);
GXInitLightDir(&lightObj, 1.0f, 0.0f, 0.0f);
GXInitLightColor(&lightObj, {0, 255, 0, 128});
GXInitLightAttn(&lightObj, 0.5f, 0.3f, 0.2f, 1.0f, 0.01f, 0.001f);
GXLoadLightObjImm(&lightObj, GX_LIGHT3);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& light = g_gxState.lights[3];
EXPECT_NEAR(light.color[0], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[1], 255.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[2], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[3], 128.f / 255.f, 1.f / 255.f);
EXPECT_FLOAT_EQ(light.pos[0], -50.0f);
EXPECT_FLOAT_EQ(light.pos[1], 0.0f);
EXPECT_FLOAT_EQ(light.pos[2], 75.0f);
EXPECT_FLOAT_EQ(light.dir[0], -1.0f);
EXPECT_FLOAT_EQ(light.dir[1], 0.0f);
EXPECT_FLOAT_EQ(light.dir[2], 0.0f);
EXPECT_FLOAT_EQ(light.cosAtt[0], 0.5f);
EXPECT_FLOAT_EQ(light.cosAtt[1], 0.3f);
EXPECT_FLOAT_EQ(light.cosAtt[2], 0.2f);
EXPECT_FLOAT_EQ(light.distAtt[0], 1.0f);
EXPECT_FLOAT_EQ(light.distAtt[1], 0.01f);
EXPECT_FLOAT_EQ(light.distAtt[2], 0.001f);
}
TEST_F(GXFifoTest, LoadLightObjImm_Light7_LastLight) {
GXLightObj lightObj;
GXInitLightPos(&lightObj, 0.0f, 1000.0f, 0.0f);
GXInitLightDir(&lightObj, 0.0f, 0.0f, -1.0f);
GXInitLightColor(&lightObj, {128, 128, 128, 255});
GXInitLightAttn(&lightObj, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f);
GXLoadLightObjImm(&lightObj, GX_LIGHT7);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& light = g_gxState.lights[7];
EXPECT_NEAR(light.color[0], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[1], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[2], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(light.color[3], 255.f / 255.f, 1.f / 255.f);
EXPECT_FLOAT_EQ(light.pos[0], 0.0f);
EXPECT_FLOAT_EQ(light.pos[1], 1000.0f);
EXPECT_FLOAT_EQ(light.pos[2], 0.0f);
EXPECT_FLOAT_EQ(light.dir[0], 0.0f);
EXPECT_FLOAT_EQ(light.dir[1], 0.0f);
EXPECT_FLOAT_EQ(light.dir[2], 1.0f);
}
TEST_F(GXFifoTest, LoadLightObjImm_SpotLight) {
GXLightObj lightObj;
GXInitLightPos(&lightObj, 10.0f, 20.0f, 30.0f);
GXInitLightDir(&lightObj, 0.0f, -1.0f, 0.0f);
GXInitLightSpot(&lightObj, 45.0f, GX_SP_COS);
GXInitLightDistAttn(&lightObj, 100.0f, 0.5f, GX_DA_MEDIUM);
GXInitLightColor(&lightObj, {255, 255, 255, 255});
GXLoadLightObjImm(&lightObj, GX_LIGHT1);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& light = g_gxState.lights[1];
EXPECT_NEAR(light.color[0], 1.0f, 1.f / 255.f);
EXPECT_NEAR(light.color[1], 1.0f, 1.f / 255.f);
EXPECT_NEAR(light.color[2], 1.0f, 1.f / 255.f);
EXPECT_FLOAT_EQ(light.pos[0], 10.0f);
EXPECT_FLOAT_EQ(light.pos[1], 20.0f);
EXPECT_FLOAT_EQ(light.pos[2], 30.0f);
// GX_SP_COS with cutoff=45: cr = cos(45 * pi / 180)
// a0 = -cr/(1-cr), a1 = 1/(1-cr), a2 = 0
float cr = std::cos(45.0f * M_PIF / 180.0f);
EXPECT_FLOAT_EQ(light.cosAtt[0], -cr / (1.0f - cr));
EXPECT_FLOAT_EQ(light.cosAtt[1], 1.0f / (1.0f - cr));
EXPECT_FLOAT_EQ(light.cosAtt[2], 0.0f);
// GX_DA_MEDIUM with refDist=100, refBright=0.5:
// k0 = 1, k1 = 0.5*(1-b)/(b*d), k2 = 0.5*(1-b)/(b*d*d)
EXPECT_FLOAT_EQ(light.distAtt[0], 1.0f);
EXPECT_FLOAT_EQ(light.distAtt[1], 0.5f * 0.5f / (0.5f * 100.0f));
EXPECT_FLOAT_EQ(light.distAtt[2], 0.5f * 0.5f / (0.5f * 100.0f * 100.0f));
}
// --- GXSetChanCtrl (XF 0x100E-0x1011) ---
TEST_F(GXFifoTest, ChanCtrl_Color0_LightingEnabled) {
GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT0 | GX_LIGHT1, GX_DF_CLAMP, GX_AF_SPOT);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& cfg = g_gxState.colorChannelConfig[GX_COLOR0];
EXPECT_TRUE(cfg.lightingEnabled);
EXPECT_EQ(cfg.matSrc, GX_SRC_VTX);
EXPECT_EQ(cfg.ambSrc, GX_SRC_REG);
EXPECT_EQ(cfg.diffFn, GX_DF_CLAMP);
EXPECT_EQ(cfg.attnFn, GX_AF_SPOT);
// Light mask should be 0x03 (lights 0 and 1)
auto& state = g_gxState.colorChannelState[GX_COLOR0];
EXPECT_TRUE(state.lightMask[0]);
EXPECT_TRUE(state.lightMask[1]);
EXPECT_FALSE(state.lightMask[2]);
}
TEST_F(GXFifoTest, ChanCtrl_Alpha0_NoLighting) {
GXSetChanCtrl(GX_ALPHA0, false, GX_SRC_VTX, GX_SRC_REG, 0, GX_DF_NONE, GX_AF_NONE);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& cfg = g_gxState.colorChannelConfig[GX_ALPHA0];
EXPECT_FALSE(cfg.lightingEnabled);
EXPECT_EQ(cfg.matSrc, GX_SRC_REG);
EXPECT_EQ(cfg.ambSrc, GX_SRC_VTX);
EXPECT_EQ(cfg.attnFn, GX_AF_NONE);
}
TEST_F(GXFifoTest, ChanCtrl_Color1_SpecularLighting) {
GXSetChanCtrl(GX_COLOR1, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT2 | GX_LIGHT5, GX_DF_SIGN, GX_AF_SPEC);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& cfg = g_gxState.colorChannelConfig[GX_COLOR1];
EXPECT_TRUE(cfg.lightingEnabled);
EXPECT_EQ(cfg.matSrc, GX_SRC_REG);
EXPECT_EQ(cfg.ambSrc, GX_SRC_REG);
EXPECT_EQ(cfg.diffFn, GX_DF_NONE);
EXPECT_EQ(cfg.attnFn, GX_AF_SPEC);
auto& state = g_gxState.colorChannelState[GX_COLOR1];
EXPECT_FALSE(state.lightMask[0]);
EXPECT_FALSE(state.lightMask[1]);
EXPECT_TRUE(state.lightMask[2]);
EXPECT_FALSE(state.lightMask[3]);
EXPECT_FALSE(state.lightMask[4]);
EXPECT_TRUE(state.lightMask[5]);
}
TEST_F(GXFifoTest, ChanCtrl_EncodesHardwareAttenuationField) {
GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT0, GX_DF_SIGN, GX_AF_SPEC);
auto specBytes = capture_fifo();
ASSERT_GE(specBytes.size(), 9u);
EXPECT_EQ((read_be32_at(specBytes, 5) >> 7) & 0x3u, static_cast<u32>(GX_DF_NONE));
EXPECT_EQ((read_be32_at(specBytes, 5) >> 9) & 0x3u, 1u);
GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT0, GX_DF_CLAMP, GX_AF_SPOT);
auto spotBytes = capture_fifo();
ASSERT_GE(spotBytes.size(), 9u);
EXPECT_EQ((read_be32_at(spotBytes, 5) >> 9) & 0x3u, 3u);
GXSetChanCtrl(GX_COLOR0, true, GX_SRC_REG, GX_SRC_REG, GX_LIGHT0, GX_DF_NONE, GX_AF_NONE);
auto noneBytes = capture_fifo();
ASSERT_GE(noneBytes.size(), 9u);
EXPECT_EQ((read_be32_at(noneBytes, 5) >> 9) & 0x3u, 2u);
}
TEST_F(GXFifoTest, ChanCtrl_DecodesRawHardwareAttenuationField) {
reset_gx_state();
decode_fifo(xf_cmd(0x100E, {1u << 9}));
EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_SPEC);
reset_gx_state();
decode_fifo(xf_cmd(0x100E, {2u << 9}));
EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_NONE);
reset_gx_state();
decode_fifo(xf_cmd(0x100E, {3u << 9}));
EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_SPOT);
reset_gx_state();
decode_fifo(xf_cmd(0x100E, {0u << 9}));
EXPECT_EQ(g_gxState.colorChannelConfig[GX_COLOR0].attnFn, GX_AF_NONE);
}
TEST_F(GXFifoTest, ChanCtrl_Color0A0_Compound) {
// GX_COLOR0A0 should set both GX_COLOR0 and GX_ALPHA0
GXSetChanCtrl(GX_COLOR0A0, true, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT0, GX_DF_CLAMP, GX_AF_SPOT);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// Both COLOR0 and ALPHA0 should be configured identically
auto& cfgC = g_gxState.colorChannelConfig[GX_COLOR0];
EXPECT_TRUE(cfgC.lightingEnabled);
EXPECT_EQ(cfgC.matSrc, GX_SRC_VTX);
EXPECT_EQ(cfgC.ambSrc, GX_SRC_REG);
EXPECT_EQ(cfgC.diffFn, GX_DF_CLAMP);
EXPECT_EQ(cfgC.attnFn, GX_AF_SPOT);
auto& cfgA = g_gxState.colorChannelConfig[GX_ALPHA0];
EXPECT_TRUE(cfgA.lightingEnabled);
EXPECT_EQ(cfgA.matSrc, GX_SRC_VTX);
EXPECT_EQ(cfgA.ambSrc, GX_SRC_REG);
EXPECT_EQ(cfgA.attnFn, GX_AF_SPOT);
EXPECT_TRUE(g_gxState.colorChannelState[GX_COLOR0].lightMask[0]);
EXPECT_TRUE(g_gxState.colorChannelState[GX_ALPHA0].lightMask[0]);
}
TEST_F(GXFifoTest, ChanCtrl_Color1A1_Compound) {
GXSetChanCtrl(GX_COLOR1A1, false, GX_SRC_VTX, GX_SRC_VTX, 0, GX_DF_NONE, GX_AF_NONE);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& cfgC = g_gxState.colorChannelConfig[GX_COLOR1];
EXPECT_FALSE(cfgC.lightingEnabled);
EXPECT_EQ(cfgC.ambSrc, GX_SRC_VTX);
EXPECT_EQ(cfgC.matSrc, GX_SRC_VTX);
auto& cfgA = g_gxState.colorChannelConfig[GX_ALPHA1];
EXPECT_FALSE(cfgA.lightingEnabled);
EXPECT_EQ(cfgA.ambSrc, GX_SRC_VTX);
EXPECT_EQ(cfgA.matSrc, GX_SRC_VTX);
}
// --- GXSetTexCoordGen2 (XF 0x1040-0x105F) ---
TEST_F(GXFifoTest, TexCoordGen_Mtx2x4_Tex0) {
GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0, GX_FALSE, GX_PTIDENTITY);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& tcg = g_gxState.tcgs[GX_TEXCOORD0];
EXPECT_EQ(tcg.type, GX_TG_MTX2x4);
EXPECT_EQ(tcg.src, GX_TG_TEX0);
EXPECT_EQ(tcg.mtx, GX_TEXMTX0);
EXPECT_EQ(tcg.postMtx, GX_PTIDENTITY);
}
TEST_F(GXFifoTest, TexCoordGen_Mtx3x4_Nrm) {
GXSetTexCoordGen2(GX_TEXCOORD1, GX_TG_MTX3x4, GX_TG_NRM, GX_TEXMTX0, GX_TRUE, GX_PTTEXMTX0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& tcg = g_gxState.tcgs[GX_TEXCOORD1];
EXPECT_EQ(tcg.type, GX_TG_MTX3x4);
EXPECT_EQ(tcg.src, GX_TG_NRM);
EXPECT_EQ(tcg.mtx, GX_TEXMTX0);
EXPECT_TRUE(tcg.normalize);
EXPECT_EQ(tcg.postMtx, GX_PTTEXMTX0);
}
TEST_F(GXFifoTest, TexCoordGen_SRTG_Color0) {
GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_SRTG, GX_TG_COLOR0, GX_TEXMTX0, GX_FALSE, GX_PTIDENTITY);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& tcg = g_gxState.tcgs[GX_TEXCOORD0];
EXPECT_EQ(tcg.type, GX_TG_SRTG);
EXPECT_EQ(tcg.mtx, GX_TEXMTX0);
}
TEST_F(GXFifoTest, TexCoordGen_NonZeroMtx) {
GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX3x4, GX_TG_TEX0, GX_TEXMTX3, GX_FALSE, GX_PTTEXMTX5);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& tcg = g_gxState.tcgs[GX_TEXCOORD0];
EXPECT_EQ(tcg.type, GX_TG_MTX3x4);
EXPECT_EQ(tcg.src, GX_TG_TEX0);
EXPECT_EQ(tcg.mtx, GX_TEXMTX3);
EXPECT_EQ(tcg.postMtx, GX_PTTEXMTX5);
}
TEST_F(GXFifoTest, TexCoordGen_MultipleCoords) {
GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX3x4, GX_TG_TEX0, GX_TEXMTX0, GX_FALSE, GX_PTTEXMTX0);
GXSetTexCoordGen2(GX_TEXCOORD1, GX_TG_MTX3x4, GX_TG_TEX1, GX_TEXMTX1, GX_FALSE, GX_PTTEXMTX1);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tcgs[0].mtx, GX_TEXMTX0);
EXPECT_EQ(g_gxState.tcgs[0].postMtx, GX_PTTEXMTX0);
EXPECT_EQ(g_gxState.tcgs[0].src, GX_TG_TEX0);
EXPECT_EQ(g_gxState.tcgs[1].mtx, GX_TEXMTX1);
EXPECT_EQ(g_gxState.tcgs[1].postMtx, GX_PTTEXMTX1);
EXPECT_EQ(g_gxState.tcgs[1].src, GX_TG_TEX1);
}
TEST_F(GXFifoTest, TexCoordGen_HighCoord_MatIdxB) {
// TexCoord4+ uses matIdxB
GXSetTexCoordGen2(GX_TEXCOORD4, GX_TG_MTX2x4, GX_TG_TEX4, GX_TEXMTX5, GX_FALSE, GX_PTTEXMTX3);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& tcg = g_gxState.tcgs[GX_TEXCOORD4];
EXPECT_EQ(tcg.type, GX_TG_MTX2x4);
EXPECT_EQ(tcg.src, GX_TG_TEX4);
EXPECT_EQ(tcg.mtx, GX_TEXMTX5);
EXPECT_EQ(tcg.postMtx, GX_PTTEXMTX3);
}
TEST_F(GXFifoTest, TexCoordGen_Identity) {
GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY, GX_FALSE, GX_PTIDENTITY);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& tcg = g_gxState.tcgs[GX_TEXCOORD0];
EXPECT_EQ(tcg.mtx, GX_IDENTITY);
EXPECT_EQ(tcg.postMtx, GX_PTIDENTITY);
}
TEST_F(GXFifoTest, MatrixIndexA_DecodesTexMatricesFromCpPacket) {
const u32 value =
(GX_PNMTX3 << 0) | (GX_TEXMTX3 << 6) | (GX_TEXMTX4 << 12) | (GX_IDENTITY << 18) | (GX_TEXMTX7 << 24);
auto bytes = cp_cmd(0x30, value);
reset_gx_state();
g_gxState.tcgs[0].mtx = GX_IDENTITY;
g_gxState.tcgs[1].mtx = GX_IDENTITY;
g_gxState.tcgs[2].mtx = GX_TEXMTX0;
g_gxState.tcgs[3].mtx = GX_IDENTITY;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.currentPnMtx, 3u);
EXPECT_EQ(g_gxState.tcgs[0].mtx, GX_TEXMTX3);
EXPECT_EQ(g_gxState.tcgs[1].mtx, GX_TEXMTX4);
EXPECT_EQ(g_gxState.tcgs[2].mtx, GX_IDENTITY);
EXPECT_EQ(g_gxState.tcgs[3].mtx, GX_TEXMTX7);
}
TEST_F(GXFifoTest, MatrixIndexB_DecodesTexMatricesFromCpPacket) {
const u32 value = (GX_TEXMTX4 << 0) | (GX_TEXMTX5 << 6) | (GX_IDENTITY << 12) | (GX_TEXMTX9 << 18);
auto bytes = cp_cmd(0x40, value);
reset_gx_state();
g_gxState.tcgs[4].mtx = GX_IDENTITY;
g_gxState.tcgs[5].mtx = GX_IDENTITY;
g_gxState.tcgs[6].mtx = GX_TEXMTX0;
g_gxState.tcgs[7].mtx = GX_IDENTITY;
decode_fifo(bytes);
EXPECT_EQ(g_gxState.tcgs[4].mtx, GX_TEXMTX4);
EXPECT_EQ(g_gxState.tcgs[5].mtx, GX_TEXMTX5);
EXPECT_EQ(g_gxState.tcgs[6].mtx, GX_IDENTITY);
EXPECT_EQ(g_gxState.tcgs[7].mtx, GX_TEXMTX9);
}
// --- GXSetChanAmbColor / GXSetChanMatColor (XF 0x100A-0x100D) ---
TEST_F(GXFifoTest, ChanAmbColor_Color0) {
GXColor amb = {64, 128, 192, 255};
GXSetChanAmbColor(GX_COLOR0, amb);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& state = g_gxState.colorChannelState[GX_COLOR0];
EXPECT_NEAR(state.ambColor[0], 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.ambColor[1], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.ambColor[2], 192.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.ambColor[3], 255.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, ChanMatColor_Color0) {
GXColor mat = {255, 0, 128, 64};
GXSetChanMatColor(GX_COLOR0, mat);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& state = g_gxState.colorChannelState[GX_COLOR0];
EXPECT_NEAR(state.matColor[0], 255.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.matColor[1], 0.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.matColor[2], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.matColor[3], 64.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, ChanAmbColor_Color1) {
GXColor amb = {10, 20, 30, 40};
GXSetChanAmbColor(GX_COLOR1, amb);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& state = g_gxState.colorChannelState[GX_COLOR1];
EXPECT_NEAR(state.ambColor[0], 10.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.ambColor[1], 20.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.ambColor[2], 30.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.ambColor[3], 40.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, ChanMatColor_Color1) {
GXColor mat = {100, 150, 200, 250};
GXSetChanMatColor(GX_COLOR1, mat);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& state = g_gxState.colorChannelState[GX_COLOR1];
EXPECT_NEAR(state.matColor[0], 100.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.matColor[1], 150.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.matColor[2], 200.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(state.matColor[3], 250.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, ChanAmbColor_Color0A0_Compound) {
// GX_COLOR0A0 should write to both COLOR0 and ALPHA0 XF registers
GXColor amb = {80, 160, 240, 128};
GXSetChanAmbColor(GX_COLOR0A0, amb);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& stateC = g_gxState.colorChannelState[GX_COLOR0];
EXPECT_NEAR(stateC.ambColor[0], 80.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateC.ambColor[1], 160.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateC.ambColor[2], 240.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateC.ambColor[3], 128.f / 255.f, 1.f / 255.f);
// ALPHA0 shares the same XF register as COLOR0, so should match
auto& stateA = g_gxState.colorChannelState[GX_ALPHA0];
EXPECT_NEAR(stateA.ambColor[0], 80.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateA.ambColor[3], 128.f / 255.f, 1.f / 255.f);
}
TEST_F(GXFifoTest, ChanMatColor_Color1A1_Compound) {
GXColor mat = {32, 64, 96, 128};
GXSetChanMatColor(GX_COLOR1A1, mat);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& stateC = g_gxState.colorChannelState[GX_COLOR1];
EXPECT_NEAR(stateC.matColor[0], 32.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateC.matColor[1], 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateC.matColor[2], 96.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateC.matColor[3], 128.f / 255.f, 1.f / 255.f);
auto& stateA = g_gxState.colorChannelState[GX_ALPHA1];
EXPECT_NEAR(stateA.matColor[0], 32.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(stateA.matColor[3], 128.f / 255.f, 1.f / 255.f);
}
// GXSetFog (BP 0xEE-0xF2): fog A/B/C parameters, type and color
// --- Fog with perspective linear fog, typical parameters ---
TEST_F(GXFifoTest, Fog_PerspLin_Typical) {
GXColor fogColor = {128, 200, 255, 255};
GXSetFog(GX_FOG_PERSP_LIN, 100.f, 900.f, 0.1f, 1000.f, fogColor);
auto bytes = capture_fifo();
// Should produce 5 BP writes (0xEE-0xF2): 5 * 5 = 25 bytes
ASSERT_EQ(bytes.size(), 25u);
// Verify BP opcodes and register IDs
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0xEE);
EXPECT_EQ(bytes[5], 0x61);
EXPECT_EQ(bytes[6], 0xEF);
EXPECT_EQ(bytes[10], 0x61);
EXPECT_EQ(bytes[11], 0xF0);
EXPECT_EQ(bytes[15], 0x61);
EXPECT_EQ(bytes[16], 0xF1);
EXPECT_EQ(bytes[20], 0x61);
EXPECT_EQ(bytes[21], 0xF2);
reset_gx_state();
decode_fifo(bytes);
// Compute expected A, B, C from the SDK formula
float nearZ = 0.1f, farZ = 1000.f, startZ = 100.f, endZ = 900.f;
float A = (farZ * nearZ) / ((farZ - nearZ) * (endZ - startZ));
float B = farZ / (farZ - nearZ);
float C = startZ / (endZ - startZ);
// Allow tolerance for encoding precision loss (11-bit mantissa)
EXPECT_NEAR(g_gxState.fog.a, A, std::abs(A) * 1e-3f);
EXPECT_NEAR(g_gxState.fog.b, B, std::abs(B) * 1e-3f);
EXPECT_NEAR(g_gxState.fog.c, C, std::abs(C) * 1e-3f);
expect_fog_raw_fields_match_decoded_state();
EXPECT_EQ(g_gxState.fog.type, GX_FOG_PERSP_LIN);
EXPECT_NEAR(g_gxState.fog.color[0], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.fog.color[1], 200.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.fog.color[2], 255.f / 255.f, 1.f / 255.f);
}
// --- Orthographic fog keeps the projection bit and uses the SDK ortho coefficients ---
TEST_F(GXFifoTest, Fog_OrthoLin_Typical) {
GXColor fogColor = {128, 200, 255, 255};
GXSetFog(GX_FOG_ORTHO_LIN, 100.f, 900.f, 0.1f, 1000.f, fogColor);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 25u);
const u32 fog3 = (static_cast<u32>(bytes[16]) << 24) | (static_cast<u32>(bytes[17]) << 16) |
(static_cast<u32>(bytes[18]) << 8) | static_cast<u32>(bytes[19]);
EXPECT_EQ((fog3 >> 20) & 1u, 1u);
EXPECT_EQ((fog3 >> 21) & 7u, static_cast<u32>(GX_FOG_LIN));
reset_gx_state();
decode_fifo(bytes);
float nearZ = 0.1f, farZ = 1000.f, startZ = 100.f, endZ = 900.f;
float A = (farZ - nearZ) / (endZ - startZ);
float C = (startZ - nearZ) / (endZ - startZ);
EXPECT_NEAR(g_gxState.fog.a, A, std::abs(A) * 1e-3f);
EXPECT_FLOAT_EQ(g_gxState.fog.b, 0.f);
EXPECT_NEAR(g_gxState.fog.c, C, std::abs(C) * 1e-3f);
expect_fog_raw_fields_match_decoded_state();
EXPECT_EQ(g_gxState.fog.type, GX_FOG_ORTHO_LIN);
}
// --- Fog with degenerate parameters (nearZ == farZ) ---
TEST_F(GXFifoTest, Fog_Degenerate_EqualDepths) {
GXColor fogColor = {0, 0, 0, 255};
GXSetFog(GX_FOG_PERSP_EXP, 0.f, 100.f, 10.f, 10.f, fogColor);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// When nearZ == farZ, SDK sets A=0, B=0.5, C=0
EXPECT_FLOAT_EQ(g_gxState.fog.a, 0.f);
EXPECT_NEAR(g_gxState.fog.b, 0.5f, 1e-3f);
EXPECT_FLOAT_EQ(g_gxState.fog.c, 0.f);
expect_fog_raw_fields_match_decoded_state();
EXPECT_EQ(g_gxState.fog.type, GX_FOG_PERSP_EXP);
}
// --- Fog type: none ---
TEST_F(GXFifoTest, Fog_None) {
GXColor fogColor = {64, 64, 64, 255};
GXSetFog(GX_FOG_NONE, 0.f, 0.f, 0.f, 0.f, fogColor);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.fog.type, GX_FOG_NONE);
EXPECT_FLOAT_EQ(g_gxState.fog.a, 0.f);
EXPECT_NEAR(g_gxState.fog.b, 0.5f, 1e-3f);
EXPECT_FLOAT_EQ(g_gxState.fog.c, 0.f);
expect_fog_raw_fields_match_decoded_state();
EXPECT_NEAR(g_gxState.fog.color[0], 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.fog.color[1], 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.fog.color[2], 64.f / 255.f, 1.f / 255.f);
}
// --- Fog with perspective reverse exponential squared type ---
TEST_F(GXFifoTest, Fog_PerspRevExp2) {
GXColor fogColor = {255, 0, 0, 255};
GXSetFog(GX_FOG_PERSP_REVEXP2, 50.f, 500.f, 1.f, 1000.f, fogColor);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
float nearZ = 1.f, farZ = 1000.f, startZ = 50.f, endZ = 500.f;
float A = (farZ * nearZ) / ((farZ - nearZ) * (endZ - startZ));
float B = farZ / (farZ - nearZ);
float C = startZ / (endZ - startZ);
EXPECT_NEAR(g_gxState.fog.a, A, std::abs(A) * 1e-3f);
EXPECT_NEAR(g_gxState.fog.b, B, std::abs(B) * 1e-3f);
EXPECT_NEAR(g_gxState.fog.c, C, std::abs(C) * 1e-3f);
expect_fog_raw_fields_match_decoded_state();
EXPECT_EQ(g_gxState.fog.type, GX_FOG_PERSP_REVEXP2);
EXPECT_NEAR(g_gxState.fog.color[0], 1.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.fog.color[1], 0.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.fog.color[2], 0.f, 1.f / 255.f);
}
// GXSetIndTexMtx (BP 0x06-0x0E): indirect texture matrix parameters
// --- IndTexMtx 0 with half-scale diagonal matrix ---
// Note: 11-bit signed range limits values to [-1.0, 0.999], so 1.0 is not representable.
TEST_F(GXFifoTest, IndTexMtx0_HalfScale) {
f32 mtx[2][3] = {
{0.5f, 0.0f, 0.0f},
{0.0f, 0.5f, 0.0f},
};
GXSetIndTexMtx(GX_ITM_0, mtx, 0);
auto bytes = capture_fifo();
// Should produce 3 BP writes: 3 * 5 = 15 bytes
ASSERT_EQ(bytes.size(), 15u);
// Verify BP opcodes and register IDs (0x06, 0x07, 0x08 for matrix 0)
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0x06);
EXPECT_EQ(bytes[5], 0x61);
EXPECT_EQ(bytes[6], 0x07);
EXPECT_EQ(bytes[10], 0x61);
EXPECT_EQ(bytes[11], 0x08);
reset_gx_state();
decode_fifo(bytes);
const auto& info = g_gxState.indTexMtxs[0];
// 11-bit fixed-point (1/1024) precision
float tol = 1.0f / 1024.0f;
EXPECT_NEAR(info.mtx.m0.x, 0.5f, tol);
EXPECT_NEAR(info.mtx.m0.y, 0.0f, tol);
EXPECT_NEAR(info.mtx.m1.x, 0.0f, tol);
EXPECT_NEAR(info.mtx.m1.y, 0.5f, tol);
EXPECT_NEAR(info.mtx.m2.x, 0.0f, tol);
EXPECT_NEAR(info.mtx.m2.y, 0.0f, tol);
EXPECT_EQ(info.scaleExp, 0);
}
// --- IndTexMtx 1 with fractional values and positive scale ---
TEST_F(GXFifoTest, IndTexMtx1_FractionalWithScale) {
f32 mtx[2][3] = {
{0.5f, 0.25f, -0.125f},
{-0.5f, 0.75f, 0.0f},
};
GXSetIndTexMtx(GX_ITM_1, mtx, 3);
auto bytes = capture_fifo();
// Register IDs for matrix 1: 0x09, 0x0A, 0x0B
ASSERT_EQ(bytes.size(), 15u);
EXPECT_EQ(bytes[1], 0x09);
EXPECT_EQ(bytes[6], 0x0A);
EXPECT_EQ(bytes[11], 0x0B);
reset_gx_state();
decode_fifo(bytes);
const auto& info = g_gxState.indTexMtxs[1];
float tol = 1.0f / 1024.0f;
EXPECT_NEAR(info.mtx.m0.x, 0.5f, tol);
EXPECT_NEAR(info.mtx.m0.y, -0.5f, tol);
EXPECT_NEAR(info.mtx.m1.x, 0.25f, tol);
EXPECT_NEAR(info.mtx.m1.y, 0.75f, tol);
EXPECT_NEAR(info.mtx.m2.x, -0.125f, tol);
EXPECT_NEAR(info.mtx.m2.y, 0.0f, tol);
EXPECT_EQ(info.scaleExp, 3);
}
// --- IndTexMtx 2 with negative scale exponent ---
TEST_F(GXFifoTest, IndTexMtx2_NegativeScale) {
f32 mtx[2][3] = {
{0.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 0.0f},
};
GXSetIndTexMtx(GX_ITM_2, mtx, -5);
auto bytes = capture_fifo();
// Register IDs for matrix 2: 0x0C, 0x0D, 0x0E
ASSERT_EQ(bytes.size(), 15u);
EXPECT_EQ(bytes[1], 0x0C);
EXPECT_EQ(bytes[6], 0x0D);
EXPECT_EQ(bytes[11], 0x0E);
reset_gx_state();
decode_fifo(bytes);
const auto& info = g_gxState.indTexMtxs[2];
EXPECT_EQ(info.scaleExp, -5);
}
TEST_F(GXFifoTest, IndTexMtxScaleMultiplier_MatchesHardwareExponent) {
EXPECT_FLOAT_EQ(aurora::gx::indirect_matrix_scale_multiplier(0), 1.0f);
EXPECT_FLOAT_EQ(aurora::gx::indirect_matrix_scale_multiplier(3), 8.0f);
EXPECT_FLOAT_EQ(aurora::gx::indirect_matrix_scale_multiplier(-5), 0.03125f);
}
TEST_F(GXFifoTest, IndTexMtxDynamicAliases_MapToSameSlotsAsSdk) {
f32 mtxS[2][3] = {
{0.25f, 0.0f, 0.0f},
{0.0f, 0.25f, 0.0f},
};
f32 mtxT[2][3] = {
{0.5f, 0.0f, 0.0f},
{0.0f, 0.5f, 0.0f},
};
GXSetIndTexMtx(GX_ITM_S1, mtxS, 2);
GXSetIndTexMtx(GX_ITM_T2, mtxT, -3);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 30u);
EXPECT_EQ(bytes[1], 0x09);
EXPECT_EQ(bytes[6], 0x0A);
EXPECT_EQ(bytes[11], 0x0B);
EXPECT_EQ(bytes[16], 0x0C);
EXPECT_EQ(bytes[21], 0x0D);
EXPECT_EQ(bytes[26], 0x0E);
reset_gx_state();
decode_fifo(bytes);
constexpr float tol = 1.0f / 1024.0f;
EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m0.x, 0.25f, tol);
EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m1.y, 0.25f, tol);
EXPECT_EQ(g_gxState.indTexMtxs[1].scaleExp, 2);
EXPECT_NEAR(g_gxState.indTexMtxs[2].mtx.m0.x, 0.5f, tol);
EXPECT_NEAR(g_gxState.indTexMtxs[2].mtx.m1.y, 0.5f, tol);
EXPECT_EQ(g_gxState.indTexMtxs[2].scaleExp, -3);
}
// --- IndTexMtx 0 does not affect matrix 1 ---
TEST_F(GXFifoTest, IndTexMtx0_Isolation) {
f32 mtx0[2][3] = {
{0.5f, 0.0f, 0.0f},
{0.0f, 0.5f, 0.0f},
};
f32 mtx1[2][3] = {
{-1.0f, 0.0f, 0.0f},
{0.0f, -1.0f, 0.0f},
};
GXSetIndTexMtx(GX_ITM_0, mtx0, 1);
GXSetIndTexMtx(GX_ITM_1, mtx1, -2);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
float tol = 1.0f / 1024.0f;
// Matrix 0
EXPECT_NEAR(g_gxState.indTexMtxs[0].mtx.m0.x, 0.5f, tol);
EXPECT_NEAR(g_gxState.indTexMtxs[0].mtx.m1.y, 0.5f, tol);
EXPECT_EQ(g_gxState.indTexMtxs[0].scaleExp, 1);
// Matrix 1
EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m0.x, -1.0f, tol);
EXPECT_NEAR(g_gxState.indTexMtxs[1].mtx.m1.y, -1.0f, tol);
EXPECT_EQ(g_gxState.indTexMtxs[1].scaleExp, -2);
}
// SU texture coordinate scale (BP 0x30-0x3F)
// --- GXSetTexCoordScaleManually sets width/height ---
TEST_F(GXFifoTest, TexCoordScale_Manual_Coord0) {
GXSetTexCoordScaleManually(GX_TEXCOORD0, GX_TRUE, 256, 128);
auto bytes = capture_fifo();
// Two BP writes (suTs0 + suTs1): 2 * 5 = 10 bytes
ASSERT_EQ(bytes.size(), 10u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0x30); // suTs0[0]
EXPECT_EQ(bytes[5], 0x61);
EXPECT_EQ(bytes[6], 0x31); // suTs1[0]
reset_gx_state();
decode_fifo(bytes);
const auto& tcs = g_gxState.texCoordScales[0];
EXPECT_EQ(tcs.scaleS, 255u); // width - 1
EXPECT_EQ(tcs.scaleT, 127u); // height - 1
}
// --- GXSetTexCoordScaleManually for coord 3 ---
TEST_F(GXFifoTest, TexCoordScale_Manual_Coord3) {
GXSetTexCoordScaleManually(GX_TEXCOORD3, GX_TRUE, 512, 512);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 10u);
EXPECT_EQ(bytes[1], 0x36); // suTs0[3] = 0x30 + 3*2
EXPECT_EQ(bytes[6], 0x37); // suTs1[3] = 0x31 + 3*2
reset_gx_state();
decode_fifo(bytes);
const auto& tcs = g_gxState.texCoordScales[3];
EXPECT_EQ(tcs.scaleS, 511u);
EXPECT_EQ(tcs.scaleT, 511u);
}
// --- GXSetTexCoordScaleManually with bias and cyl wrap ---
TEST_F(GXFifoTest, TexCoordScale_BiasAndCylWrap) {
// Enable manual mode first, then set bias and cyl wrap
GXSetTexCoordScaleManually(GX_TEXCOORD0, GX_TRUE, 64, 64);
capture_fifo(); // discard
GXSetTexCoordBias(GX_TEXCOORD0, GX_TRUE, GX_FALSE);
auto biasBytes = capture_fifo();
GXSetTexCoordCylWrap(GX_TEXCOORD0, GX_FALSE, GX_TRUE);
auto cylBytes = capture_fifo();
// Each writes 2 BP regs
ASSERT_EQ(biasBytes.size(), 10u);
ASSERT_EQ(cylBytes.size(), 10u);
reset_gx_state();
decode_fifo(biasBytes);
decode_fifo(cylBytes);
const auto& tcs = g_gxState.texCoordScales[0];
EXPECT_TRUE(tcs.biasS);
EXPECT_FALSE(tcs.biasT);
EXPECT_FALSE(tcs.cylWrapS);
EXPECT_TRUE(tcs.cylWrapT);
}
// --- GXEnableTexOffsets ---
TEST_F(GXFifoTest, TexCoordScale_TexOffsets) {
GXEnableTexOffsets(GX_TEXCOORD2, GX_TRUE, GX_TRUE);
auto bytes = capture_fifo();
// One BP write (suTs0 only): 5 bytes
ASSERT_EQ(bytes.size(), 5u);
EXPECT_EQ(bytes[1], 0x34); // suTs0[2] = 0x30 + 2*2
reset_gx_state();
decode_fifo(bytes);
const auto& tcs = g_gxState.texCoordScales[2];
EXPECT_TRUE(tcs.lineOffset);
EXPECT_TRUE(tcs.pointOffset);
}
TEST_F(GXFifoTest, TexCoordScale_TexOffsets_Disabled) {
GXEnableTexOffsets(GX_TEXCOORD2, GX_FALSE, GX_FALSE);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 5u);
EXPECT_EQ(bytes[1], 0x34);
reset_gx_state();
g_gxState.texCoordScales[2].lineOffset = true;
g_gxState.texCoordScales[2].pointOffset = true;
decode_fifo(bytes);
const auto& tcs = g_gxState.texCoordScales[2];
EXPECT_FALSE(tcs.lineOffset);
EXPECT_FALSE(tcs.pointOffset);
}
// --- Coord isolation: writing coord 0 doesn't affect coord 1 ---
TEST_F(GXFifoTest, TexCoordScale_Isolation) {
GXSetTexCoordScaleManually(GX_TEXCOORD0, GX_TRUE, 100, 200);
GXSetTexCoordScaleManually(GX_TEXCOORD1, GX_TRUE, 300, 400);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.texCoordScales[0].scaleS, 99u);
EXPECT_EQ(g_gxState.texCoordScales[0].scaleT, 199u);
EXPECT_EQ(g_gxState.texCoordScales[1].scaleS, 299u);
EXPECT_EQ(g_gxState.texCoordScales[1].scaleT, 399u);
}
// GXSetCopyClear (BP 0x4F-0x51): clear color and depth
TEST_F(GXFifoTest, DispCopyState_EncodesBpAndYScale) {
GXSetDispCopySrc(4, 8, 640, 480);
GXSetDispCopyDst(640, 480);
const u32 lines = GXSetDispCopyYScale(1.0f);
auto bytes = capture_fifo();
EXPECT_EQ(lines, 480u);
EXPECT_TRUE(has_bp_write(bytes, 0x49));
EXPECT_TRUE(has_bp_write(bytes, 0x4A));
EXPECT_TRUE(has_bp_write(bytes, 0x4D));
EXPECT_TRUE(has_bp_write(bytes, 0x4E));
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.dispCopySrc.x, 4);
EXPECT_EQ(g_gxState.dispCopySrc.y, 8);
EXPECT_EQ(g_gxState.dispCopySrc.width, 640);
EXPECT_EQ(g_gxState.dispCopySrc.height, 480);
EXPECT_NEAR(g_gxState.dispCopyYScale, 1.0f, 0.001f);
}
TEST_F(GXFifoTest, DispCopyYScaleHelpers_MatchIdentityScale) {
EXPECT_EQ(GXGetNumXfbLines(480, 1.0f), 480u);
EXPECT_NEAR(GXGetYScaleFactor(480, 480), 1.0f, 0.001f);
}
TEST_F(GXFifoTest, CopyFilter_EncodesAndDecodesAaAndVerticalFilter) {
u8 samples[12][2] = {};
for (size_t i = 0; i < 12; ++i) {
samples[i][0] = static_cast<u8>((i * 2) & 0x0f);
samples[i][1] = static_cast<u8>((i * 2 + 1) & 0x0f);
}
u8 vfilter[7] = {1, 2, 3, 4, 5, 6, 7};
GXSetCopyFilter(GX_TRUE, samples, GX_TRUE, vfilter);
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 30u);
EXPECT_EQ(bytes[1], 0x01);
EXPECT_EQ(bytes[6], 0x02);
EXPECT_EQ(bytes[11], 0x03);
EXPECT_EQ(bytes[16], 0x04);
EXPECT_EQ(bytes[21], 0x53);
EXPECT_EQ(bytes[26], 0x54);
reset_gx_state();
decode_fifo(bytes);
EXPECT_TRUE(g_gxState.copyFilterAa);
EXPECT_TRUE(g_gxState.copyFilterVf);
for (size_t i = 0; i < 12; ++i) {
EXPECT_EQ(g_gxState.copyFilterSamplePattern[i][0], samples[i][0]);
EXPECT_EQ(g_gxState.copyFilterSamplePattern[i][1], samples[i][1]);
}
for (size_t i = 0; i < 7; ++i) {
EXPECT_EQ(g_gxState.copyFilterVFilter[i], vfilter[i]);
}
}
TEST_F(GXFifoTest, CopyFilter_DisabledUsesSdkFallbackCoefficients) {
u8 samples[12][2] = {};
u8 vfilter[7] = {};
GXSetCopyFilter(GX_FALSE, samples, GX_FALSE, vfilter);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_FALSE(g_gxState.copyFilterAa);
EXPECT_FALSE(g_gxState.copyFilterVf);
for (const auto& sample : g_gxState.copyFilterSamplePattern) {
EXPECT_EQ(sample[0], 6u);
EXPECT_EQ(sample[1], 6u);
}
const std::array<u8, 7> expectedVFilter{0, 0, 21, 22, 21, 0, 0};
EXPECT_EQ(g_gxState.copyFilterVFilter, expectedVFilter);
}
TEST_F(GXFifoTest, DispCopyGamma_UpdatesCopyTriggerShadowBits) {
GXSetDispCopyGamma(GX_GM_2_2);
EXPECT_EQ(g_gxState.dispCopyGamma, GX_GM_2_2);
EXPECT_EQ((g_gxState.bpRegCache[0x52] >> 7) & 3u, static_cast<u32>(GX_GM_2_2));
}
TEST_F(GXFifoTest, CopyTrigger_DecodesClampGammaFormatHalfScaleAndFrameMode) {
const u32 value = (static_cast<u32>(GX_CLAMP_TOP) << 0) | (static_cast<u32>(GX_TF_RGBA8) << 3) |
(static_cast<u32>(GX_GM_1_7) << 7) | (1u << 9) | (2u << 12);
auto bytes = bp_cmd(0x52, value);
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.copyClamp, GX_CLAMP_TOP);
EXPECT_EQ(g_gxState.texCopyFmt, GX_TF_RGBA8);
EXPECT_EQ(g_gxState.dispCopyGamma, GX_GM_1_7);
EXPECT_TRUE(g_gxState.texCopyHalfScale);
EXPECT_EQ(g_gxState.dispCopyFrame2Field, 2u);
}
TEST_F(GXFifoTest, CopyTexClearTruePassesScratchRectAndUpdateMasksToResolve) {
std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
gxState().colorUpdate = true;
gxState().alphaUpdate = true;
gxState().depthUpdate = true;
gxState().dstAlpha = UINT32_MAX;
gxState().clearColor = {64.f / 255.f, 128.f / 255.f, 192.f / 255.f, 32.f / 255.f};
gxState().clearDepth = 0x123456;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
const auto& resolve = records.front();
EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{336, 300, 152, 114}));
ASSERT_TRUE(resolve.sourceRectPixels.has_value());
EXPECT_EQ(resolve.sourceRectPixels->x(), 336.f);
EXPECT_EQ(resolve.sourceRectPixels->y(), 300.f);
EXPECT_EQ(resolve.sourceRectPixels->z(), 152.f);
EXPECT_EQ(resolve.sourceRectPixels->w(), 114.f);
EXPECT_TRUE(resolve.clearColor);
EXPECT_TRUE(resolve.clearAlpha);
EXPECT_TRUE(resolve.clearDepth);
EXPECT_NEAR(resolve.clearColorValue.x(), 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(resolve.clearColorValue.y(), 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(resolve.clearColorValue.z(), 192.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(resolve.clearColorValue.w(), 32.f / 255.f, 1.f / 255.f);
// clear_depth_value() maps the guest's GX-distance clear depth into the host depth-buffer
// convention, so under reversed Z the stored value is the 1-x mirror of the guest's normalized
// depth. Expressed through UseReversedZ rather than hardcoded, so this expectation follows the
// convention instead of pinning one side of it.
const float expectedNormalizedClearDepth = 0x123456 / 16777216.f;
EXPECT_NEAR(resolve.clearDepthValue,
aurora::gx::UseReversedZ ? 1.f - expectedNormalizedClearDepth : expectedNormalizedClearDepth,
1.f / 16777216.f);
EXPECT_EQ(resolve.resolveFormat, GX_TF_RGBA8);
EXPECT_FALSE(resolve.halfScale);
EXPECT_FALSE(resolve.forceOpaqueAlpha);
EXPECT_EQ(gxState().copyTextures.at(image.data()).revision, 1u);
}
TEST_F(GXFifoTest, CopyTexColorFormatMarksResolvePersistent) {
std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE);
aurora::gfx::testing::set_current_frame(42);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_TRUE(records.front().persistentCopy);
ASSERT_TRUE(records.front().texture);
EXPECT_TRUE(records.front().texture->isEfbCopy);
EXPECT_EQ(records.front().texture->lastEfbCopyFrame, 42u);
EXPECT_TRUE(records.front().texture->is_recent_efb_copy(42));
EXPECT_TRUE(records.front().texture->is_recent_efb_copy(43));
EXPECT_FALSE(records.front().texture->is_recent_efb_copy(44));
GXTexObj_ texObj{};
texObj.mWidth = 152;
texObj.mHeight = 114;
texObj.mFormat = GX_TF_RGBA8;
gxState().textures[GX_TEXMAP0] = aurora::gfx::TextureBind{texObj, records.front().texture};
aurora::gx::ShaderConfig shader{};
shader.numTexGens = 1;
shader.tevStageCount = 1;
shader.tevStages[0].texCoordId = GX_TEXCOORD0;
shader.tevStages[0].texMapId = GX_TEXMAP0;
shader.tevStages[0].colorPass.d = GX_CC_TEXC;
shader.tevStages[0].alphaPass.d = GX_CA_TEXA;
const auto info = aurora::gx::build_shader_info(shader);
aurora::gfx::testing::reset_uniform_allocations();
const auto freshLayout = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{},
aurora::gx::FrameInterpolationDrawIdentity{}, false);
EXPECT_TRUE(freshLayout.replayLayout.nativeEfbEffect);
// Once retained instead of regenerated, the same reduced alpha texture is a
// one-shot 2D bake (the path used by MKW's minimap), not live post-processing.
aurora::gfx::testing::set_current_frame(44);
const auto retainedLayout = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{},
aurora::gx::FrameInterpolationDrawIdentity{}, false);
EXPECT_FALSE(retainedLayout.replayLayout.nativeEfbEffect);
}
TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE);
aurora::gfx::testing::set_current_frame(10);
GXCopyTex(image.data(), GX_FALSE);
aurora::gfx::testing::set_current_frame(11);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 2u);
EXPECT_TRUE(records[0].persistentCopy);
EXPECT_FALSE(records[1].persistentCopy);
}
TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE);
aurora::gfx::testing::set_current_frame(10);
GXCopyTex(image.data(), GX_FALSE);
aurora::gfx::testing::set_current_frame(12);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 2u);
EXPECT_TRUE(records[0].persistentCopy);
EXPECT_TRUE(records[1].persistentCopy);
}
TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
std::array<u8, 4 * 4 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(0, 0, 4, 4);
GXSetTexCopyDst(4, 4, GX_TF_Z24X8, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_FALSE(records.front().persistentCopy);
}
TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) {
GXSetDispCopySrc(0, 0, 32, 32);
GXSetDispCopyDst(32, 32);
GXCopyDisp(nullptr, GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_FALSE(records.front().persistentCopy);
}
TEST_F(GXFifoTest, CopyTexMipmapDstRequestsHalfScaleResolve) {
std::array<u8, 76 * 57 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(76, 57, GX_TF_RGBA8, GX_TRUE);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
const auto& resolve = records.front();
EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{336, 300, 152, 114}));
ASSERT_TRUE(resolve.sourceRectPixels.has_value());
EXPECT_EQ(resolve.sourceRectPixels->x(), 336.f);
EXPECT_EQ(resolve.sourceRectPixels->y(), 300.f);
EXPECT_EQ(resolve.sourceRectPixels->z(), 152.f);
EXPECT_EQ(resolve.sourceRectPixels->w(), 114.f);
EXPECT_TRUE(resolve.halfScale);
EXPECT_TRUE(gxState().texCopyHalfScale);
EXPECT_EQ(gxState().copyTextures.at(image.data()).width, 76u);
EXPECT_EQ(gxState().copyTextures.at(image.data()).height, 57u);
}
TEST_F(GXFifoTest, CopyTexRetainsInternalResolutionInGpuCopyAndGuestDimensionsInCache) {
std::array<u8, 152 * 114 * 4> image{};
aurora::gfx::testing::set_framebuffer_sizes(640, 528, 2560, 2112);
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
const auto& resolve = records.front();
EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{1344, 1200, 608, 456}));
ASSERT_TRUE(resolve.sourceRectPixels.has_value());
EXPECT_EQ(resolve.sourceRectPixels->x(), 1344.f);
EXPECT_EQ(resolve.sourceRectPixels->y(), 1200.f);
EXPECT_EQ(resolve.sourceRectPixels->z(), 608.f);
EXPECT_EQ(resolve.sourceRectPixels->w(), 456.f);
ASSERT_TRUE(resolve.texture);
EXPECT_EQ(resolve.texture->size.width, 608u);
EXPECT_EQ(resolve.texture->size.height, 456u);
EXPECT_FLOAT_EQ(resolve.copyFilterRowStride, 4.0f);
const auto& copy = gxState().copyTextures.at(image.data());
EXPECT_EQ(copy.width, 152u);
EXPECT_EQ(copy.height, 114u);
EXPECT_EQ(copy.dataSize, GXGetTexBufferSize(152, 114, GX_TF_RGBA8, GX_FALSE, 0));
}
TEST_F(GXFifoTest, CopyTexUsesExactRationalScaledEdgesBeforeClearing) {
std::array<u8, 32 * 32 * 4> image{};
// float32 evaluates 7 * (62 / 14) just below 31. Without stabilizing
// the rational copy edges, floor() expands this clear to begin at x/y 30.
aurora::gfx::testing::set_framebuffer_sizes(14, 14, 62, 62);
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(7, 7, 7, 7);
GXSetTexCopyDst(31, 31, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
const auto& resolve = records.front();
EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{31, 31, 31, 31}));
ASSERT_TRUE(resolve.sourceRectPixels.has_value());
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->x(), 31.0f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->y(), 31.0f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->z(), 31.0f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->w(), 31.0f);
}
TEST_F(GXFifoTest, CopyTexClearDoesNotOverlapFractionalLeftEdge) {
std::array<u8, 152 * 114 * 4> image{};
// MKW split-screen scratch-copy geometry from the regression capture: the scaled source edge is
// x=492.975, so flooring clears column 492 while the matching viewport rasterizes from 493.
aurora::gfx::testing::set_framebuffer_sizes(640, 528, 939, 528);
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
const auto& resolve = records.front();
EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{493, 300, 223, 114}));
ASSERT_TRUE(resolve.sourceRectPixels.has_value());
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->x(), 492.975f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->y(), 300.0f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->z(), 223.0125f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->w(), 114.0f);
}
TEST_F(GXFifoTest, CopyTexClearBoundsRemainExactAtSixTimesInternalResolution) {
std::array<u8, 152 * 114 * 4> image{};
aurora::gfx::testing::set_framebuffer_sizes(640, 528, 3840, 3168);
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(336, 300, 152, 114);
GXSetTexCopyDst(152, 114, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
const auto& resolve = records.front();
EXPECT_EQ(resolve.rect, (aurora::gfx::ClipRect{2016, 1800, 912, 684}));
ASSERT_TRUE(resolve.sourceRectPixels.has_value());
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->x(), 2016.0f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->y(), 1800.0f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->z(), 912.0f);
EXPECT_FLOAT_EQ(resolve.sourceRectPixels->w(), 684.0f);
}
TEST_F(GXFifoTest, CopyTexRecreatesGpuCopyWhenInternalResolutionChanges) {
std::array<u8, 64 * 64 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(0, 0, 64, 64);
GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const auto* nativeHandle = gxState().copyTextures.at(image.data()).handle.get();
ASSERT_NE(nativeHandle, nullptr);
EXPECT_EQ(nativeHandle->size.width, 64u);
aurora::gfx::testing::set_framebuffer_sizes(640, 480, 1280, 960);
GXCopyTex(image.data(), GX_FALSE);
const auto& scaledCopy = gxState().copyTextures.at(image.data());
ASSERT_TRUE(scaledCopy.handle);
EXPECT_NE(scaledCopy.handle.get(), nativeHandle);
EXPECT_EQ(scaledCopy.handle->size.width, 128u);
EXPECT_EQ(scaledCopy.handle->size.height, 128u);
EXPECT_EQ(scaledCopy.width, 64u);
EXPECT_EQ(scaledCopy.height, 64u);
EXPECT_EQ(scaledCopy.revision, 2u);
}
TEST_F(GXFifoTest, CopyTexPassesVerticalCopyFilterCoefficientsToResolve) {
std::array<u8, 32 * 32 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
u8 vfilter[7] = {3, 5, 7, 11, 13, 17, 19};
GXSetCopyFilter(GX_FALSE, nullptr, GX_TRUE, vfilter);
GXSetTexCopySrc(0, 0, 32, 32);
GXSetTexCopyDst(32, 32, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_EQ(records.front().copyFilterCoefficients, (std::array<u32, 3>{8, 31, 36}));
}
TEST_F(GXFifoTest, CopyTexPassesVerticalCopyClampToResolve) {
std::array<u8, 32 * 32 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetCopyClamp(GX_CLAMP_TOP);
GXSetTexCopySrc(8, 12, 32, 32);
GXSetTexCopyDst(32, 32, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_TRUE(records.front().clampTop);
EXPECT_FALSE(records.front().clampBottom);
}
TEST_F(GXFifoTest, CopyDispCanDisableSpatialCopyFilterWithoutChangingBrightness) {
u8 vfilter[7] = {3, 5, 7, 11, 13, 17, 19};
aurora::g_config.disableCopyFilter = true;
GXSetCopyFilter(GX_FALSE, nullptr, GX_TRUE, vfilter);
GXSetDispCopySrc(0, 0, 32, 32);
GXSetDispCopyDst(32, 32);
GXCopyDisp(nullptr, GX_FALSE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_EQ(records.front().copyFilterCoefficients, (std::array<u32, 3>{0, 75, 0}));
}
TEST_F(GXFifoTest, CopyTexDepthZ16PreservesVerticalCopyFilterForConversion) {
std::array<u8, 32 * 32 * 2> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
u8 vfilter[7] = {21, 0, 0, 22, 0, 21, 0};
GXSetCopyFilter(GX_FALSE, nullptr, GX_TRUE, vfilter);
EXPECT_TRUE(gxState().copyFilterVf);
EXPECT_EQ(gxState().copyFilterVFilter, (std::array<u8, 7>{21, 0, 0, 22, 0, 21, 0}));
GXSetTexCopySrc(0, 0, 32, 32);
GXSetTexCopyDst(32, 32, GX_TF_Z16, GX_FALSE);
EXPECT_EQ(gxState().copyFilterVFilter, (std::array<u8, 7>{21, 0, 0, 22, 0, 21, 0}));
GXCopyTex(image.data(), GX_FALSE);
EXPECT_EQ(gxState().copyFilterVFilter, (std::array<u8, 7>{21, 0, 0, 22, 0, 21, 0}));
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_EQ(records.front().resolveFormat, GX_TF_Z16);
EXPECT_EQ(records.front().copyFilterCoefficients, (std::array<u32, 3>{21, 22, 21}));
EXPECT_FALSE(records.front().forceOpaqueAlpha);
EXPECT_EQ(gxState().copyTextures.at(image.data()).format, GX_TF_Z16);
}
TEST_F(GXFifoTest, CopyTexDepthZ16UsesRa8Ia8Semantics) {
constexpr auto rgba = aurora::gfx::tex_copy_conv::detail::z16_ra8_as_ia8_rgba(0x12);
EXPECT_EQ(rgba, (std::array<std::uint8_t, 4>{0x12, 0x12, 0x12, 0xff}));
constexpr auto shader = aurora::gfx::tex_copy_conv::detail::Z16FragmentShader;
EXPECT_NE(shader.find("let i = f32(depth_bytes.r)"), std::string_view::npos);
EXPECT_NE(shader.find("return vec4f(i, i, i, 1.0)"), std::string_view::npos);
}
TEST_F(GXFifoTest, CopyTexClearTrueNoOpsWhenAllUpdateMasksAreDisabled) {
std::array<u8, 64 * 64 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
gxState().colorUpdate = false;
gxState().alphaUpdate = false;
gxState().depthUpdate = false;
gxState().dstAlpha = UINT32_MAX;
GXSetTexCopySrc(8, 16, 64, 64);
GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_FALSE(records.front().clearColor);
EXPECT_FALSE(records.front().clearAlpha);
EXPECT_FALSE(records.front().clearDepth);
}
TEST_F(GXFifoTest, CopyTexClearTrueRgbTargetKeepsFormatAndForcesOpaqueCopiedAlpha) {
std::array<u8, 32 * 32 * 4> image{};
gxState().pixelFmt = GX_PF_RGB8_Z24;
gxState().colorUpdate = true;
gxState().alphaUpdate = false;
gxState().depthUpdate = false;
gxState().dstAlpha = UINT32_MAX;
gxState().clearColor = {0.2f, 0.4f, 0.6f, 0.25f};
GXSetTexCopySrc(0, 0, 32, 32);
GXSetTexCopyDst(32, 32, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_TRUE(records.front().clearColor);
EXPECT_FALSE(records.front().clearAlpha);
EXPECT_FALSE(records.front().clearDepth);
EXPECT_NEAR(records.front().clearColorValue.x(), 0.2f, 1.f / 255.f);
EXPECT_NEAR(records.front().clearColorValue.y(), 0.4f, 1.f / 255.f);
EXPECT_NEAR(records.front().clearColorValue.z(), 0.6f, 1.f / 255.f);
EXPECT_EQ(records.front().clearColorValue.w(), 0.25f);
EXPECT_EQ(records.front().resolveFormat, GX_TF_RGBA8);
EXPECT_TRUE(records.front().forceOpaqueAlpha);
}
TEST_F(GXFifoTest, CopyTexSameTargetReusesHostTextureUntilSampledThisFrame) {
std::array<u8, 64 * 64 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(0, 0, 64, 64);
GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const auto& first = gxState().copyTextures.at(image.data());
ASSERT_TRUE(first.handle);
const auto* firstHandle = first.handle.get();
EXPECT_EQ(first.revision, 1u);
GXCopyTex(image.data(), GX_FALSE);
const auto& second = gxState().copyTextures.at(image.data());
EXPECT_EQ(second.handle.get(), firstHandle);
EXPECT_EQ(second.revision, 2u);
}
TEST_F(GXFifoTest, CopyTexSameFrameSampledTargetGetsFreshHostTexture) {
std::array<u8, 64 * 64 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(0, 0, 64, 64);
GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const aurora::gx::GXState::CopyTextureKey key{
.dest = image.data(),
.width = 64,
.height = 64,
.format = GX_TF_RGBA8,
};
const auto& first = gxState().copyTextures.at(image.data());
ASSERT_TRUE(first.handle);
const auto* firstHandle = first.handle.get();
auto& cached = gxState().copyTextureCache.at(key);
cached.lastSampledFrame = aurora::gfx::current_frame();
cached.sampledThisFrame = true;
GXCopyTex(image.data(), GX_FALSE);
const auto& second = gxState().copyTextures.at(image.data());
ASSERT_TRUE(second.handle);
EXPECT_NE(second.handle.get(), firstHandle);
EXPECT_EQ(second.revision, 2u);
}
TEST_F(GXFifoTest, CopyTexSameTargetDifferentShapeRetiresHistoricalCacheEntry) {
std::vector<u8> image(608 * 456 * 4);
gxState().pixelFmt = GX_PF_RGBA6_Z24;
GXSetTexCopySrc(0, 0, 608, 456);
GXSetTexCopyDst(608, 456, GX_TF_RGB565, GX_FALSE);
GXCopyTex(image.data(), GX_FALSE);
const aurora::gx::GXState::CopyTextureKey fullKey{
.dest = image.data(),
.width = 608,
.height = 456,
.format = GX_TF_RGB565,
};
const auto& fullCopy = gxState().copyTextureCache.at(fullKey);
ASSERT_TRUE(fullCopy.handle);
const auto* fullHandle = fullCopy.handle.get();
GXSetTexCopySrc(0, 0, 304, 228);
GXSetTexCopyDst(304, 228, GX_TF_RGBA8, GX_TRUE);
GXCopyTex(image.data(), GX_FALSE);
const aurora::gx::GXState::CopyTextureKey halfKey{
.dest = image.data(),
.width = 304,
.height = 228,
.format = GX_TF_RGBA8,
};
const auto& halfCopy = gxState().copyTextureCache.at(halfKey);
ASSERT_TRUE(halfCopy.handle);
EXPECT_FALSE(gxState().copyTextureCache.contains(fullKey));
EXPECT_EQ(gxState().copyTextureCache.size(), 1u);
EXPECT_NE(halfCopy.handle.get(), fullHandle);
EXPECT_EQ(gxState().copyTextures.at(image.data()).handle.get(), halfCopy.handle.get());
EXPECT_EQ(gxState().copyTextures.at(image.data()).width, 304u);
EXPECT_EQ(gxState().copyTextures.at(image.data()).height, 228u);
EXPECT_EQ(gxState().copyTextures.at(image.data()).format, GX_TF_RGBA8);
}
TEST_F(GXFifoTest, CopyTexCtfFormatsCanBackCompatibleTextureFormats) {
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_R4, GX_TF_I4));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_R8, GX_TF_I8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_G8, GX_TF_I8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_B8, GX_TF_I8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_RA4, GX_TF_IA4));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_RA8, GX_TF_IA8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_RG8, GX_TF_IA8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_GB8, GX_TF_IA8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_TF_Z16, GX_TF_IA8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_CTF_YUVA8, GX_TF_RGBA8));
EXPECT_TRUE(aurora::gx::copy_texture_format_compatible(GX_TF_Z24X8, GX_TF_RGBA8));
EXPECT_FALSE(aurora::gx::copy_texture_format_compatible(GX_CTF_R8, GX_TF_RGBA8));
}
TEST_F(GXFifoTest, FieldMaskModeRevBitsAndFogRange_DecodeKnownBpState) {
std::vector<u8> bytes;
const auto fieldMask = bp_cmd(0x44, 0x03);
const auto revBits = bp_cmd(0x58, 0x0F);
const auto fieldMode = bp_cmd(0x68, 0x01);
const auto fogRangeBase = bp_cmd(0xE8, 0x0156);
const auto fogRangeK0 = bp_cmd(0xE9, 0x123456);
bytes.insert(bytes.end(), fieldMask.begin(), fieldMask.end());
bytes.insert(bytes.end(), revBits.begin(), revBits.end());
bytes.insert(bytes.end(), fieldMode.begin(), fieldMode.end());
bytes.insert(bytes.end(), fogRangeBase.begin(), fogRangeBase.end());
bytes.insert(bytes.end(), fogRangeK0.begin(), fogRangeK0.end());
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.fieldMask, 0x03u);
EXPECT_EQ(g_gxState.revBits, 0x0Fu);
EXPECT_EQ(g_gxState.fieldMode, 0x01u);
EXPECT_EQ(g_gxState.fogRange[0], 0x0156u);
EXPECT_EQ(g_gxState.fogRange[1], 0x123456u);
}
TEST_F(GXFifoTest, XfErrorAndDualTex_DecodeKnownRawState) {
std::vector<u8> bytes;
const auto xfError = xf_cmd(0x1000, {0x3F});
const auto dualTex = xf_cmd(0x1012, {0x01});
bytes.insert(bytes.end(), xfError.begin(), xfError.end());
bytes.insert(bytes.end(), dualTex.begin(), dualTex.end());
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.xfError, 0x3Fu);
EXPECT_EQ(g_gxState.dualTex, 0x01u);
}
TEST_F(GXFifoTest, ClearBoundingBox_EncodesBpAndDecodesFallbackExtents) {
GXClearBoundingBox();
auto bytes = capture_fifo();
ASSERT_EQ(bytes.size(), 10u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0x55);
EXPECT_EQ(bytes[5], 0x61);
EXPECT_EQ(bytes[6], 0x56);
reset_gx_state();
g_gxState.boundingBox = {1, 2, 3, 4};
decode_fifo(bytes);
EXPECT_EQ(g_gxState.boundingBox[0], 1023u);
EXPECT_EQ(g_gxState.boundingBox[1], 0u);
EXPECT_EQ(g_gxState.boundingBox[2], 1023u);
EXPECT_EQ(g_gxState.boundingBox[3], 0u);
}
TEST_F(GXFifoTest, ReadBoundingBox_ReturnsTrackedState) {
g_gxState.boundingBox = {10, 20, 30, 40};
u16 left = 0;
u16 right = 0;
u16 top = 0;
u16 bottom = 0;
GXReadBoundingBox(&left, &right, &top, &bottom);
EXPECT_EQ(left, 10u);
EXPECT_EQ(right, 20u);
EXPECT_EQ(top, 30u);
EXPECT_EQ(bottom, 40u);
}
// --- Clear color and depth round-trip ---
TEST_F(GXFifoTest, CopyClear_ColorAndDepth) {
GXColor color = {64, 128, 192, 255};
GXSetCopyClear(color, 0x00ABCDEF);
auto bytes = capture_fifo();
// 3 BP writes: 3 * 5 = 15 bytes
ASSERT_EQ(bytes.size(), 15u);
EXPECT_EQ(bytes[0], 0x61);
EXPECT_EQ(bytes[1], 0x4F); // R + A
EXPECT_EQ(bytes[5], 0x61);
EXPECT_EQ(bytes[6], 0x50); // B + G
EXPECT_EQ(bytes[10], 0x61);
EXPECT_EQ(bytes[11], 0x51); // Z
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.clearColor[0], 64.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[1], 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[2], 192.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[3], 255.f / 255.f, 1.f / 255.f);
EXPECT_EQ(g_gxState.clearDepth, 0x00ABCDEFu);
}
// --- Clear with black and zero depth ---
TEST_F(GXFifoTest, CopyClear_BlackZeroDepth) {
GXColor color = {0, 0, 0, 0};
GXSetCopyClear(color, 0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.clearColor[0], 0.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[1], 0.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[2], 0.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[3], 0.f, 1.f / 255.f);
EXPECT_EQ(g_gxState.clearDepth, 0u);
}
// --- Clear with max depth ---
TEST_F(GXFifoTest, CopyClear_MaxDepth) {
GXColor color = {255, 255, 255, 128};
GXSetCopyClear(color, 0xFFFFFF);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_NEAR(g_gxState.clearColor[0], 1.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[1], 1.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[2], 1.f, 1.f / 255.f);
EXPECT_NEAR(g_gxState.clearColor[3], 128.f / 255.f, 1.f / 255.f);
EXPECT_EQ(g_gxState.clearDepth, 0xFFFFFFu);
}
TEST_F(GXFifoTest, PeekZ_ReturnsClearDepthFallbackAndRequestsSnapshot) {
g_gxState.clearDepth = 0x123456;
u32 z = 0;
GXPeekZ(10, 20, &z);
EXPECT_EQ(z, 0x123456u);
EXPECT_TRUE(aurora::gfx::depth_peek::testing::snapshot_requested());
}
TEST_F(GXFifoTest, PeekZ_ReturnsLatestCompletedSnapshot) {
aurora::gfx::depth_peek::testing::set_latest(2, 2, {0x000001, 0x000002, 0x000003, 0x01000004});
u32 z = 0;
GXPeekZ(1, 1, &z);
EXPECT_EQ(z, 0x000004u);
EXPECT_TRUE(aurora::gfx::depth_peek::testing::snapshot_requested());
}
TEST_F(GXFifoTest, PeekZ_OutOfRangeReturnsClearDepthFallback) {
g_gxState.clearDepth = 0xabcdef;
aurora::gfx::depth_peek::testing::set_latest(1, 1, {0x000001});
u32 z = 0;
GXPeekZ(1, 0, &z);
EXPECT_EQ(z, 0xabcdefu);
EXPECT_TRUE(aurora::gfx::depth_peek::testing::snapshot_requested());
}
// Composite tests (multiple state changes in a single FIFO stream)
TEST_F(GXFifoTest, Composite_BlendAndZMode) {
GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_NOOP);
GXSetZMode(true, GX_LEQUAL, true);
GXSetAlphaCompare(GX_GREATER, 128, GX_AOP_AND, GX_ALWAYS, 0);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.blendMode, GX_BM_BLEND);
EXPECT_EQ(g_gxState.blendFacSrc, GX_BL_SRCALPHA);
EXPECT_EQ(g_gxState.blendFacDst, GX_BL_INVSRCALPHA);
EXPECT_TRUE(g_gxState.depthCompare);
EXPECT_EQ(g_gxState.depthFunc, GX_LEQUAL);
EXPECT_TRUE(g_gxState.depthUpdate);
EXPECT_EQ(g_gxState.alphaCompare.comp0, GX_GREATER);
EXPECT_EQ(g_gxState.alphaCompare.ref0, 128u);
}
// --- GXLoadTexMtxImm for PTTexMtx (XF 0x500-0x5EF) ---
TEST_F(GXFifoTest, LoadPTTexMtx_Identity) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 1.0f;
mtx.m1[1] = 1.0f;
mtx.m2[2] = 1.0f;
GXLoadTexMtxImm(&mtx, GX_PTTEXMTX0, GX_MTX3x4);
auto bytes = capture_fifo();
// XF opcode 0x10, addr = (64 - 64) * 4 + 0x500 = 0x500, count = 12
ASSERT_GE(bytes.size(), 5u);
EXPECT_EQ(bytes[0], 0x10);
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.ptTexMtxs[0];
EXPECT_FLOAT_EQ(decoded.m0[0], 1.0f);
EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 1.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 1.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f);
}
TEST_F(GXFifoTest, LoadPTTexMtx_ArbitraryValues) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 2.0f;
mtx.m0[1] = 0.5f;
mtx.m0[2] = 0.0f;
mtx.m0[3] = 10.0f;
mtx.m1[0] = -0.5f;
mtx.m1[1] = 3.0f;
mtx.m1[2] = 0.0f;
mtx.m1[3] = 20.0f;
mtx.m2[0] = 0.0f;
mtx.m2[1] = 0.0f;
mtx.m2[2] = 1.5f;
mtx.m2[3] = -5.0f;
GXLoadTexMtxImm(&mtx, GX_PTTEXMTX0, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.ptTexMtxs[0];
EXPECT_FLOAT_EQ(decoded.m0[0], 2.0f);
EXPECT_FLOAT_EQ(decoded.m0[1], 0.5f);
EXPECT_FLOAT_EQ(decoded.m0[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m0[3], 10.0f);
EXPECT_FLOAT_EQ(decoded.m1[0], -0.5f);
EXPECT_FLOAT_EQ(decoded.m1[1], 3.0f);
EXPECT_FLOAT_EQ(decoded.m1[2], 0.0f);
EXPECT_FLOAT_EQ(decoded.m1[3], 20.0f);
EXPECT_FLOAT_EQ(decoded.m2[0], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 1.5f);
EXPECT_FLOAT_EQ(decoded.m2[3], -5.0f);
}
TEST_F(GXFifoTest, LoadPTTexMtx_DifferentSlots) {
aurora::Mat3x4<float> mtx0{};
mtx0.m0[0] = 1.0f;
mtx0.m1[1] = 1.0f;
mtx0.m2[2] = 1.0f;
aurora::Mat3x4<float> mtx5{};
mtx5.m0[0] = 5.0f;
mtx5.m1[1] = 6.0f;
mtx5.m2[2] = 7.0f;
mtx5.m0[3] = 100.0f;
GXLoadTexMtxImm(&mtx0, GX_PTTEXMTX0, GX_MTX3x4);
GXLoadTexMtxImm(&mtx5, GX_PTTEXMTX5, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// Slot 0
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m0[0], 1.0f);
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m1[1], 1.0f);
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m2[2], 1.0f);
// Slot 5: GX_PTTEXMTX5 = 79, index = (79 - 64) / 3 = 5
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m0[0], 5.0f);
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m1[1], 6.0f);
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m2[2], 7.0f);
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[5].m0[3], 100.0f);
}
TEST_F(GXFifoTest, LoadPTTexMtx_LastSlot) {
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 42.0f;
mtx.m1[1] = 43.0f;
mtx.m2[2] = 44.0f;
GXLoadTexMtxImm(&mtx, GX_PTTEXMTX19, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
auto& decoded = g_gxState.ptTexMtxs[19];
EXPECT_FLOAT_EQ(decoded.m0[0], 42.0f);
EXPECT_FLOAT_EQ(decoded.m1[1], 43.0f);
EXPECT_FLOAT_EQ(decoded.m2[2], 44.0f);
// Other elements should be zero (from reset)
EXPECT_FLOAT_EQ(decoded.m0[1], 0.0f);
EXPECT_FLOAT_EQ(decoded.m2[3], 0.0f);
}
TEST_F(GXFifoTest, LoadPTTexMtx_Isolation) {
// Loading PTTexMtx0 should not affect PTTexMtx1
aurora::Mat3x4<float> mtx{};
mtx.m0[0] = 99.0f;
mtx.m1[1] = 88.0f;
mtx.m2[2] = 77.0f;
GXLoadTexMtxImm(&mtx, GX_PTTEXMTX0, GX_MTX3x4);
auto bytes = capture_fifo();
reset_gx_state();
decode_fifo(bytes);
// Slot 0 should have our values
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[0].m0[0], 99.0f);
// Slot 1 should remain zeroed
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[1].m0[0], 0.0f);
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[1].m1[1], 0.0f);
EXPECT_FLOAT_EQ(g_gxState.ptTexMtxs[1].m2[2], 0.0f);
}
// Composite / multi-command tests
TEST_F(GXFifoTest, Composite_TevSetup) {
// Set up a simple 1-stage TEV that passes through texture color
GXSetNumTevStages(1);
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0);
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_TEXC);
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA);
// TEV order writes to dirty state, so flush before capture
auto bytes = flush_and_capture();
reset_gx_state();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.numTevStages, 1u);
EXPECT_EQ(g_gxState.tevStages[0].texMapId, GX_TEXMAP0);
EXPECT_EQ(g_gxState.tevStages[0].texCoordId, GX_TEXCOORD0);
EXPECT_EQ(g_gxState.tevStages[0].channelId, GX_COLOR0A0);
EXPECT_EQ(g_gxState.tevStages[0].colorPass.d, GX_CC_TEXC);
EXPECT_EQ(g_gxState.tevStages[0].alphaPass.d, GX_CA_TEXA);
}
// Mario Kart Wii's dynamic shadows: EGG::DrawPathShadowVolume counts the shadow
// volumes' coverage into the EFB alpha plane with perspective draws, then one
// full-screen orthographic quad darkens the image by destination alpha. The
// quad samples nothing, so only its blend factors say it composes with the
// framebuffer; it must keep its recorded transforms in an immersive eye.
static void draw_full_screen_ortho_quad_with_blend(GXBlendFactor src, GXBlendFactor dst) {
aurora::gfx::testing::use_real_vertex_format_helpers(true);
aurora::gfx::testing::use_draw_command_tracking(true);
aurora::Mat4x4<float> proj{};
proj.m0[0] = 2.0f / 608.0f;
proj.m0[3] = -1.0f;
proj.m1[1] = -2.0f / 456.0f;
proj.m1[3] = 1.0f;
proj.m2[2] = -1.0f;
proj.m3[3] = 1.0f;
GXSetProjection(&proj, GX_ORTHOGRAPHIC);
GXSetViewport(0.0f, 0.0f, 608.0f, 456.0f, 0.0f, 1.0f);
GXSetScissor(0, 0, 608, 456);
aurora::Mat3x4<float> identity{};
identity.m0[0] = identity.m1[1] = identity.m2[2] = 1.0f;
GXLoadPosMtxImm(&identity, GX_PNMTX0);
GXSetCurrentMtx(GX_PNMTX0);
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
GXSetNumChans(0);
GXSetNumTexGens(0);
GXSetNumTevStages(1);
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL);
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0);
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_A0);
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
GXSetBlendMode(GX_BM_BLEND, src, dst, GX_LO_CLEAR);
GXSetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE);
const float corners[4][2]{{0.0f, 0.0f}, {608.0f, 0.0f}, {608.0f, 456.0f}, {0.0f, 456.0f}};
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
for (const auto& corner : corners) {
GXPosition3f32(corner[0], corner[1], 0.0f);
}
GXEnd();
}
TEST_F(GXFifoTest, OrthographicQuadBlendingWithDestinationAlphaIsNativeEfbEffect) {
draw_full_screen_ortho_quad_with_blend(GX_BL_DSTALPHA, GX_BL_INVDSTALPHA);
decode_fifo(flush_and_capture());
const auto* draw = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(draw, nullptr);
EXPECT_FALSE(draw->uniformReplayLayout.perspective);
EXPECT_TRUE(draw->uniformReplayLayout.nativeEfbEffect);
}
TEST_F(GXFifoTest, OrthographicQuadBlendingWithSourceAlphaStaysOnScreen) {
// An ordinary alpha-blended 2D element (a fade, a translucent HUD panel)
// depends on its own alpha only and remains virtual-screen content.
draw_full_screen_ortho_quad_with_blend(GX_BL_SRCALPHA, GX_BL_INVSRCALPHA);
decode_fifo(flush_and_capture());
const auto* draw = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(draw, nullptr);
EXPECT_FALSE(draw->uniformReplayLayout.perspective);
EXPECT_FALSE(draw->uniformReplayLayout.nativeEfbEffect);
}
TEST_F(GXFifoTest, OffscreenViewportPassOverFreshCopyIsNativeEfbEffect) {
// MKW builds its object shadow map in a 440x440 corner of the EFB: every
// stage is a full-resolution, unblended orthographic pass over the previous
// stage's copy, drawn in that corner's viewport and copied back out. The
// displayed frame never shows those passes, so they are not 2D-layer content.
std::array<u8, 440 * 440 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
aurora::gfx::testing::set_framebuffer_sizes(640, 528, 640, 528);
GXSetTexCopySrc(0, 0, 440, 440);
GXSetTexCopyDst(440, 440, GX_TF_RGBA8, GX_FALSE);
aurora::gfx::testing::set_current_frame(42);
GXCopyTex(image.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
ASSERT_TRUE(records.front().texture);
GXTexObj_ texObj{};
texObj.mWidth = 440;
texObj.mHeight = 440;
texObj.mFormat = GX_TF_RGBA8;
gxState().textures[GX_TEXMAP0] = aurora::gfx::TextureBind{texObj, records.front().texture};
gxState().blendMode = GX_BM_NONE;
aurora::gx::ShaderConfig shader{};
shader.numTexGens = 1;
shader.tevStageCount = 1;
shader.tevStages[0].texCoordId = GX_TEXCOORD0;
shader.tevStages[0].texMapId = GX_TEXMAP0;
shader.tevStages[0].colorPass.d = GX_CC_TEXC;
shader.tevStages[0].alphaPass.d = GX_CA_TEXA;
const auto info = aurora::gx::build_shader_info(shader);
gxState().logicalViewport = {0.0f, 0.0f, 440.0f, 440.0f, 0.0f, 1.0f};
aurora::gfx::testing::reset_uniform_allocations();
const auto cornerLayout = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{},
aurora::gx::FrameInterpolationDrawIdentity{}, false);
EXPECT_TRUE(cornerLayout.replayLayout.nativeEfbEffect);
// The same full-resolution, opaque copy drawn across the whole frame is a
// frozen-frame background (pause menus) and stays on the screen.
gxState().logicalViewport = {0.0f, 0.0f, 640.0f, 528.0f, 0.0f, 1.0f};
aurora::gfx::testing::reset_uniform_allocations();
const auto frameLayout = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{},
aurora::gx::FrameInterpolationDrawIdentity{}, false);
EXPECT_FALSE(frameLayout.replayLayout.nativeEfbEffect);
}
TEST(GXPipelineConfig, StereoStencilFieldCarriesTheCompositeSourceBlendBit) {
aurora::gx::PipelineConfig config{};
config.stereoStencil = aurora::gx::kStereoStencilFormat | aurora::gx::kCompositeSourceBlend;
EXPECT_TRUE(aurora::gx::valid_pipeline_config(config));
config.stereoStencil = (aurora::gx::kStereoStencilFormat | aurora::gx::kCompositeSourceBlend) + 1;
EXPECT_FALSE(aurora::gx::valid_pipeline_config(config));
}
TEST_F(GXFifoTest, NativeCompositeRecordsTheFrameSizedDepthCopyItSamples) {
// Mario Kart Wii's ghost kart: one orthographic quad blends a frame-sized colour copy back
// over the race with the depth of a frame-sized depth copy. The layout names that depth copy so
// the eye replay can find the pass whose draws produced it.
// Frame-sized buffers: static so two of them do not exhaust the test thread's stack.
static std::array<u8, 608 * 456 * 4> colour{};
static std::array<u8, 608 * 456 * 4> depth{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
aurora::gfx::testing::set_framebuffer_sizes(640, 528, 640, 528);
aurora::gfx::testing::set_current_frame(50);
GXSetTexCopySrc(0, 0, 608, 456);
GXSetTexCopyDst(608, 456, GX_TF_Z24X8, GX_FALSE);
GXCopyTex(depth.data(), GX_TRUE);
GXSetTexCopyDst(608, 456, GX_TF_RGB565, GX_FALSE);
GXCopyTex(colour.data(), GX_TRUE);
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 2u);
ASSERT_TRUE(records[0].texture);
ASSERT_TRUE(records[1].texture);
GXTexObj_ colourObj{};
colourObj.mWidth = 608;
colourObj.mHeight = 456;
colourObj.mFormat = GX_TF_RGB565;
GXTexObj_ depthObj{};
depthObj.mWidth = 608;
depthObj.mHeight = 456;
depthObj.mFormat = GX_TF_Z24X8;
gxState().textures[GX_TEXMAP0] = aurora::gfx::TextureBind{colourObj, records[1].texture};
gxState().textures[GX_TEXMAP1] = aurora::gfx::TextureBind{depthObj, records[0].texture};
gxState().blendMode = GX_BM_BLEND;
gxState().blendFacSrc = GX_BL_SRCALPHA;
gxState().blendFacDst = GX_BL_INVSRCALPHA;
gxState().logicalViewport = {0.0f, 0.0f, 640.0f, 528.0f, 0.0f, 1.0f};
aurora::gx::ShaderConfig shader{};
shader.numTexGens = 2;
shader.tevStageCount = 2;
shader.tevStages[0].texCoordId = GX_TEXCOORD0;
shader.tevStages[0].texMapId = GX_TEXMAP0;
shader.tevStages[0].colorPass.d = GX_CC_TEXC;
shader.tevStages[0].alphaPass.d = GX_CA_TEXA;
shader.tevStages[1].texCoordId = GX_TEXCOORD1;
shader.tevStages[1].texMapId = GX_TEXMAP1;
shader.tevStages[1].colorPass.d = GX_CC_TEXC;
shader.tevStages[1].alphaPass.d = GX_CA_TEXA;
const auto info = aurora::gx::build_shader_info(shader);
aurora::gfx::testing::reset_uniform_allocations();
const auto composite = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{},
aurora::gx::FrameInterpolationDrawIdentity{}, false);
EXPECT_TRUE(composite.replayLayout.nativeEfbEffect);
EXPECT_EQ(composite.replayLayout.compositeDepthCopy, records[0].texture.get());
// The same draw with the game camera is world geometry, whatever it samples.
aurora::gfx::testing::reset_uniform_allocations();
const auto world = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{},
aurora::gx::FrameInterpolationDrawIdentity{}, true);
EXPECT_EQ(world.replayLayout.compositeDepthCopy, nullptr);
}
static void draw_quad_with_projection(GXProjectionType type) {
aurora::gfx::testing::use_real_vertex_format_helpers(true);
aurora::gfx::testing::use_draw_command_tracking(true);
aurora::Mat4x4<float> proj{};
if (type == GX_PERSPECTIVE) {
proj.m0[0] = 1.0f;
proj.m1[1] = 1.0f;
proj.m2[2] = -1.0f;
proj.m2[3] = -1.0f;
proj.m3[2] = -1.0f;
} else {
proj.m0[0] = 2.0f / 608.0f;
proj.m0[3] = -1.0f;
proj.m1[1] = -2.0f / 456.0f;
proj.m1[3] = 1.0f;
proj.m2[2] = -1.0f;
proj.m3[3] = 1.0f;
}
GXSetProjection(&proj, type);
GXSetViewport(0.0f, 0.0f, 608.0f, 456.0f, 0.0f, 1.0f);
GXSetScissor(0, 0, 608, 456);
aurora::Mat3x4<float> identity{};
identity.m0[0] = identity.m1[1] = identity.m2[2] = 1.0f;
GXLoadPosMtxImm(&identity, GX_PNMTX0);
GXSetCurrentMtx(GX_PNMTX0);
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
GXSetNumChans(0);
GXSetNumTexGens(0);
GXSetNumTevStages(1);
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL);
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0);
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_A0);
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
const float corners[4][2]{{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
for (const auto& corner : corners) {
GXPosition3f32(corner[0], corner[1], -5.0f);
}
GXEnd();
}
TEST_F(GXFifoTest, CompositeSourcePipelineGetsConstantAlphaSiblingsFromItsNextDraw) {
// An eye re-issues MKW's ghost kart draws at the composite with constant-alpha siblings of
// their pipelines. Only a pipeline an eye asked for gets them, from its next draw on, and
// only in perspective draws.
draw_quad_with_projection(GX_PERSPECTIVE);
decode_fifo(flush_and_capture());
const auto* first = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(first, nullptr);
ASSERT_TRUE(first->uniformReplayLayout.perspective);
ASSERT_NE(first->pipeline, 0u);
EXPECT_EQ(first->compositeSourcePipeline, 0u);
EXPECT_EQ(first->stereoCompositeSourcePipeline, 0u);
const auto pipeline = first->pipeline;
aurora::gx::note_composite_source_pipeline(pipeline);
draw_quad_with_projection(GX_PERSPECTIVE);
decode_fifo(flush_and_capture());
const auto* next = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(next, nullptr);
ASSERT_EQ(next->pipeline, pipeline);
EXPECT_NE(next->compositeSourcePipeline, 0u);
EXPECT_NE(next->stereoCompositeSourcePipeline, 0u);
EXPECT_NE(next->compositeSourcePipeline, next->pipeline);
EXPECT_NE(next->stereoCompositeSourcePipeline, next->stereoPipeline);
EXPECT_NE(next->compositeSourcePipeline, next->stereoCompositeSourcePipeline);
draw_quad_with_projection(GX_ORTHOGRAPHIC);
decode_fifo(flush_and_capture());
const auto* flat = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(flat, nullptr);
ASSERT_EQ(flat->pipeline, pipeline);
EXPECT_EQ(flat->compositeSourcePipeline, 0u);
EXPECT_EQ(flat->stereoCompositeSourcePipeline, 0u);
}