backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
878
test/integration/emulator_object_preview_test.cc
Normal file
878
test/integration/emulator_object_preview_test.cc
Normal file
@@ -0,0 +1,878 @@
|
||||
// Integration tests for DungeonObjectEmulatorPreview
|
||||
// Tests the SNES emulator-based object rendering pipeline
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "app/emu/render/save_state_manager.h"
|
||||
|
||||
#include "app/emu/snes.h"
|
||||
#include "rom/rom.h"
|
||||
#include "test_utils.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
// Convert 8BPP linear tile data to 4BPP SNES planar format
|
||||
// This is a copy of the function in dungeon_object_emulator_preview.cc for testing
|
||||
std::vector<uint8_t> ConvertLinear8bppToPlanar4bpp(
|
||||
const std::vector<uint8_t>& linear_data) {
|
||||
size_t num_tiles = linear_data.size() / 64; // 64 bytes per 8x8 tile
|
||||
std::vector<uint8_t> planar_data(num_tiles * 32); // 32 bytes per tile
|
||||
|
||||
for (size_t tile = 0; tile < num_tiles; ++tile) {
|
||||
const uint8_t* src = linear_data.data() + tile * 64;
|
||||
uint8_t* dst = planar_data.data() + tile * 32;
|
||||
|
||||
for (int row = 0; row < 8; ++row) {
|
||||
uint8_t bp0 = 0, bp1 = 0, bp2 = 0, bp3 = 0;
|
||||
|
||||
for (int col = 0; col < 8; ++col) {
|
||||
uint8_t pixel = src[row * 8 + col] & 0x0F; // Low 4 bits only
|
||||
int bit = 7 - col; // MSB first
|
||||
|
||||
bp0 |= ((pixel >> 0) & 1) << bit;
|
||||
bp1 |= ((pixel >> 1) & 1) << bit;
|
||||
bp2 |= ((pixel >> 2) & 1) << bit;
|
||||
bp3 |= ((pixel >> 3) & 1) << bit;
|
||||
}
|
||||
|
||||
// SNES 4BPP interleaving: bp0,bp1 for rows 0-7 first, then bp2,bp3
|
||||
dst[row * 2] = bp0;
|
||||
dst[row * 2 + 1] = bp1;
|
||||
dst[16 + row * 2] = bp2;
|
||||
dst[16 + row * 2 + 1] = bp3;
|
||||
}
|
||||
}
|
||||
|
||||
return planar_data;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// =============================================================================
|
||||
// Unit Tests for 8BPP to 4BPP Conversion
|
||||
// =============================================================================
|
||||
|
||||
class BppConversionTest : public ::testing::Test {
|
||||
protected:
|
||||
// Create a simple test tile with known pixel values
|
||||
std::vector<uint8_t> CreateTestTile(uint8_t fill_value) {
|
||||
std::vector<uint8_t> tile(64, fill_value);
|
||||
return tile;
|
||||
}
|
||||
|
||||
// Create a gradient tile for testing bit extraction
|
||||
std::vector<uint8_t> CreateGradientTile() {
|
||||
std::vector<uint8_t> tile(64);
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
tile[i] = i % 16; // Values 0-15 repeating
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(BppConversionTest, EmptyInputProducesEmptyOutput) {
|
||||
std::vector<uint8_t> empty;
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(empty);
|
||||
EXPECT_TRUE(result.empty());
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, SingleTileProducesCorrectSize) {
|
||||
auto tile = CreateTestTile(0);
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(tile);
|
||||
|
||||
// 64 bytes input (8BPP) -> 32 bytes output (4BPP)
|
||||
EXPECT_EQ(result.size(), 32u);
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, MultipleTilesProduceCorrectSize) {
|
||||
// Create 4 tiles (256 bytes)
|
||||
std::vector<uint8_t> tiles(256, 0);
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(tiles);
|
||||
|
||||
// 256 bytes input -> 128 bytes output
|
||||
EXPECT_EQ(result.size(), 128u);
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, AllZerosProducesAllZeros) {
|
||||
auto tile = CreateTestTile(0);
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(tile);
|
||||
|
||||
for (uint8_t byte : result) {
|
||||
EXPECT_EQ(byte, 0u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, AllOnesProducesCorrectPattern) {
|
||||
// Pixel value 1 = bit 0 set
|
||||
auto tile = CreateTestTile(1);
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(tile);
|
||||
|
||||
// With all pixels = 1, bitplane 0 should be all 0xFF
|
||||
// Bitplanes 1, 2, 3 should be all 0x00
|
||||
for (int row = 0; row < 8; ++row) {
|
||||
EXPECT_EQ(result[row * 2], 0xFF) << "Row " << row << " bp0";
|
||||
EXPECT_EQ(result[row * 2 + 1], 0x00) << "Row " << row << " bp1";
|
||||
EXPECT_EQ(result[16 + row * 2], 0x00) << "Row " << row << " bp2";
|
||||
EXPECT_EQ(result[16 + row * 2 + 1], 0x00) << "Row " << row << " bp3";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, Value15ProducesAllBitsSet) {
|
||||
// Pixel value 15 (0xF) = all 4 bits set
|
||||
auto tile = CreateTestTile(15);
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(tile);
|
||||
|
||||
// All bitplanes should be 0xFF
|
||||
for (int row = 0; row < 8; ++row) {
|
||||
EXPECT_EQ(result[row * 2], 0xFF) << "Row " << row << " bp0";
|
||||
EXPECT_EQ(result[row * 2 + 1], 0xFF) << "Row " << row << " bp1";
|
||||
EXPECT_EQ(result[16 + row * 2], 0xFF) << "Row " << row << " bp2";
|
||||
EXPECT_EQ(result[16 + row * 2 + 1], 0xFF) << "Row " << row << " bp3";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, HighBitsAreIgnored) {
|
||||
// Pixel value 0xFF should be treated as 0x0F (low 4 bits only)
|
||||
auto tile_ff = CreateTestTile(0xFF);
|
||||
auto tile_0f = CreateTestTile(0x0F);
|
||||
|
||||
auto result_ff = ConvertLinear8bppToPlanar4bpp(tile_ff);
|
||||
auto result_0f = ConvertLinear8bppToPlanar4bpp(tile_0f);
|
||||
|
||||
EXPECT_EQ(result_ff, result_0f);
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, SinglePixelBitplaneExtraction) {
|
||||
// Create a tile with just the first pixel set to value 5 (0101 binary)
|
||||
std::vector<uint8_t> tile(64, 0);
|
||||
tile[0] = 5; // First pixel = 0101
|
||||
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(tile);
|
||||
|
||||
// First pixel is at MSB position (bit 7) of first row
|
||||
// Value 5 = 0101 = bp0=1, bp1=0, bp2=1, bp3=0
|
||||
EXPECT_EQ(result[0] & 0x80, 0x80) << "bp0 bit 7 should be set";
|
||||
EXPECT_EQ(result[1] & 0x80, 0x00) << "bp1 bit 7 should be clear";
|
||||
EXPECT_EQ(result[16] & 0x80, 0x80) << "bp2 bit 7 should be set";
|
||||
EXPECT_EQ(result[17] & 0x80, 0x00) << "bp3 bit 7 should be clear";
|
||||
}
|
||||
|
||||
TEST_F(BppConversionTest, GradientTileConversion) {
|
||||
auto tile = CreateGradientTile();
|
||||
auto result = ConvertLinear8bppToPlanar4bpp(tile);
|
||||
|
||||
// Verify size
|
||||
EXPECT_EQ(result.size(), 32u);
|
||||
|
||||
// The gradient should produce non-trivial bitplane data
|
||||
bool has_nonzero_bp0 = false;
|
||||
bool has_nonzero_bp1 = false;
|
||||
bool has_nonzero_bp2 = false;
|
||||
bool has_nonzero_bp3 = false;
|
||||
|
||||
for (int row = 0; row < 8; ++row) {
|
||||
if (result[row * 2] != 0) has_nonzero_bp0 = true;
|
||||
if (result[row * 2 + 1] != 0) has_nonzero_bp1 = true;
|
||||
if (result[16 + row * 2] != 0) has_nonzero_bp2 = true;
|
||||
if (result[16 + row * 2 + 1] != 0) has_nonzero_bp3 = true;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(has_nonzero_bp0) << "Gradient should have non-zero bp0";
|
||||
EXPECT_TRUE(has_nonzero_bp1) << "Gradient should have non-zero bp1";
|
||||
EXPECT_TRUE(has_nonzero_bp2) << "Gradient should have non-zero bp2";
|
||||
EXPECT_TRUE(has_nonzero_bp3) << "Gradient should have non-zero bp3";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Integration Tests with SNES Emulator (requires ROM)
|
||||
// =============================================================================
|
||||
|
||||
class EmulatorObjectPreviewTest : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
|
||||
// Initialize SNES emulator with ROM
|
||||
snes_ = std::make_unique<emu::Snes>();
|
||||
snes_->Init(rom()->vector());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
snes_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
// Setup CPU state for object handler execution
|
||||
void SetupCpuForHandler(uint16_t handler_offset) {
|
||||
auto& cpu = snes_->cpu();
|
||||
|
||||
// Reset and configure
|
||||
snes_->Reset(true);
|
||||
|
||||
cpu.PB = 0x01; // Program bank
|
||||
cpu.DB = 0x7E; // Data bank (WRAM)
|
||||
cpu.D = 0x0000; // Direct page
|
||||
cpu.SetSP(0x01FF); // Stack pointer
|
||||
cpu.status = 0x30; // 8-bit A/X/Y mode
|
||||
|
||||
// Set PC to handler
|
||||
cpu.PC = handler_offset;
|
||||
}
|
||||
|
||||
// Lookup object handler from ROM
|
||||
uint16_t LookupObjectHandler(int object_id) {
|
||||
auto rom_data = rom()->data();
|
||||
uint32_t table_addr = 0;
|
||||
|
||||
if (object_id < 0x100) {
|
||||
table_addr = 0x018200 + (object_id * 2);
|
||||
} else if (object_id < 0x200) {
|
||||
table_addr = 0x018470 + ((object_id - 0x100) * 2);
|
||||
} else {
|
||||
table_addr = 0x0185F0 + ((object_id - 0x200) * 2);
|
||||
}
|
||||
|
||||
if (table_addr < rom()->size() - 1) {
|
||||
return rom_data[table_addr] | (rom_data[table_addr + 1] << 8);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::unique_ptr<emu::Snes> snes_;
|
||||
};
|
||||
|
||||
TEST_F(EmulatorObjectPreviewTest, SnesInitializesCorrectly) {
|
||||
ASSERT_NE(snes_, nullptr);
|
||||
|
||||
// Verify CPU is accessible
|
||||
auto& cpu = snes_->cpu();
|
||||
EXPECT_EQ(cpu.PB, 0x00); // After init, PB should be 0
|
||||
}
|
||||
|
||||
TEST_F(EmulatorObjectPreviewTest, ObjectHandlerTableLookup) {
|
||||
// Test that handler table addresses are valid
|
||||
|
||||
// Object 0x00 should have a handler
|
||||
uint16_t handler_0 = LookupObjectHandler(0x00);
|
||||
EXPECT_NE(handler_0, 0x0000) << "Object 0x00 should have a handler";
|
||||
|
||||
// Object 0x100 (Type 2)
|
||||
uint16_t handler_100 = LookupObjectHandler(0x100);
|
||||
// May or may not have handler, just verify lookup doesn't crash
|
||||
|
||||
// Object 0x200 (Type 3)
|
||||
uint16_t handler_200 = LookupObjectHandler(0x200);
|
||||
// May or may not have handler
|
||||
|
||||
printf("[TEST] Handler 0x00 = $%04X\n", handler_0);
|
||||
printf("[TEST] Handler 0x100 = $%04X\n", handler_100);
|
||||
printf("[TEST] Handler 0x200 = $%04X\n", handler_200);
|
||||
}
|
||||
|
||||
// DISABLED: CPU execution from manually-set PC doesn't work as expected.
|
||||
// After Init(), the emulator's internal state causes RunOpcode() to
|
||||
// jump to the reset vector ($8000) instead of executing from the set PC.
|
||||
// This documents a limitation in using the emulator for isolated code execution.
|
||||
TEST_F(EmulatorObjectPreviewTest, DISABLED_CpuCanExecuteInstructions) {
|
||||
auto& cpu = snes_->cpu();
|
||||
|
||||
// Write a NOP (EA) instruction to WRAM at a known location
|
||||
snes_->Write(0x7E1000, 0xEA); // NOP
|
||||
snes_->Write(0x7E1001, 0xEA); // NOP
|
||||
snes_->Write(0x7E1002, 0xEA); // NOP
|
||||
|
||||
// Verify the writes worked
|
||||
EXPECT_EQ(snes_->Read(0x7E1000), 0xEA) << "WRAM write should persist";
|
||||
|
||||
// Setup CPU to execute from WRAM
|
||||
cpu.PB = 0x7E;
|
||||
cpu.PC = 0x1000;
|
||||
cpu.DB = 0x7E;
|
||||
cpu.SetSP(0x01FF);
|
||||
cpu.status = 0x30;
|
||||
|
||||
uint16_t initial_pc = cpu.PC;
|
||||
|
||||
// Execute one NOP instruction
|
||||
cpu.RunOpcode();
|
||||
|
||||
// NOP is a 1-byte instruction, so PC should advance by 1
|
||||
EXPECT_EQ(cpu.PC, initial_pc + 1)
|
||||
<< "PC should advance by 1 after NOP (was " << initial_pc
|
||||
<< ", now " << cpu.PC << ")";
|
||||
}
|
||||
|
||||
TEST_F(EmulatorObjectPreviewTest, WramReadWrite) {
|
||||
// Test WRAM access
|
||||
const uint32_t test_addr = 0x7E2000;
|
||||
|
||||
// Write test pattern
|
||||
snes_->Write(test_addr, 0xAB);
|
||||
snes_->Write(test_addr + 1, 0xCD);
|
||||
|
||||
// Read back
|
||||
uint8_t lo = snes_->Read(test_addr);
|
||||
uint8_t hi = snes_->Read(test_addr + 1);
|
||||
|
||||
EXPECT_EQ(lo, 0xAB);
|
||||
EXPECT_EQ(hi, 0xCD);
|
||||
|
||||
uint16_t word = lo | (hi << 8);
|
||||
EXPECT_EQ(word, 0xCDAB);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorObjectPreviewTest, VramCanBeWritten) {
|
||||
auto& ppu = snes_->ppu();
|
||||
|
||||
// Write test data to VRAM
|
||||
ppu.vram[0] = 0x1234;
|
||||
ppu.vram[1] = 0x5678;
|
||||
|
||||
EXPECT_EQ(ppu.vram[0], 0x1234);
|
||||
EXPECT_EQ(ppu.vram[1], 0x5678);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorObjectPreviewTest, CgramCanBeWritten) {
|
||||
auto& ppu = snes_->ppu();
|
||||
|
||||
// Write test palette data to CGRAM
|
||||
ppu.cgram[0] = 0x0000; // Black
|
||||
ppu.cgram[1] = 0x7FFF; // White
|
||||
ppu.cgram[2] = 0x001F; // Red
|
||||
|
||||
EXPECT_EQ(ppu.cgram[0], 0x0000);
|
||||
EXPECT_EQ(ppu.cgram[1], 0x7FFF);
|
||||
EXPECT_EQ(ppu.cgram[2], 0x001F);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorObjectPreviewTest, RoomGraphicsCanBeLoaded) {
|
||||
// Load room 0
|
||||
zelda3::Room room = zelda3::LoadRoomFromRom(rom(), 0);
|
||||
|
||||
// Load graphics
|
||||
room.LoadRoomGraphics(room.blockset);
|
||||
room.CopyRoomGraphicsToBuffer();
|
||||
|
||||
const auto& gfx_buffer = room.get_gfx_buffer();
|
||||
|
||||
// Verify buffer is populated
|
||||
EXPECT_EQ(gfx_buffer.size(), 65536u) << "Graphics buffer should be 64KB";
|
||||
|
||||
// Count non-zero bytes
|
||||
int nonzero_count = 0;
|
||||
for (uint8_t byte : gfx_buffer) {
|
||||
if (byte != 0) nonzero_count++;
|
||||
}
|
||||
|
||||
EXPECT_GT(nonzero_count, 0) << "Graphics buffer should have non-zero data";
|
||||
printf("[TEST] Graphics buffer: %d non-zero bytes out of 65536\n", nonzero_count);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorObjectPreviewTest, GraphicsConversionProducesValidData) {
|
||||
// Load room graphics
|
||||
zelda3::Room room = zelda3::LoadRoomFromRom(rom(), 0);
|
||||
room.LoadRoomGraphics(room.blockset);
|
||||
room.CopyRoomGraphicsToBuffer();
|
||||
|
||||
const auto& gfx_buffer = room.get_gfx_buffer();
|
||||
|
||||
// Convert to 4BPP planar
|
||||
std::vector<uint8_t> linear_data(gfx_buffer.begin(), gfx_buffer.end());
|
||||
auto planar_data = ConvertLinear8bppToPlanar4bpp(linear_data);
|
||||
|
||||
// Verify conversion
|
||||
EXPECT_EQ(planar_data.size(), 32768u) << "4BPP should be half the size of 8BPP";
|
||||
|
||||
// Count non-zero bytes in converted data
|
||||
int nonzero_count = 0;
|
||||
for (uint8_t byte : planar_data) {
|
||||
if (byte != 0) nonzero_count++;
|
||||
}
|
||||
|
||||
EXPECT_GT(nonzero_count, 0) << "Converted data should have non-zero bytes";
|
||||
printf("[TEST] Planar data: %d non-zero bytes out of 32768\n", nonzero_count);
|
||||
}
|
||||
|
||||
// Test documenting current limitation - handlers require full game state
|
||||
// Test documenting current limitation - handlers require full game state
|
||||
// Enabled now that we can inject save states!
|
||||
TEST_F(EmulatorObjectPreviewTest, HandlerExecutionRequiresGameState) {
|
||||
// Initialize SaveStateManager
|
||||
auto state_manager = std::make_unique<emu::render::SaveStateManager>(snes_.get(), rom());
|
||||
state_manager->SetStateDirectory("/tmp/yaze_test_states");
|
||||
|
||||
// Load the Sanctuary state (room 0x0012) which we generated in SaveStateGenerationTest
|
||||
// This provides the necessary game state (tables, pointers, etc.)
|
||||
printf("[TEST] Loading state for room 0x0012...\n");
|
||||
auto status = state_manager->LoadState(emu::render::StateType::kRoomLoaded, 0x0012);
|
||||
if (!status.ok()) {
|
||||
printf("[TEST] Failed to load state: %s. Skipping test.\n", status.message().data());
|
||||
return;
|
||||
}
|
||||
printf("[TEST] State loaded successfully.\n");
|
||||
|
||||
uint16_t handler = LookupObjectHandler(0x00);
|
||||
ASSERT_NE(handler, 0x0000) << "Object 0x00 should have a handler";
|
||||
printf("[TEST] Handler address: $%04X\n", handler);
|
||||
|
||||
// We don't need full SetupCpuForHandler because LoadState sets up the CPU
|
||||
// But we do need to set PC to the handler and setup the stack for return
|
||||
auto& cpu = snes_->cpu();
|
||||
|
||||
// Keep the loaded state but override PC to our handler
|
||||
cpu.PC = handler;
|
||||
|
||||
// Setup return address at $01:8000 (RTL)
|
||||
// Note: We must be careful not to corrupt the stack from the save state
|
||||
// But for this test, we just want to see if it runs without crashing and writes to WRAM
|
||||
|
||||
// Write RTL at return address
|
||||
printf("[TEST] Writing RTL to $01:8000...\n");
|
||||
snes_->Write(0x018000, 0x6B);
|
||||
|
||||
// Push return address (0x018000)
|
||||
printf("[TEST] Pushing return address to SP=$%04X...\n", cpu.SP());
|
||||
uint16_t sp = cpu.SP();
|
||||
// Stack is always in Bank 0 ($00:01xx)
|
||||
snes_->Write(0x000000 | sp--, 0x01); // Bank
|
||||
snes_->Write(0x000000 | sp--, 0x80); // High
|
||||
snes_->Write(0x000000 | sp--, 0x00); // Low
|
||||
cpu.SetSP(sp);
|
||||
|
||||
// Setup X/Y for the handler (data offset and tilemap pos)
|
||||
// Object 0x00 is usually simple, but let's give it valid params
|
||||
cpu.X = 0x0000; // Data offset (dummy)
|
||||
cpu.Y = 0x0000; // Tilemap position (top-left)
|
||||
|
||||
printf("[TEST] Starting execution at $%02X:%04X...\n", cpu.PB, cpu.PC);
|
||||
|
||||
// Execute some opcodes
|
||||
int opcodes = 0;
|
||||
int max_opcodes = 5000; // Increased for safety
|
||||
while (opcodes < max_opcodes) {
|
||||
if (cpu.PB == 0x01 && cpu.PC == 0x8000) {
|
||||
printf("[TEST] Handler returned successfully at opcode %d\n", opcodes);
|
||||
break;
|
||||
}
|
||||
|
||||
// Trace execution
|
||||
uint32_t addr = (cpu.PB << 16) | cpu.PC;
|
||||
uint8_t opcode = snes_->Read(addr);
|
||||
printf("[%4d] $%02X:%04X: %02X (A=$%04X X=$%04X Y=$%04X SP=$%04X)\n",
|
||||
opcodes, cpu.PB, cpu.PC, opcode, cpu.A, cpu.X, cpu.Y, cpu.SP());
|
||||
|
||||
cpu.RunOpcode();
|
||||
opcodes++;
|
||||
}
|
||||
|
||||
printf("[TEST] Executed %d opcodes, final PC=$%02X:%04X\n",
|
||||
opcodes, cpu.PB, cpu.PC);
|
||||
|
||||
// Check if anything was written to WRAM tilemap
|
||||
// The handler for object 0x00 should write something
|
||||
bool has_tilemap_data = false;
|
||||
for (uint32_t i = 0; i < 0x100; i++) {
|
||||
if (snes_->Read(0x7E2000 + i) != 0) {
|
||||
has_tilemap_data = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(has_tilemap_data)
|
||||
<< "Handler should write to tilemap (now passing with save state!)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Emulator State Injection Tests
|
||||
// Tests for proper SNES state setup for isolated code execution
|
||||
// =============================================================================
|
||||
|
||||
class EmulatorStateInjectionTest : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
snes_ = std::make_unique<emu::Snes>();
|
||||
snes_->Init(rom()->vector());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
snes_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
// Convert SNES LoROM address to PC offset
|
||||
static uint32_t SnesToPc(uint32_t snes_addr) {
|
||||
uint8_t bank = (snes_addr >> 16) & 0xFF;
|
||||
uint16_t addr = snes_addr & 0xFFFF;
|
||||
if (addr >= 0x8000) {
|
||||
return (bank & 0x7F) * 0x8000 + (addr - 0x8000);
|
||||
}
|
||||
return snes_addr;
|
||||
}
|
||||
|
||||
std::unique_ptr<emu::Snes> snes_;
|
||||
};
|
||||
|
||||
// Test LoROM address conversion
|
||||
TEST_F(EmulatorStateInjectionTest, LoRomAddressConversion) {
|
||||
// Bank $01 handler tables
|
||||
EXPECT_EQ(SnesToPc(0x018000), 0x8000u) << "$01:8000 -> PC $8000";
|
||||
EXPECT_EQ(SnesToPc(0x018200), 0x8200u) << "$01:8200 -> PC $8200";
|
||||
EXPECT_EQ(SnesToPc(0x0186F8), 0x86F8u) << "$01:86F8 -> PC $86F8";
|
||||
|
||||
// Bank $00
|
||||
EXPECT_EQ(SnesToPc(0x008000), 0x0000u) << "$00:8000 -> PC $0000";
|
||||
EXPECT_EQ(SnesToPc(0x009B52), 0x1B52u) << "$00:9B52 -> PC $1B52";
|
||||
|
||||
// Bank $0D (palettes)
|
||||
EXPECT_EQ(SnesToPc(0x0DD308), 0x6D308u) << "$0D:D308 -> PC $6D308";
|
||||
EXPECT_EQ(SnesToPc(0x0DD734), 0x6D734u) << "$0D:D734 -> PC $6D734";
|
||||
|
||||
// Bank $02
|
||||
EXPECT_EQ(SnesToPc(0x028000), 0x10000u) << "$02:8000 -> PC $10000";
|
||||
}
|
||||
|
||||
// Test APU out_ports access
|
||||
TEST_F(EmulatorStateInjectionTest, ApuOutPortsAccess) {
|
||||
auto& apu = snes_->apu();
|
||||
|
||||
// Set mock values
|
||||
apu.out_ports_[0] = 0xAA;
|
||||
apu.out_ports_[1] = 0xBB;
|
||||
apu.out_ports_[2] = 0xCC;
|
||||
apu.out_ports_[3] = 0xDD;
|
||||
|
||||
// Verify values are set
|
||||
EXPECT_EQ(apu.out_ports_[0], 0xAA);
|
||||
EXPECT_EQ(apu.out_ports_[1], 0xBB);
|
||||
EXPECT_EQ(apu.out_ports_[2], 0xCC);
|
||||
EXPECT_EQ(apu.out_ports_[3], 0xDD);
|
||||
}
|
||||
|
||||
// Test that APU out_ports values can be read via CPU
|
||||
TEST_F(EmulatorStateInjectionTest, ApuOutPortsReadByCpu) {
|
||||
auto& apu = snes_->apu();
|
||||
|
||||
// Set mock values
|
||||
apu.out_ports_[0] = 0xAA;
|
||||
apu.out_ports_[1] = 0xBB;
|
||||
|
||||
// Read via SNES Read() - this goes through the memory mapper
|
||||
// NOTE: CatchUpApu() is called which may overwrite our values!
|
||||
// This test documents the current behavior
|
||||
uint8_t val0 = snes_->Read(0x002140);
|
||||
uint8_t val1 = snes_->Read(0x002141);
|
||||
|
||||
printf("[TEST] APU read: $2140=$%02X (expected $AA), $2141=$%02X (expected $BB)\n",
|
||||
val0, val1);
|
||||
|
||||
// These may NOT equal $AA/$BB due to CatchUpApu() running the APU
|
||||
// Document current behavior rather than asserting
|
||||
if (val0 != 0xAA || val1 != 0xBB) {
|
||||
printf("[TEST] WARNING: CatchUpApu() may have overwritten mock values\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Test handler table reading with correct LoROM conversion
|
||||
TEST_F(EmulatorStateInjectionTest, HandlerTableReadWithLoRom) {
|
||||
auto rom_data = rom()->data();
|
||||
|
||||
// Read object 0x00 handler from the correct PC offset
|
||||
uint32_t handler_table_snes = 0x018200; // Type 1 handler table
|
||||
uint32_t handler_table_pc = SnesToPc(handler_table_snes);
|
||||
|
||||
EXPECT_EQ(handler_table_pc, 0x8200u);
|
||||
|
||||
if (handler_table_pc + 1 < rom()->size()) {
|
||||
uint16_t handler = rom_data[handler_table_pc] |
|
||||
(rom_data[handler_table_pc + 1] << 8);
|
||||
printf("[TEST] Object 0x00 handler (from PC $%04X): $%04X\n",
|
||||
handler_table_pc, handler);
|
||||
|
||||
// Handler should be in the $8xxx-$9xxx range (bank $01 code)
|
||||
EXPECT_GE(handler, 0x8000u) << "Handler should be >= $8000";
|
||||
EXPECT_LT(handler, 0x10000u) << "Handler should be < $10000";
|
||||
} else {
|
||||
FAIL() << "Handler table address out of ROM bounds";
|
||||
}
|
||||
}
|
||||
|
||||
// Test data offset table reading
|
||||
TEST_F(EmulatorStateInjectionTest, DataOffsetTableReadWithLoRom) {
|
||||
auto rom_data = rom()->data();
|
||||
|
||||
// Read object 0x00 data offset
|
||||
uint32_t data_table_snes = 0x018000; // Type 1 data table
|
||||
uint32_t data_table_pc = SnesToPc(data_table_snes);
|
||||
|
||||
EXPECT_EQ(data_table_pc, 0x8000u);
|
||||
|
||||
if (data_table_pc + 1 < rom()->size()) {
|
||||
uint16_t data_offset = rom_data[data_table_pc] |
|
||||
(rom_data[data_table_pc + 1] << 8);
|
||||
printf("[TEST] Object 0x00 data offset (from PC $%04X): $%04X\n",
|
||||
data_table_pc, data_offset);
|
||||
|
||||
// Data offset is into RoomDrawObjectData, should be reasonable
|
||||
EXPECT_LT(data_offset, 0x8000u) << "Data offset should be < $8000";
|
||||
} else {
|
||||
FAIL() << "Data table address out of ROM bounds";
|
||||
}
|
||||
}
|
||||
|
||||
// Test tilemap pointer setup
|
||||
TEST_F(EmulatorStateInjectionTest, TilemapPointerSetup) {
|
||||
// Setup tilemap pointers in zero page
|
||||
constexpr uint32_t kBG1TilemapBase = 0x7E2000;
|
||||
constexpr uint32_t kRowStride = 0x80;
|
||||
constexpr uint8_t kPointerAddrs[] = {0xBF, 0xC2, 0xC5, 0xC8, 0xCB,
|
||||
0xCE, 0xD1, 0xD4, 0xD7, 0xDA, 0xDD};
|
||||
|
||||
for (int i = 0; i < 11; ++i) {
|
||||
uint32_t wram_addr = kBG1TilemapBase + (i * kRowStride);
|
||||
uint8_t lo = wram_addr & 0xFF;
|
||||
uint8_t mid = (wram_addr >> 8) & 0xFF;
|
||||
uint8_t hi = (wram_addr >> 16) & 0xFF;
|
||||
|
||||
uint8_t zp_addr = kPointerAddrs[i];
|
||||
snes_->Write(0x7E0000 | zp_addr, lo);
|
||||
snes_->Write(0x7E0000 | (zp_addr + 1), mid);
|
||||
snes_->Write(0x7E0000 | (zp_addr + 2), hi);
|
||||
}
|
||||
|
||||
// Verify pointers were written correctly
|
||||
for (int i = 0; i < 11; ++i) {
|
||||
uint8_t zp_addr = kPointerAddrs[i];
|
||||
uint8_t lo = snes_->Read(0x7E0000 | zp_addr);
|
||||
uint8_t mid = snes_->Read(0x7E0000 | (zp_addr + 1));
|
||||
uint8_t hi = snes_->Read(0x7E0000 | (zp_addr + 2));
|
||||
|
||||
uint32_t ptr = lo | (mid << 8) | (hi << 16);
|
||||
uint32_t expected = kBG1TilemapBase + (i * kRowStride);
|
||||
|
||||
EXPECT_EQ(ptr, expected) << "Tilemap ptr $" << std::hex << (int)zp_addr
|
||||
<< " should be $" << expected;
|
||||
}
|
||||
}
|
||||
|
||||
// Test sprite auxiliary palette loading
|
||||
TEST_F(EmulatorStateInjectionTest, SpriteAuxPaletteLoading) {
|
||||
auto rom_data = rom()->data();
|
||||
|
||||
// Sprite aux palettes at $0D:D308
|
||||
uint32_t palette_snes = 0x0DD308;
|
||||
uint32_t palette_pc = SnesToPc(palette_snes);
|
||||
|
||||
EXPECT_EQ(palette_pc, 0x6D308u);
|
||||
|
||||
if (palette_pc + 60 < rom()->size()) {
|
||||
// Read first few palette colors
|
||||
std::vector<uint16_t> colors;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
uint16_t color = rom_data[palette_pc + i * 2] |
|
||||
(rom_data[palette_pc + i * 2 + 1] << 8);
|
||||
colors.push_back(color);
|
||||
}
|
||||
|
||||
printf("[TEST] First 10 sprite aux palette colors:\n");
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
printf(" [%d] $%04X\n", i, colors[i]);
|
||||
}
|
||||
|
||||
// At least some colors should be non-zero
|
||||
int nonzero = 0;
|
||||
for (uint16_t c : colors) {
|
||||
if (c != 0) nonzero++;
|
||||
}
|
||||
EXPECT_GT(nonzero, 0) << "Sprite aux palette should have some non-zero colors";
|
||||
} else {
|
||||
printf("[TEST] WARNING: Sprite aux palette address $%X out of bounds\n", palette_pc);
|
||||
}
|
||||
}
|
||||
|
||||
// Test CPU state setup for handler execution
|
||||
TEST_F(EmulatorStateInjectionTest, CpuStateSetup) {
|
||||
snes_->Reset(true);
|
||||
auto& cpu = snes_->cpu();
|
||||
|
||||
// Setup CPU state as we do in the preview
|
||||
cpu.PB = 0x01;
|
||||
cpu.DB = 0x7E;
|
||||
cpu.D = 0x0000;
|
||||
cpu.SetSP(0x01FF);
|
||||
cpu.status = 0x30;
|
||||
cpu.E = 0;
|
||||
cpu.X = 0x03D8; // Sample data offset
|
||||
cpu.Y = 0x0820; // Sample tilemap position
|
||||
cpu.PC = 0x8B89; // Sample handler address
|
||||
|
||||
EXPECT_EQ(cpu.PB, 0x01);
|
||||
EXPECT_EQ(cpu.DB, 0x7E);
|
||||
EXPECT_EQ(cpu.D, 0x0000);
|
||||
EXPECT_EQ(cpu.SP(), 0x01FF);
|
||||
EXPECT_EQ(cpu.X, 0x03D8);
|
||||
EXPECT_EQ(cpu.Y, 0x0820);
|
||||
EXPECT_EQ(cpu.PC, 0x8B89);
|
||||
}
|
||||
|
||||
// Test STP trap setup
|
||||
// NOTE: Writing to bank $01 ROM space doesn't persist - ROM is read-only.
|
||||
// This test verifies we can write STP to WRAM instead for trap detection.
|
||||
TEST_F(EmulatorStateInjectionTest, StpTrapSetup) {
|
||||
// $01:FF00 is ROM space - writes don't persist
|
||||
// Instead, use a WRAM address for trap setup
|
||||
const uint32_t wram_trap_addr = 0x7EFF00; // High WRAM
|
||||
snes_->Write(wram_trap_addr, 0xDB); // STP opcode
|
||||
|
||||
// Verify write to WRAM succeeds
|
||||
uint8_t opcode = snes_->Read(wram_trap_addr);
|
||||
EXPECT_EQ(opcode, 0xDB) << "STP opcode should be written to WRAM trap address";
|
||||
|
||||
// Document the ROM write limitation
|
||||
const uint32_t rom_trap_addr = 0x01FF00;
|
||||
snes_->Write(rom_trap_addr, 0xDB);
|
||||
uint8_t rom_opcode = snes_->Read(rom_trap_addr);
|
||||
// This will NOT equal 0xDB because ROM is read-only
|
||||
// The actual value depends on what's in the ROM at that address
|
||||
EXPECT_NE(rom_opcode, 0xDB)
|
||||
<< "ROM space writes should NOT persist (ROM is read-only)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Handler Execution Tracing Tests
|
||||
// These tests help diagnose why handlers fail to execute properly
|
||||
// =============================================================================
|
||||
|
||||
class HandlerExecutionTraceTest : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
snes_ = std::make_unique<emu::Snes>();
|
||||
snes_->Init(rom()->vector());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
snes_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
// Convert SNES LoROM address to PC offset
|
||||
static uint32_t SnesToPc(uint32_t snes_addr) {
|
||||
uint8_t bank = (snes_addr >> 16) & 0xFF;
|
||||
uint16_t addr = snes_addr & 0xFFFF;
|
||||
if (addr >= 0x8000) {
|
||||
return (bank & 0x7F) * 0x8000 + (addr - 0x8000);
|
||||
}
|
||||
return snes_addr;
|
||||
}
|
||||
|
||||
// Trace first N opcodes of execution
|
||||
void TraceExecution(int num_opcodes) {
|
||||
auto& cpu = snes_->cpu();
|
||||
|
||||
printf("\n[TRACE] Starting execution trace from $%02X:%04X\n", cpu.PB, cpu.PC);
|
||||
printf(" X=$%04X Y=$%04X A=$%04X SP=$%04X\n",
|
||||
cpu.X, cpu.Y, cpu.A, cpu.SP());
|
||||
|
||||
for (int i = 0; i < num_opcodes; ++i) {
|
||||
uint32_t addr = (cpu.PB << 16) | cpu.PC;
|
||||
uint8_t opcode = snes_->Read(addr);
|
||||
|
||||
printf("[%4d] $%02X:%04X: %02X", i, cpu.PB, cpu.PC, opcode);
|
||||
|
||||
// Execute
|
||||
cpu.RunOpcode();
|
||||
|
||||
printf(" -> $%02X:%04X (A=$%04X X=$%04X Y=$%04X)\n",
|
||||
cpu.PB, cpu.PC, cpu.A, cpu.X, cpu.Y);
|
||||
|
||||
// Check for STP
|
||||
if (opcode == 0xDB) {
|
||||
printf("[TRACE] STP encountered, stopping\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we hit APU loop
|
||||
if (cpu.PB == 0x00 && cpu.PC == 0x8891) {
|
||||
printf("[TRACE] Hit APU loop at $00:8891\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<emu::Snes> snes_;
|
||||
};
|
||||
|
||||
// Trace first few instructions of object 0x00 handler
|
||||
TEST_F(HandlerExecutionTraceTest, TraceObject00Handler) {
|
||||
auto rom_data = rom()->data();
|
||||
|
||||
// Get handler address
|
||||
uint32_t handler_table_pc = SnesToPc(0x018200);
|
||||
uint16_t handler = rom_data[handler_table_pc] |
|
||||
(rom_data[handler_table_pc + 1] << 8);
|
||||
|
||||
printf("[TEST] Object 0x00 handler: $%04X\n", handler);
|
||||
|
||||
// Get data offset
|
||||
uint32_t data_table_pc = SnesToPc(0x018000);
|
||||
uint16_t data_offset = rom_data[data_table_pc] |
|
||||
(rom_data[data_table_pc + 1] << 8);
|
||||
|
||||
printf("[TEST] Object 0x00 data offset: $%04X\n", data_offset);
|
||||
|
||||
// Setup emulator state
|
||||
snes_->Reset(true);
|
||||
auto& cpu = snes_->cpu();
|
||||
auto& apu = snes_->apu();
|
||||
|
||||
// Setup APU mock
|
||||
apu.out_ports_[0] = 0xAA;
|
||||
apu.out_ports_[1] = 0xBB;
|
||||
|
||||
// Setup tilemap pointers
|
||||
constexpr uint32_t kBG1TilemapBase = 0x7E2000;
|
||||
constexpr uint8_t kPointerAddrs[] = {0xBF, 0xC2, 0xC5, 0xC8, 0xCB,
|
||||
0xCE, 0xD1, 0xD4, 0xD7, 0xDA, 0xDD};
|
||||
for (int i = 0; i < 11; ++i) {
|
||||
uint32_t wram_addr = kBG1TilemapBase + (i * 0x80);
|
||||
snes_->Write(0x7E0000 | kPointerAddrs[i], wram_addr & 0xFF);
|
||||
snes_->Write(0x7E0000 | (kPointerAddrs[i] + 1), (wram_addr >> 8) & 0xFF);
|
||||
snes_->Write(0x7E0000 | (kPointerAddrs[i] + 2), (wram_addr >> 16) & 0xFF);
|
||||
}
|
||||
|
||||
// Clear tilemap buffer
|
||||
for (uint32_t i = 0; i < 0x2000; i++) {
|
||||
snes_->Write(0x7E2000 + i, 0x00);
|
||||
}
|
||||
|
||||
// Setup CPU for handler
|
||||
cpu.PB = 0x01;
|
||||
cpu.DB = 0x7E;
|
||||
cpu.D = 0x0000;
|
||||
cpu.SetSP(0x01FF);
|
||||
cpu.status = 0x30;
|
||||
cpu.E = 0;
|
||||
cpu.X = data_offset;
|
||||
cpu.Y = 0x0820; // Tilemap position (16,16)
|
||||
cpu.PC = handler;
|
||||
|
||||
// Trace first 20 instructions
|
||||
TraceExecution(20);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
Reference in New Issue
Block a user