Files
Yaroze.NET/tests/Yaroze.Tests/Integration/EmulatorIntegrationTests.cs
T
2026-01-24 17:23:50 +01:00

333 lines
9.8 KiB
C#

using Yaroze.Core;
using Yaroze.Core.Interfaces;
namespace Yaroze.Tests.Integration;
public class EmulatorIntegrationTests
{
/// <summary>
/// Simple trace sink for testing.
/// </summary>
private class TestTraceSink : ITraceSink
{
public List<string> InstructionTrace { get; } = new();
public List<string> ExceptionTrace { get; } = new();
public void TraceInstruction(uint pc, uint instruction, string? disassembly = null)
{
InstructionTrace.Add($"PC=0x{pc:X8} Instr=0x{instruction:X8}");
}
public void TraceMemoryRead(uint address, uint value, int size)
{
// Not needed for these tests
}
public void TraceMemoryWrite(uint address, uint value, int size)
{
// Not needed for these tests
}
public void TraceException(string exceptionType, uint pc)
{
ExceptionTrace.Add($"{exceptionType} at 0x{pc:X8}");
}
}
private byte[] CreateTestExe(params uint[] instructions)
{
// Create PS-EXE with specified instructions
byte[] exe = new byte[0x800 + instructions.Length * 4];
// Magic
Array.Copy(System.Text.Encoding.ASCII.GetBytes("PS-X EXE"), 0, exe, 0, 8);
// Entry point
WriteUInt32LE(exe, 0x010, 0x80010000);
// GP
WriteUInt32LE(exe, 0x014, 0x80020000);
// Load address
WriteUInt32LE(exe, 0x018, 0x80010000);
// File size
WriteUInt32LE(exe, 0x01C, (uint)(instructions.Length * 4));
// Write instructions
for (int i = 0; i < instructions.Length; i++)
{
WriteUInt32LE(exe, 0x800 + i * 4, instructions[i]);
}
return exe;
}
private void WriteUInt32LE(byte[] data, int offset, uint value)
{
data[offset] = (byte)(value & 0xFF);
data[offset + 1] = (byte)((value >> 8) & 0xFF);
data[offset + 2] = (byte)((value >> 16) & 0xFF);
data[offset + 3] = (byte)((value >> 24) & 0xFF);
}
[Fact]
public void Emulator_LoadAndRun_SimpleProgram()
{
var emu = new Emulator();
// Create a simple program:
// ADDIU $1, $0, 10 # $1 = 10
// ADDIU $2, $0, 20 # $2 = 20
// ADDU $3, $1, $2 # $3 = $1 + $2 = 30
// NOP (loop forever)
byte[] exeData = CreateTestExe(
0x2401000A, // ADDIU $1, $0, 10
0x24020014, // ADDIU $2, $0, 20
0x00221821, // ADDU $3, $1, $2
0x00000000 // NOP
);
emu.LoadExe(exeData);
// Execute the program
emu.StepN(4);
// Check results
Assert.Equal(10u, emu.Cpu.Registers.ReadGPR(1));
Assert.Equal(20u, emu.Cpu.Registers.ReadGPR(2));
Assert.Equal(30u, emu.Cpu.Registers.ReadGPR(3));
}
[Fact]
public void Emulator_LoadAndRun_WithBranch()
{
var emu = new Emulator();
// Program with a branch:
// ADDIU $1, $0, 5
// ADDIU $2, $0, 5
// BEQ $1, $2, skip
// ADDIU $3, $0, 99 (should be skipped)
// skip: ADDIU $4, $0, 42
byte[] exeData = CreateTestExe(
0x24010005, // ADDIU $1, $0, 5
0x24020005, // ADDIU $2, $0, 5
0x10220001, // BEQ $1, $2, +1 (skip next instruction)
0x24030063, // ADDIU $3, $0, 99 (skipped)
0x2404002A // ADDIU $4, $0, 42
);
emu.LoadExe(exeData);
// Execute
emu.StepN(5);
// $3 should be 0 (instruction was skipped)
// $4 should be 42
Assert.Equal(0u, emu.Cpu.Registers.ReadGPR(3));
Assert.Equal(42u, emu.Cpu.Registers.ReadGPR(4));
}
[Fact]
public void Emulator_LoadAndRun_MemoryOperations()
{
var emu = new Emulator();
// Program using memory:
// LUI $1, 0x8001 # $1 = 0x80010000
// ADDIU $1, $1, 0x100 # $1 = 0x80010100
// ADDIU $2, $0, 0x42 # $2 = 0x42
// SW $2, 0($1) # Store 0x42 at 0x80010100
// LW $3, 0($1) # Load from 0x80010100
// NOP (for load delay)
byte[] exeData = CreateTestExe(
0x3C018001, // LUI $1, 0x8001
0x24210100, // ADDIU $1, $1, 0x100
0x24020042, // ADDIU $2, $0, 0x42
0xAC220000, // SW $2, 0($1)
0x8C230000, // LW $3, 0($1)
0x00000000 // NOP
);
emu.LoadExe(exeData);
// Execute
emu.StepN(6);
// $3 should contain the loaded value (0x42)
Assert.Equal(0x42u, emu.Cpu.Registers.ReadGPR(3));
}
[Fact]
public void Emulator_TraceSink_CapturesExecution()
{
var emu = new Emulator();
var trace = new TestTraceSink();
emu.SetTraceSink(trace);
byte[] exeData = CreateTestExe(
0x00000000, // NOP
0x00000000, // NOP
0x00000000 // NOP
);
emu.LoadExe(exeData);
emu.StepN(3);
// Should have traced 3 instructions
Assert.Equal(3, trace.InstructionTrace.Count);
}
[Fact]
public void Emulator_SYSCALL_TriggersException()
{
var emu = new Emulator();
var trace = new TestTraceSink();
emu.SetTraceSink(trace);
byte[] exeData = CreateTestExe(
0x0000000C // SYSCALL
);
emu.LoadExe(exeData);
emu.Step();
// Should have triggered a syscall exception
Assert.Single(trace.ExceptionTrace);
Assert.Contains("Syscall", trace.ExceptionTrace[0]);
}
[Fact]
public void Emulator_Stats_UpdatesCorrectly()
{
var emu = new Emulator();
byte[] exeData = CreateTestExe(
0x00000000,
0x00000000,
0x00000000
);
emu.LoadExe(exeData);
var statsBefore = emu.GetStats();
Assert.Equal(0ul, statsBefore.TotalCycles);
emu.StepN(3);
var statsAfter = emu.GetStats();
Assert.Equal(3ul, statsAfter.TotalCycles);
}
[Fact]
public void Emulator_Reset_ClearsState()
{
var emu = new Emulator();
byte[] exeData = CreateTestExe(
0x24010042 // ADDIU $1, $0, 0x42
);
emu.LoadExe(exeData);
emu.Step();
// $1 should be 0x42
Assert.Equal(0x42u, emu.Cpu.Registers.ReadGPR(1));
// Reset
emu.Reset();
// $1 should be 0 again
Assert.Equal(0u, emu.Cpu.Registers.ReadGPR(1));
// PC should be at BIOS entry point
Assert.Equal(0xBFC00000u, emu.Cpu.Registers.PC);
}
[Fact]
public void Emulator_ComplexCalculation_Fibonacci()
{
var emu = new Emulator();
// Calculate Fibonacci(10) = 55
// $1 = a (current)
// $2 = b (next)
// $3 = counter
// $4 = temp
byte[] exeData = CreateTestExe(
// Initialize: a=0, b=1, counter=10
0x24010000, // ADDIU $1, $0, 0 # a = 0
0x24020001, // ADDIU $2, $0, 1 # b = 1
0x2403000A, // ADDIU $3, $0, 10 # counter = 10
// loop:
0x00221021, // ADDU $4, $1, $2 # temp = a + b
0x00400821, // ADDU $1, $2, $0 # a = b
0x00800000, // SLL $0, $0, 0 # NOP (for pipeline)
0x00801021, // ADDU $2, $4, $0 # b = temp
0x2463FFFF, // ADDIU $3, $3, -1 # counter--
0x1460FFFA, // BNE $3, $0, loop # if counter != 0, goto loop
0x00000000 // NOP (delay slot)
);
emu.LoadExe(exeData);
// Run enough iterations to complete the loop
// Each iteration: 7 instructions
// 10 iterations + setup = ~80 instructions
emu.StepN(100);
// $1 should contain Fibonacci(10) = 55
Assert.Equal(55u, emu.Cpu.Registers.ReadGPR(1));
}
[Fact]
public void Emulator_GteInstructions_ExecuteWithoutCrashing()
{
// Test that programs using GTE (COP2) instructions can run
var emu = new Emulator();
byte[] exeData = CreateTestExe(
// Setup test values
0x24010010, // ADDIU $1, $0, 0x10 # $1 = 16
0x24020020, // ADDIU $2, $0, 0x20 # $2 = 32
// Write to GTE data register
0x48810000, // MTC2 $1, $0 # GTE[0] = $1
0x48820001, // MTC2 $2, $1 # GTE[1] = $2
// Write to GTE control register
0x48C10005, // CTC2 $1, $5 # GTE_CTL[5] = $1
// Read from GTE data register
0x48030000, // MFC2 $3, $0 # $3 = GTE[0]
0x00000000, // NOP (load delay slot)
// Read from GTE control register
0x48440005, // CFC2 $4, $5 # $4 = GTE_CTL[5]
0x00000000, // NOP (load delay slot)
// Execute GTE command (minimal implementation - clears FLAG register)
0x4A180001, // COP2 command (RTPS - Perspective Transformation)
// Store/Load GTE register to/from memory
0xE8010100, // SWC2 $1, 0x100($0) # RAM[0x100] = GTE[1]
0xC8050100 // LWC2 $5, 0x100($0) # GTE[5] = RAM[0x100]
);
emu.LoadExe(exeData);
// Execute all instructions - should not throw
emu.StepN(13);
// Verify results
Assert.Equal(0x10u, emu.Cpu.Registers.ReadGPR(3)); // $3 = GTE[0] = 16
Assert.Equal(0x10u, emu.Cpu.Registers.ReadGPR(4)); // $4 = GTE_CTL[5] = 16
Assert.Equal(0x10u, emu.Cpu.Gte.ReadDataRegister(0)); // GTE[0] = 16
Assert.Equal(0x20u, emu.Cpu.Gte.ReadDataRegister(1)); // GTE[1] = 32
Assert.Equal(0x10u, emu.Cpu.Gte.ReadControlRegister(5)); // GTE_CTL[5] = 16
Assert.Equal(0x20u, emu.Cpu.Gte.ReadDataRegister(5)); // GTE[5] = 32 (from LWC2)
}
}