mirror of
https://github.com/ApfelTeeSaft/Yaroze.NET.git
synced 2026-08-26 19:43:27 +00:00
333 lines
9.8 KiB
C#
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)
|
|
}
|
|
}
|