backend-infra-engineer: Release v0.3.9-hotfix7 snapshot

This commit is contained in:
scawful
2025-11-23 13:37:10 -05:00
parent c8289bffda
commit 2934c82b75
202 changed files with 34914 additions and 845 deletions

View File

@@ -0,0 +1,336 @@
/**
* @file disassembler_test.cc
* @brief Unit tests for the 65816 disassembler
*
* These tests validate the disassembler that enables AI-assisted
* assembly debugging for ROM hacking.
*/
#include "app/emu/debug/disassembler.h"
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace yaze {
namespace emu {
namespace debug {
namespace {
class Disassembler65816Test : public ::testing::Test {
protected:
// Helper to create a memory reader from a buffer
Disassembler65816::MemoryReader CreateMemoryReader(
const std::vector<uint8_t>& buffer, uint32_t base_address = 0) {
return [buffer, base_address](uint32_t addr) -> uint8_t {
uint32_t offset = addr - base_address;
if (offset < buffer.size()) {
return buffer[offset];
}
return 0;
};
}
Disassembler65816 disassembler_;
};
// --- Basic Instruction Tests ---
TEST_F(Disassembler65816Test, DisassembleNOP) {
std::vector<uint8_t> code = {0xEA}; // NOP
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.address, 0u);
EXPECT_EQ(result.opcode, 0xEA);
EXPECT_EQ(result.mnemonic, "NOP");
EXPECT_EQ(result.size, 1u);
}
TEST_F(Disassembler65816Test, DisassembleSEI) {
std::vector<uint8_t> code = {0x78}; // SEI
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.opcode, 0x78);
EXPECT_EQ(result.mnemonic, "SEI");
EXPECT_EQ(result.size, 1u);
}
// --- Immediate Addressing Tests ---
TEST_F(Disassembler65816Test, DisassembleLDAImmediate8Bit) {
std::vector<uint8_t> code = {0xA9, 0x42}; // LDA #$42
auto reader = CreateMemoryReader(code);
// m_flag = true means 8-bit accumulator
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "LDA");
EXPECT_EQ(result.size, 2u);
EXPECT_TRUE(result.operand_str.find("42") != std::string::npos);
}
TEST_F(Disassembler65816Test, DisassembleLDAImmediate16Bit) {
std::vector<uint8_t> code = {0xA9, 0x34, 0x12}; // LDA #$1234
auto reader = CreateMemoryReader(code);
// m_flag = false means 16-bit accumulator
auto result = disassembler_.Disassemble(0, reader, false, true);
EXPECT_EQ(result.mnemonic, "LDA");
EXPECT_EQ(result.size, 3u);
}
TEST_F(Disassembler65816Test, DisassembleLDXImmediate8Bit) {
std::vector<uint8_t> code = {0xA2, 0x10}; // LDX #$10
auto reader = CreateMemoryReader(code);
// x_flag = true means 8-bit index registers
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "LDX");
EXPECT_EQ(result.size, 2u);
}
TEST_F(Disassembler65816Test, DisassembleLDXImmediate16Bit) {
std::vector<uint8_t> code = {0xA2, 0x00, 0x80}; // LDX #$8000
auto reader = CreateMemoryReader(code);
// x_flag = false means 16-bit index registers
auto result = disassembler_.Disassemble(0, reader, true, false);
EXPECT_EQ(result.mnemonic, "LDX");
EXPECT_EQ(result.size, 3u);
}
// --- Absolute Addressing Tests ---
TEST_F(Disassembler65816Test, DisassembleLDAAbsolute) {
std::vector<uint8_t> code = {0xAD, 0x00, 0x80}; // LDA $8000
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "LDA");
EXPECT_EQ(result.size, 3u);
EXPECT_TRUE(result.operand_str.find("8000") != std::string::npos);
}
TEST_F(Disassembler65816Test, DisassembleSTAAbsoluteLong) {
std::vector<uint8_t> code = {0x8F, 0x00, 0x80, 0x7E}; // STA $7E8000
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "STA");
EXPECT_EQ(result.size, 4u);
EXPECT_TRUE(result.operand_str.find("7E8000") != std::string::npos);
}
// --- Jump/Call Instruction Tests ---
TEST_F(Disassembler65816Test, DisassembleJSR) {
std::vector<uint8_t> code = {0x20, 0x00, 0x80}; // JSR $8000
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "JSR");
EXPECT_EQ(result.size, 3u);
EXPECT_TRUE(result.is_call);
EXPECT_FALSE(result.is_return);
}
TEST_F(Disassembler65816Test, DisassembleJSL) {
std::vector<uint8_t> code = {0x22, 0x00, 0x80, 0x00}; // JSL $008000
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "JSL");
EXPECT_EQ(result.size, 4u);
EXPECT_TRUE(result.is_call);
}
TEST_F(Disassembler65816Test, DisassembleRTS) {
std::vector<uint8_t> code = {0x60}; // RTS
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "RTS");
EXPECT_EQ(result.size, 1u);
EXPECT_FALSE(result.is_call);
EXPECT_TRUE(result.is_return);
}
TEST_F(Disassembler65816Test, DisassembleRTL) {
std::vector<uint8_t> code = {0x6B}; // RTL
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "RTL");
EXPECT_EQ(result.size, 1u);
EXPECT_TRUE(result.is_return);
}
// --- Branch Instruction Tests ---
TEST_F(Disassembler65816Test, DisassembleBNE) {
std::vector<uint8_t> code = {0xD0, 0x10}; // BNE +16
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "BNE");
EXPECT_EQ(result.size, 2u);
EXPECT_TRUE(result.is_branch);
}
TEST_F(Disassembler65816Test, DisassembleBRA) {
std::vector<uint8_t> code = {0x80, 0xFE}; // BRA -2 (infinite loop)
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "BRA");
EXPECT_EQ(result.size, 2u);
EXPECT_TRUE(result.is_branch);
}
TEST_F(Disassembler65816Test, DisassembleJMPAbsolute) {
std::vector<uint8_t> code = {0x4C, 0x00, 0x80}; // JMP $8000
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "JMP");
EXPECT_EQ(result.size, 3u);
EXPECT_TRUE(result.is_branch);
}
// --- Range Disassembly Tests ---
TEST_F(Disassembler65816Test, DisassembleRange) {
// Small program:
// 8000: SEI ; Disable interrupts
// 8001: CLC ; Clear carry
// 8002: XCE ; Exchange carry and emulation
// 8003: LDA #$00 ; Load 0 into A
// 8005: STA $2100 ; Store to PPU brightness register
std::vector<uint8_t> code = {0x78, 0x18, 0xFB, 0xA9, 0x00, 0x8D, 0x00, 0x21};
auto reader = CreateMemoryReader(code, 0x008000);
auto result =
disassembler_.DisassembleRange(0x008000, 5, reader, true, true);
ASSERT_EQ(result.size(), 5u);
EXPECT_EQ(result[0].mnemonic, "SEI");
EXPECT_EQ(result[1].mnemonic, "CLC");
EXPECT_EQ(result[2].mnemonic, "XCE");
EXPECT_EQ(result[3].mnemonic, "LDA");
EXPECT_EQ(result[4].mnemonic, "STA");
}
// --- Indexed Addressing Tests ---
TEST_F(Disassembler65816Test, DisassembleLDAAbsoluteX) {
std::vector<uint8_t> code = {0xBD, 0x00, 0x80}; // LDA $8000,X
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "LDA");
EXPECT_EQ(result.size, 3u);
EXPECT_TRUE(result.operand_str.find("X") != std::string::npos);
}
TEST_F(Disassembler65816Test, DisassembleLDADirectPageIndirectY) {
std::vector<uint8_t> code = {0xB1, 0x10}; // LDA ($10),Y
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "LDA");
EXPECT_EQ(result.size, 2u);
EXPECT_TRUE(result.operand_str.find("Y") != std::string::npos);
}
// --- Special Instructions Tests ---
TEST_F(Disassembler65816Test, DisassembleREP) {
std::vector<uint8_t> code = {0xC2, 0x30}; // REP #$30 (16-bit A, X, Y)
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "REP");
EXPECT_EQ(result.size, 2u);
}
TEST_F(Disassembler65816Test, DisassembleSEP) {
std::vector<uint8_t> code = {0xE2, 0x20}; // SEP #$20 (8-bit A)
auto reader = CreateMemoryReader(code);
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "SEP");
EXPECT_EQ(result.size, 2u);
}
// --- Instruction Size Tests ---
TEST_F(Disassembler65816Test, GetInstructionSizeImplied) {
// NOP, RTS, RTL all have size 1
EXPECT_EQ(disassembler_.GetInstructionSize(0xEA, true, true), 1u); // NOP
EXPECT_EQ(disassembler_.GetInstructionSize(0x60, true, true), 1u); // RTS
EXPECT_EQ(disassembler_.GetInstructionSize(0x6B, true, true), 1u); // RTL
}
TEST_F(Disassembler65816Test, GetInstructionSizeAbsolute) {
// Absolute addressing is 3 bytes
EXPECT_EQ(disassembler_.GetInstructionSize(0xAD, true, true), 3u); // LDA abs
EXPECT_EQ(disassembler_.GetInstructionSize(0x8D, true, true), 3u); // STA abs
EXPECT_EQ(disassembler_.GetInstructionSize(0x20, true, true), 3u); // JSR abs
}
TEST_F(Disassembler65816Test, GetInstructionSizeLong) {
// Long addressing is 4 bytes
EXPECT_EQ(disassembler_.GetInstructionSize(0xAF, true, true), 4u); // LDA long
EXPECT_EQ(disassembler_.GetInstructionSize(0x22, true, true), 4u); // JSL long
}
// --- Symbol Resolution Tests ---
TEST_F(Disassembler65816Test, DisassembleWithSymbolResolver) {
std::vector<uint8_t> code = {0x20, 0x00, 0x80}; // JSR $8000
auto reader = CreateMemoryReader(code);
// Set up a symbol resolver that knows about $8000
disassembler_.SetSymbolResolver([](uint32_t addr) -> std::string {
if (addr == 0x008000) {
return "Reset";
}
return "";
});
auto result = disassembler_.Disassemble(0, reader, true, true);
EXPECT_EQ(result.mnemonic, "JSR");
// The operand_str should contain the symbol name
EXPECT_TRUE(result.operand_str.find("Reset") != std::string::npos ||
result.operand_str.find("8000") != std::string::npos);
}
} // namespace
} // namespace debug
} // namespace emu
} // namespace yaze

View File

@@ -0,0 +1,568 @@
/**
* @file ppu_catchup_test.cc
* @brief Unit tests for the PPU JIT catch-up system
*
* Tests the mid-scanline raster effect support:
* - StartLine(int line) - Initialize scanline, evaluate sprites
* - CatchUp(int h_pos) - Render pixels from last position to h_pos
* - RunLine(int line) - Legacy wrapper calling StartLine + CatchUp
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include "app/emu/memory/memory.h"
#include "app/emu/video/ppu.h"
#include "mocks/mock_memory.h"
namespace yaze {
namespace emu {
using ::testing::_;
using ::testing::Return;
/**
* @class PpuCatchupTestFixture
* @brief Test fixture for PPU catch-up system tests
*
* Provides a PPU instance with mock memory and helper methods
* for inspecting rendered output. Uses only public PPU APIs
* (Write, PutPixels, etc.) to ensure tests validate the public interface.
*/
class PpuCatchupTestFixture : public ::testing::Test {
protected:
void SetUp() override {
// Initialize mock memory with defaults
mock_memory_.memory_.resize(0x1000000, 0);
mock_memory_.Init();
// Setup default return values for memory interface
ON_CALL(mock_memory_, h_pos()).WillByDefault(Return(0));
ON_CALL(mock_memory_, v_pos()).WillByDefault(Return(0));
ON_CALL(mock_memory_, pal_timing()).WillByDefault(Return(false));
ON_CALL(mock_memory_, open_bus()).WillByDefault(Return(0));
// Create PPU with mock memory
ppu_ = std::make_unique<Ppu>(mock_memory_);
ppu_->Init();
ppu_->Reset();
// Initialize output pixel buffer for inspection
output_pixels_.resize(512 * 4 * 480, 0);
}
void TearDown() override { ppu_.reset(); }
/**
* @brief Copy pixel buffer to output array for inspection
*/
void CopyPixelBuffer() { ppu_->PutPixels(output_pixels_.data()); }
/**
* @brief Get pixel color at a specific position in the pixel buffer
* @param x X position (0-255)
* @param y Y position (0-238)
* @param even_frame True for even frame, false for odd
* @return ARGB color value
*
* Uses PutPixels() public API to copy the internal pixel buffer
* to an output array for inspection.
*/
uint32_t GetPixelAt(int x, int y, bool even_frame = true) {
// Copy pixel buffer to output array first
CopyPixelBuffer();
// Output buffer layout after PutPixels: row * 2048 + x * 8
// PutPixels copies to dest with row = y * 2 + (overscan ? 2 : 16)
// For simplicity, use the internal buffer structure
int dest_row = y * 2 + (ppu_->frame_overscan_ ? 2 : 16);
int offset = dest_row * 2048 + x * 8;
// Read BGRX format (format 0)
uint8_t b = output_pixels_[offset + 0];
uint8_t g = output_pixels_[offset + 1];
uint8_t r = output_pixels_[offset + 2];
uint8_t a = output_pixels_[offset + 3];
return (a << 24) | (r << 16) | (g << 8) | b;
}
/**
* @brief Check if pixel at position was rendered (non-zero)
*
* This checks the alpha channel in the output buffer after PutPixels.
* When pixels are rendered, they have alpha = 0xFF.
*/
bool IsPixelRendered(int x, int y, bool even_frame = true) {
CopyPixelBuffer();
int dest_row = y * 2 + (ppu_->frame_overscan_ ? 2 : 16);
int offset = dest_row * 2048 + x * 8;
// Check if alpha channel is 0xFF (rendered pixel)
return output_pixels_[offset + 3] == 0xFF;
}
/**
* @brief Setup a simple palette for testing
*/
void SetupTestPalette() {
// Set backdrop color (palette entry 0) to a known non-black value
// Format: 0bbbbbgggggrrrrr (15-bit BGR)
ppu_->cgram[0] = 0x001F; // Red backdrop
ppu_->cgram[1] = 0x03E0; // Green
ppu_->cgram[2] = 0x7C00; // Blue
}
/**
* @brief Enable main screen rendering for testing
*/
void EnableMainScreen() {
// Enable forced blank to false and brightness to max
ppu_->forced_blank_ = false;
ppu_->brightness = 15;
ppu_->mode = 0; // Mode 0 for simplicity
// Write to PPU registers via the Write method for proper state setup
// $2100: Screen Display - brightness 15, forced blank off
ppu_->Write(0x00, 0x0F);
// $212C: Main Screen Designation - enable BG1
ppu_->Write(0x2C, 0x01);
}
MockMemory mock_memory_;
std::unique_ptr<Ppu> ppu_;
std::vector<uint8_t> output_pixels_;
// Constants for cycle/pixel conversion
static constexpr int kCyclesPerPixel = 4;
static constexpr int kScreenWidth = 256;
static constexpr int kMaxHPos = kScreenWidth * kCyclesPerPixel; // 1024
};
// =============================================================================
// Basic Functionality Tests
// =============================================================================
TEST_F(PpuCatchupTestFixture, StartLineResetsRenderPosition) {
// GIVEN: PPU in a state where some pixels might have been rendered
ppu_->StartLine(50);
ppu_->CatchUp(400); // Render some pixels
// WHEN: Starting a new line
ppu_->StartLine(51);
// THEN: The next CatchUp should render from the beginning (x=0)
// We verify by rendering a small range and checking pixels are rendered
SetupTestPalette();
EnableMainScreen();
ppu_->CatchUp(40); // Render first 10 pixels (40/4 = 10)
// Pixel at x=0 should be rendered
EXPECT_TRUE(IsPixelRendered(0, 50));
}
TEST_F(PpuCatchupTestFixture, CatchUpRendersPixelRange) {
// GIVEN: PPU initialized for a scanline
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(100);
// WHEN: Calling CatchUp with h_pos = 200 (50 pixels)
ppu_->CatchUp(200);
// THEN: Pixels 0-49 should be rendered (h_pos 200 / 4 = 50)
for (int x = 0; x < 50; ++x) {
EXPECT_TRUE(IsPixelRendered(x, 99))
<< "Pixel at x=" << x << " should be rendered";
}
}
TEST_F(PpuCatchupTestFixture, CatchUpConvertsHPosToPosCorrectly) {
// GIVEN: PPU ready to render
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
// Test various h_pos values and their expected pixel counts
// h_pos / 4 = pixel position (1 pixel = 4 master cycles)
struct TestCase {
int h_pos;
int expected_pixels;
};
TestCase test_cases[] = {
{4, 1}, // 4 cycles = 1 pixel
{8, 2}, // 8 cycles = 2 pixels
{40, 10}, // 40 cycles = 10 pixels
{100, 25}, // 100 cycles = 25 pixels
{256, 64}, // 256 cycles = 64 pixels
};
for (const auto& tc : test_cases) {
ppu_->StartLine(50);
ppu_->CatchUp(tc.h_pos);
// Verify the last expected pixel is rendered
int last_pixel = tc.expected_pixels - 1;
EXPECT_TRUE(IsPixelRendered(last_pixel, 49))
<< "h_pos=" << tc.h_pos << " should render pixel " << last_pixel;
}
}
TEST_F(PpuCatchupTestFixture, CatchUpClampsTo256Pixels) {
// GIVEN: PPU ready to render
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
// WHEN: Calling CatchUp with h_pos > 1024 (beyond screen width)
ppu_->CatchUp(2000); // Should clamp to 256 pixels
// THEN: All 256 pixels should be rendered, but no more
for (int x = 0; x < 256; ++x) {
EXPECT_TRUE(IsPixelRendered(x, 49))
<< "Pixel at x=" << x << " should be rendered";
}
}
TEST_F(PpuCatchupTestFixture, CatchUpSkipsIfAlreadyRendered) {
// GIVEN: PPU has already rendered some pixels
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
ppu_->CatchUp(400); // Render pixels 0-99
// Record state of pixel buffer at position that's already rendered
uint32_t pixel_before = GetPixelAt(50, 49);
// WHEN: Calling CatchUp with same or earlier h_pos
ppu_->CatchUp(200); // Earlier than previous catch-up
ppu_->CatchUp(400); // Same as previous catch-up
// THEN: No pixels should be re-rendered (state unchanged)
uint32_t pixel_after = GetPixelAt(50, 49);
EXPECT_EQ(pixel_before, pixel_after);
}
TEST_F(PpuCatchupTestFixture, CatchUpProgressiveRendering) {
// GIVEN: PPU ready to render
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
// WHEN: Making progressive CatchUp calls
ppu_->CatchUp(100); // Render pixels 0-24
ppu_->CatchUp(200); // Render pixels 25-49
ppu_->CatchUp(300); // Render pixels 50-74
ppu_->CatchUp(1024); // Complete the line
// THEN: All pixels should be rendered correctly
for (int x = 0; x < 256; ++x) {
EXPECT_TRUE(IsPixelRendered(x, 49))
<< "Pixel at x=" << x << " should be rendered";
}
}
// =============================================================================
// Integration Tests
// =============================================================================
TEST_F(PpuCatchupTestFixture, RunLineRendersFullScanline) {
// GIVEN: PPU ready to render
SetupTestPalette();
EnableMainScreen();
// WHEN: Using RunLine (legacy wrapper)
ppu_->RunLine(100);
// THEN: All 256 pixels should be rendered
for (int x = 0; x < 256; ++x) {
EXPECT_TRUE(IsPixelRendered(x, 99))
<< "Pixel at x=" << x << " should be rendered by RunLine";
}
}
TEST_F(PpuCatchupTestFixture, MultipleCatchUpCallsRenderCorrectly) {
// GIVEN: PPU ready to render (simulating multiple register writes)
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
// WHEN: Simulating multiple mid-scanline register changes
// First segment: scroll at position 0
ppu_->CatchUp(200); // Render 50 pixels
// Simulated register change would happen here in real usage
// Second segment
ppu_->CatchUp(400); // Render next 50 pixels
// Third segment
ppu_->CatchUp(1024); // Complete the line
// THEN: All segments rendered correctly
for (int x = 0; x < 256; ++x) {
EXPECT_TRUE(IsPixelRendered(x, 49))
<< "Pixel at x=" << x << " should be rendered";
}
}
TEST_F(PpuCatchupTestFixture, ConsecutiveLinesRenderIndependently) {
// GIVEN: PPU ready to render multiple lines
SetupTestPalette();
EnableMainScreen();
// WHEN: Rendering consecutive lines
for (int line = 1; line <= 10; ++line) {
ppu_->RunLine(line);
}
// THEN: Each line should be fully rendered
for (int line = 0; line < 10; ++line) {
for (int x = 0; x < 256; ++x) {
EXPECT_TRUE(IsPixelRendered(x, line))
<< "Pixel at line=" << line << ", x=" << x << " should be rendered";
}
}
}
// =============================================================================
// Edge Case Tests
// =============================================================================
TEST_F(PpuCatchupTestFixture, CatchUpDuringForcedBlank) {
// GIVEN: PPU in forced blank mode
SetupTestPalette();
ppu_->forced_blank_ = true;
ppu_->brightness = 15;
ppu_->Write(0x00, 0x8F); // Forced blank enabled
ppu_->StartLine(50);
// WHEN: Calling CatchUp during forced blank
ppu_->CatchUp(1024);
// THEN: Pixels should be black (all zeros) during forced blank
uint32_t pixel = GetPixelAt(100, 49);
// In forced blank, HandlePixel skips color calculation, resulting in black
// The alpha channel should still be set, but RGB should be 0
uint8_t r = (pixel >> 16) & 0xFF;
uint8_t g = (pixel >> 8) & 0xFF;
uint8_t b = pixel & 0xFF;
EXPECT_EQ(r, 0) << "Red channel should be 0 during forced blank";
EXPECT_EQ(g, 0) << "Green channel should be 0 during forced blank";
EXPECT_EQ(b, 0) << "Blue channel should be 0 during forced blank";
}
TEST_F(PpuCatchupTestFixture, CatchUpMode7Handling) {
// GIVEN: PPU configured for Mode 7
SetupTestPalette();
EnableMainScreen();
ppu_->mode = 7;
ppu_->Write(0x05, 0x07); // Set mode 7
// Set Mode 7 matrix to identity (simple case)
// A = 0x0100 (1.0 in fixed point)
ppu_->Write(0x1B, 0x00); // M7A low
ppu_->Write(0x1B, 0x01); // M7A high
// B = 0x0000
ppu_->Write(0x1C, 0x00); // M7B low
ppu_->Write(0x1C, 0x00); // M7B high
// C = 0x0000
ppu_->Write(0x1D, 0x00); // M7C low
ppu_->Write(0x1D, 0x00); // M7C high
// D = 0x0100 (1.0 in fixed point)
ppu_->Write(0x1E, 0x00); // M7D low
ppu_->Write(0x1E, 0x01); // M7D high
ppu_->StartLine(50);
// WHEN: Calling CatchUp in Mode 7
ppu_->CatchUp(1024);
// THEN: Mode 7 calculations should execute without crash
// and pixels should be rendered
EXPECT_TRUE(IsPixelRendered(128, 49)) << "Mode 7 should render pixels";
}
TEST_F(PpuCatchupTestFixture, CatchUpAtScanlineStart) {
// GIVEN: PPU at start of scanline
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
// WHEN: Calling CatchUp at h_pos = 0
ppu_->CatchUp(0);
// THEN: No pixels should be rendered yet (target_x = 0, nothing to render)
// This is a no-op case
// Subsequent CatchUp should still work
ppu_->CatchUp(100);
EXPECT_TRUE(IsPixelRendered(24, 49));
}
TEST_F(PpuCatchupTestFixture, CatchUpAtScanlineEnd) {
// GIVEN: PPU mid-scanline
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
ppu_->CatchUp(500); // Render first 125 pixels
// WHEN: Calling CatchUp at end of scanline (h_pos >= 1024)
ppu_->CatchUp(1024); // Should complete the remaining pixels
ppu_->CatchUp(1500); // Should be a no-op (already at end)
// THEN: All 256 pixels should be rendered
EXPECT_TRUE(IsPixelRendered(0, 49));
EXPECT_TRUE(IsPixelRendered(127, 49));
EXPECT_TRUE(IsPixelRendered(255, 49));
}
TEST_F(PpuCatchupTestFixture, CatchUpWithNegativeOrZeroDoesNotCrash) {
// GIVEN: PPU ready to render
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(50);
// WHEN: Calling CatchUp with edge case values
// These should not crash and should be handled gracefully
ppu_->CatchUp(0);
ppu_->CatchUp(1);
ppu_->CatchUp(2);
ppu_->CatchUp(3);
// THEN: No crash occurred (test passes if we get here)
SUCCEED();
}
TEST_F(PpuCatchupTestFixture, StartLineEvaluatesSprites) {
// GIVEN: PPU with sprite data in OAM
SetupTestPalette();
EnableMainScreen();
// Enable sprites on main screen
ppu_->Write(0x2C, 0x10); // Enable OBJ on main screen
// Setup a simple sprite in OAM via Write interface
// $2102/$2103: OAM address
ppu_->Write(0x02, 0x00); // OAM address low = 0
ppu_->Write(0x03, 0x00); // OAM address high = 0
// $2104: Write OAM data (two writes per word)
// Sprite 0 word 0: X-low=100, Y=50
ppu_->Write(0x04, 100); // X position low byte
ppu_->Write(0x04, 50); // Y position
// Sprite 0 word 1: tile=1, attributes=0
ppu_->Write(0x04, 0x01); // Tile number low byte
ppu_->Write(0x04, 0x00); // Attributes
// WHEN: Starting a line where sprite should be visible
ppu_->StartLine(51); // Sprites are evaluated for line-1
// THEN: Sprite evaluation should run without crash
// The obj_pixel_buffer_ should be cleared/initialized
SUCCEED();
}
TEST_F(PpuCatchupTestFixture, BrightnessAffectsRenderedPixels) {
// GIVEN: PPU with a known palette color
ppu_->cgram[0] = 0x7FFF; // White (max values)
ppu_->forced_blank_ = false;
ppu_->mode = 0;
// Test with maximum brightness
ppu_->brightness = 15;
ppu_->StartLine(10);
ppu_->CatchUp(40); // Render 10 pixels at max brightness
uint32_t pixel_max = GetPixelAt(5, 9);
// Test with half brightness
ppu_->brightness = 7;
ppu_->StartLine(20);
ppu_->CatchUp(40);
uint32_t pixel_half = GetPixelAt(5, 19);
// THEN: Lower brightness should result in darker pixels
uint8_t r_max = (pixel_max >> 16) & 0xFF;
uint8_t r_half = (pixel_half >> 16) & 0xFF;
EXPECT_GT(r_max, r_half) << "Higher brightness should produce brighter pixels";
}
TEST_F(PpuCatchupTestFixture, EvenOddFrameHandling) {
// GIVEN: PPU in different frame states
SetupTestPalette();
EnableMainScreen();
// WHEN: Rendering on even frame
ppu_->even_frame = true;
ppu_->StartLine(50);
ppu_->CatchUp(1024);
// THEN: Pixels go to even frame buffer location
EXPECT_TRUE(IsPixelRendered(128, 49, true));
// WHEN: Rendering on odd frame
ppu_->even_frame = false;
ppu_->StartLine(50);
ppu_->CatchUp(1024);
// THEN: Pixels go to odd frame buffer location
EXPECT_TRUE(IsPixelRendered(128, 49, false));
}
// =============================================================================
// Performance Boundary Tests
// =============================================================================
TEST_F(PpuCatchupTestFixture, RenderFullFrameLines) {
// GIVEN: PPU ready to render
SetupTestPalette();
EnableMainScreen();
// WHEN: Rendering a complete frame worth of visible lines (1-224)
for (int line = 1; line <= 224; ++line) {
ppu_->RunLine(line);
}
// THEN: All lines should be rendered without crash
// Spot check a few lines
EXPECT_TRUE(IsPixelRendered(128, 0)); // Line 1
EXPECT_TRUE(IsPixelRendered(128, 111)); // Line 112
EXPECT_TRUE(IsPixelRendered(128, 223)); // Line 224
}
TEST_F(PpuCatchupTestFixture, MidScanlineRegisterChangeSimulation) {
// GIVEN: PPU ready for mid-scanline raster effects
SetupTestPalette();
EnableMainScreen();
ppu_->StartLine(100);
// Simulate a game that changes scroll mid-scanline
// First part: render with current scroll
ppu_->CatchUp(128 * 4); // Render first 128 pixels
// Change scroll register via PPU Write interface
// $210D: BG1 Horizontal Scroll (two writes)
ppu_->Write(0x0D, 0x08); // Low byte of scroll = 8
ppu_->Write(0x0D, 0x00); // High byte of scroll = 0
// Second part: render remaining pixels with new scroll
ppu_->CatchUp(256 * 4);
// THEN: Both halves rendered
EXPECT_TRUE(IsPixelRendered(0, 99));
EXPECT_TRUE(IsPixelRendered(127, 99));
EXPECT_TRUE(IsPixelRendered(128, 99));
EXPECT_TRUE(IsPixelRendered(255, 99));
}
} // namespace emu
} // namespace yaze

View File

@@ -0,0 +1,268 @@
/**
* @file step_controller_test.cc
* @brief Unit tests for the 65816 step controller (call stack tracking)
*
* Tests the StepOver and StepOut functionality that enables AI-assisted
* debugging with proper subroutine tracking.
*/
#include "app/emu/debug/step_controller.h"
#include <gtest/gtest.h>
#include <cstdint>
#include <functional>
#include <vector>
namespace yaze {
namespace emu {
namespace debug {
namespace {
class StepControllerTest : public ::testing::Test {
protected:
void SetUp() override {
// Reset program state
pc_ = 0;
instruction_count_ = 0;
}
// Simulates a simple memory with program code
void SetupProgram(const std::vector<uint8_t>& code, uint32_t base = 0) {
memory_ = code;
base_address_ = base;
pc_ = base;
controller_.SetMemoryReader([this](uint32_t addr) -> uint8_t {
uint32_t offset = addr - base_address_;
if (offset < memory_.size()) {
return memory_[offset];
}
return 0;
});
controller_.SetPcGetter([this]() -> uint32_t { return pc_; });
controller_.SetSingleStepper([this]() {
// Simulate executing one instruction by advancing PC
// This is a simplified simulation - real stepping would be more complex
if (pc_ >= base_address_ && pc_ < base_address_ + memory_.size()) {
uint8_t opcode = memory_[pc_ - base_address_];
uint8_t size = GetSimulatedInstructionSize(opcode);
pc_ += size;
instruction_count_++;
}
});
}
// Simplified instruction size for testing
uint8_t GetSimulatedInstructionSize(uint8_t opcode) {
switch (opcode) {
// Implied (1 byte)
case 0xEA: // NOP
case 0x60: // RTS
case 0x6B: // RTL
case 0x40: // RTI
case 0x18: // CLC
case 0x38: // SEC
case 0x78: // SEI
return 1;
// Branch (2 bytes)
case 0xD0: // BNE
case 0xF0: // BEQ
case 0x80: // BRA
case 0xA9: // LDA #imm (8-bit)
return 2;
// Absolute (3 bytes)
case 0x20: // JSR
case 0x4C: // JMP
case 0xAD: // LDA abs
case 0x8D: // STA abs
return 3;
// Long (4 bytes)
case 0x22: // JSL
case 0x5C: // JMP long
return 4;
default:
return 1;
}
}
StepController controller_;
std::vector<uint8_t> memory_;
uint32_t base_address_ = 0;
uint32_t pc_ = 0;
uint32_t instruction_count_ = 0;
};
// --- Basic Classification Tests ---
TEST_F(StepControllerTest, ClassifyCallInstructions) {
EXPECT_TRUE(StepController::IsCallInstruction(0x20)); // JSR
EXPECT_TRUE(StepController::IsCallInstruction(0x22)); // JSL
EXPECT_TRUE(StepController::IsCallInstruction(0xFC)); // JSR (abs,X)
EXPECT_FALSE(StepController::IsCallInstruction(0xEA)); // NOP
EXPECT_FALSE(StepController::IsCallInstruction(0x4C)); // JMP
EXPECT_FALSE(StepController::IsCallInstruction(0x60)); // RTS
}
TEST_F(StepControllerTest, ClassifyReturnInstructions) {
EXPECT_TRUE(StepController::IsReturnInstruction(0x60)); // RTS
EXPECT_TRUE(StepController::IsReturnInstruction(0x6B)); // RTL
EXPECT_TRUE(StepController::IsReturnInstruction(0x40)); // RTI
EXPECT_FALSE(StepController::IsReturnInstruction(0xEA)); // NOP
EXPECT_FALSE(StepController::IsReturnInstruction(0x20)); // JSR
EXPECT_FALSE(StepController::IsReturnInstruction(0x4C)); // JMP
}
TEST_F(StepControllerTest, ClassifyBranchInstructions) {
EXPECT_TRUE(StepController::IsBranchInstruction(0x80)); // BRA
EXPECT_TRUE(StepController::IsBranchInstruction(0xD0)); // BNE
EXPECT_TRUE(StepController::IsBranchInstruction(0xF0)); // BEQ
EXPECT_TRUE(StepController::IsBranchInstruction(0x4C)); // JMP abs
EXPECT_TRUE(StepController::IsBranchInstruction(0x5C)); // JMP long
EXPECT_FALSE(StepController::IsBranchInstruction(0xEA)); // NOP
EXPECT_FALSE(StepController::IsBranchInstruction(0x20)); // JSR
EXPECT_FALSE(StepController::IsBranchInstruction(0x60)); // RTS
}
// --- StepInto Tests ---
TEST_F(StepControllerTest, StepIntoSimpleInstruction) {
// Simple program: NOP NOP NOP
SetupProgram({0xEA, 0xEA, 0xEA});
auto result = controller_.StepInto();
EXPECT_TRUE(result.success);
EXPECT_EQ(result.instructions_executed, 1u);
EXPECT_EQ(result.new_pc, 1u); // PC advanced by 1 (NOP size)
EXPECT_FALSE(result.call.has_value());
EXPECT_FALSE(result.ret.has_value());
}
TEST_F(StepControllerTest, StepIntoTracksCallStack) {
// Program: JSR $0010 at address 0
// JSR opcode (0x20) + 2-byte address = 3 bytes
SetupProgram({0x20, 0x10, 0x00}); // JSR $0010
auto result = controller_.StepInto();
EXPECT_TRUE(result.success);
EXPECT_TRUE(result.call.has_value());
EXPECT_EQ(result.call->target_address, 0x0010u);
EXPECT_EQ(controller_.GetCallDepth(), 1u);
}
// --- Call Stack Management Tests ---
TEST_F(StepControllerTest, CallStackPushesOnJSR) {
SetupProgram({0x20, 0x10, 0x00}); // JSR $0010
EXPECT_EQ(controller_.GetCallDepth(), 0u);
controller_.StepInto();
EXPECT_EQ(controller_.GetCallDepth(), 1u);
const auto& stack = controller_.GetCallStack();
EXPECT_EQ(stack.back().target_address, 0x0010u);
EXPECT_FALSE(stack.back().is_long);
}
TEST_F(StepControllerTest, CallStackPushesOnJSL) {
SetupProgram({0x22, 0x00, 0x80, 0x01}); // JSL $018000
controller_.StepInto();
EXPECT_EQ(controller_.GetCallDepth(), 1u);
const auto& stack = controller_.GetCallStack();
EXPECT_EQ(stack.back().target_address, 0x018000u);
EXPECT_TRUE(stack.back().is_long); // JSL is a long call
}
TEST_F(StepControllerTest, ClearCallStackWorks) {
SetupProgram({0x20, 0x10, 0x00}); // JSR $0010
controller_.StepInto();
EXPECT_EQ(controller_.GetCallDepth(), 1u);
controller_.ClearCallStack();
EXPECT_EQ(controller_.GetCallDepth(), 0u);
}
// --- GetInstructionSize Tests ---
TEST_F(StepControllerTest, InstructionSizeImplied) {
// Implied addressing (1 byte)
EXPECT_EQ(StepController::GetInstructionSize(0xEA, true, true), 1u); // NOP
EXPECT_EQ(StepController::GetInstructionSize(0x60, true, true), 1u); // RTS
EXPECT_EQ(StepController::GetInstructionSize(0x6B, true, true), 1u); // RTL
EXPECT_EQ(StepController::GetInstructionSize(0x40, true, true), 1u); // RTI
EXPECT_EQ(StepController::GetInstructionSize(0x18, true, true), 1u); // CLC
EXPECT_EQ(StepController::GetInstructionSize(0xFB, true, true), 1u); // XCE
}
TEST_F(StepControllerTest, InstructionSizeBranch) {
// Relative branch (2 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x80, true, true), 2u); // BRA
EXPECT_EQ(StepController::GetInstructionSize(0xD0, true, true), 2u); // BNE
EXPECT_EQ(StepController::GetInstructionSize(0xF0, true, true), 2u); // BEQ
EXPECT_EQ(StepController::GetInstructionSize(0x10, true, true), 2u); // BPL
// Relative long (3 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x82, true, true), 3u); // BRL
}
TEST_F(StepControllerTest, InstructionSizeJumpCall) {
// JSR/JMP absolute (3 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x20, true, true), 3u); // JSR
EXPECT_EQ(StepController::GetInstructionSize(0x4C, true, true), 3u); // JMP abs
EXPECT_EQ(StepController::GetInstructionSize(0xFC, true, true), 3u); // JSR (abs,X)
// Long (4 bytes)
EXPECT_EQ(StepController::GetInstructionSize(0x22, true, true), 4u); // JSL
EXPECT_EQ(StepController::GetInstructionSize(0x5C, true, true), 4u); // JMP long
}
// --- Error Handling Tests ---
TEST_F(StepControllerTest, StepIntoFailsWithoutConfiguration) {
// Don't call SetupProgram - controller is unconfigured
auto result = controller_.StepInto();
EXPECT_FALSE(result.success);
EXPECT_EQ(result.instructions_executed, 0u);
}
TEST_F(StepControllerTest, StepOutFailsWithEmptyCallStack) {
SetupProgram({0xEA, 0xEA, 0xEA}); // Just NOPs
// Don't execute any calls, so stack is empty
auto result = controller_.StepOut(100);
EXPECT_FALSE(result.success);
EXPECT_TRUE(result.message.find("empty") != std::string::npos);
}
// --- StepOver Non-Call Instruction ---
TEST_F(StepControllerTest, StepOverNonCallIsSameAsStepInto) {
// Program: NOP NOP
SetupProgram({0xEA, 0xEA});
auto result = controller_.StepOver(1000);
EXPECT_TRUE(result.success);
EXPECT_EQ(result.instructions_executed, 1u);
EXPECT_EQ(result.new_pc, 1u);
}
} // namespace
} // namespace debug
} // namespace emu
} // namespace yaze