This commit is contained in:
ApfelTeeSaft
2026-01-24 17:23:50 +01:00
parent cb9d08c10a
commit f95430898a
65 changed files with 17461 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
# .NET build outputs
bin/
obj/
out/
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio / Rider
.vs/
.idea/
*.rsuser
# Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# NuGet
*.nupkg
*.snupkg
**/packages/*
!**/packages/build/
*.nuget.props
*.nuget.targets
# Test results
TestResults/
*.trx
*.coverage
*.coveragexml
# Test ROMs and BIOS files (copyrighted, user must provide)
tests/test-roms/
*.bin
*.iso
*.cue
*.bios
SCPH*.bin
# OS files
.DS_Store
Thumbs.db
*.swp
*~
# Rider
.idea/
*.sln.iml
# Visual Studio Code
.vscode/
# Publish profiles
PublishProfiles/
# NuGet restore lock files
packages.lock.json
+326
View File
@@ -0,0 +1,326 @@
# Yaroze.Core - PlayStation 1 Emulation Library
A high-accuracy PlayStation 1 emulation core library for .NET 8, providing complete hardware emulation, analysis tools, and JIT compilation capabilities.
## Overview
Yaroze.Core is a comprehensive PS1 emulation library designed for accuracy, testability, and ease of integration. It provides both interpretation and JIT compilation of MIPS R3000A code, along with powerful static analysis tools for reverse engineering PS1 software.
## Features
### Hardware Emulation
- **MIPS R3000A CPU** - Complete instruction set (55+ instructions) with accurate delay slots, exceptions, and overflow handling
- **GPU** - 1MB VRAM, GP0/GP1 command processing, display control, CPU↔VRAM transfers
- **CD-ROM** - Full disc image support (ISO/BIN/CUE) with multi-track parsing and sector reading
- **DMA Controller** - 7 channels supporting burst, slice, and linked-list transfer modes
- **Timers** - 3 root counters with multiple clock sources and IRQ generation
- **Interrupt Controller** - Hardware interrupt routing with masking
- **GTE (COP2)** - Geometry Transformation Engine register interface
- **Memory System** - RAM, Scratchpad, BIOS, memory-mapped I/O with proper mirroring
### Analysis & Debugging
- **MIPS Disassembler** - Full R3000A instruction set with register names and symbolic references
- **Pseudo-C Decompiler** - Converts assembly to readable C-like pseudocode
- **Function Analyzer** - Control flow analysis, function discovery, and call graph generation
- **Cross-Reference Tracker** - Tracks all calls, jumps, and branch targets
- **Symbol Manager** - Label and comment management with import/export
- **Execution Tracing** - Hook-based tracing for instruction and memory access analysis
### Performance
- **JIT Compiler** - Compiles MIPS code to native x64 using .NET Expression Trees
- **Lockstep Verification** - Validates JIT output against interpreter for correctness
- **Basic Block Scanner** - Identifies compilation units for optimal performance
## Installation
### NuGet Package
```bash
dotnet add package Yaroze.Core
```
### From Source
```bash
git clone https://github.com/apfelteesaft/Yaroze.git
cd Yaroze
dotnet build src/Yaroze.Core
```
## Quick Start
### Basic Emulation
```csharp
using Yaroze.Core;
// Create emulator instance
var emulator = new Emulator();
// Load a PS-EXE file
emulator.LoadExeFromFile("GAME.EXE");
// Execute instructions
emulator.Step(); // Single step
emulator.StepN(1000); // Execute 1000 instructions
emulator.Run(); // Continuous execution (blocking)
// Access CPU state
uint pc = emulator.Cpu.Registers.PC;
uint v0 = emulator.Cpu.Registers.ReadGPR(2);
// Get execution statistics
var stats = emulator.GetStats();
Console.WriteLine($"Executed {stats.InstructionsExecuted} instructions");
```
### Loading CD-ROM Images
```csharp
var emulator = new Emulator();
// Load disc image (supports .iso, .bin, .cue)
emulator.LoadDisc("game.bin");
// Access CD-ROM device
var cdrom = emulator.CdRom;
```
### Memory Access
```csharp
var emulator = new Emulator();
// Access RAM directly
emulator.Bus.Ram.Write32(0x80000000, 0x12345678);
uint value = emulator.Bus.Ram.Read32(0x80000000);
// Write to memory via bus (handles all devices)
emulator.Bus.Write32(0x1F801810, 0xA0000000); // GPU GP0 command
```
### Disassembly & Analysis
```csharp
using Yaroze.Core.Disassembly;
using Yaroze.Core.Analysis;
var emulator = new Emulator();
emulator.LoadExeFromFile("GAME.EXE");
// Disassemble at address
var disasm = new MipsDisassembler();
uint instructionWord = emulator.Bus.Read32(0x80000000);
string assembly = disasm.Disassemble(0x80000000, instructionWord);
// Analyze functions
var analyzer = new FunctionAnalyzer(emulator.Bus.Ram);
analyzer.AnalyzeFrom(0x80000000);
foreach (var func in analyzer.GetAllFunctions())
{
Console.WriteLine($"Function at 0x{func.StartAddress:X8}");
Console.WriteLine($" Calls: {func.CallCount}");
}
// Decompile to pseudo-C
var decompiler = new PseudoCDecompiler(emulator.Bus.Ram.Data, 0x80000000);
string pseudoC = decompiler.DecompileFunction(0x80000000, 0x100);
Console.WriteLine(pseudoC);
```
### Execution Tracing
```csharp
using Yaroze.Core.Interfaces;
class MyTracer : ITraceSink
{
public void TraceInstruction(uint pc, uint instruction, string? disassembly)
{
Console.WriteLine($"[{pc:X8}] {disassembly}");
}
public void TraceMemoryRead(uint address, uint value, int size)
{
Console.WriteLine($" Read [{address:X8}] = 0x{value:X}");
}
public void TraceMemoryWrite(uint address, uint value, int size)
{
Console.WriteLine($" Write [{address:X8}] = 0x{value:X}");
}
}
var emulator = new Emulator();
emulator.SetTraceSink(new MyTracer());
emulator.LoadExeFromFile("GAME.EXE");
emulator.StepN(10); // Trace first 10 instructions
```
## Supported File Formats
| Format | Extension | Description |
|--------|-----------|-------------|
| PS-EXE | `.exe`, `.psx` | PlayStation executables with header |
| BIN | `.bin` | Raw CD-ROM images (2352 bytes/sector) |
| ISO | `.iso` | ISO 9660 filesystem images (2048 bytes/sector) |
| CUE | `.cue` | Cue sheet descriptors for multi-track discs |
## Architecture
```
Yaroze.Core/
├── CPU/ # MIPS R3000A CPU interpreter
│ ├── Cpu.cs # Main CPU implementation
│ ├── Registers.cs # Register file with delay slots
│ ├── Instruction.cs # Instruction decoding
│ ├── Coprocessor0.cs # System control coprocessor
│ └── Gte.cs # Geometry Transformation Engine
├── JIT/ # Just-In-Time compiler
│ ├── JitCompiler.cs # Expression tree based compiler
│ ├── BasicBlockScanner.cs
│ └── LockstepVerifier.cs
├── GPU/ # Graphics processing unit
│ └── Gpu.cs # VRAM, GP0/GP1 commands
├── CDROM/ # CD-ROM drive emulation
│ ├── CdRomDevice.cs # Drive controller
│ ├── DiscImage.cs # Disc image loader
│ └── CueSheet.cs # CUE file parser
├── DMA/ # DMA controller
│ └── DmaController.cs
├── Memory/ # Memory subsystem
│ ├── Bus.cs # Memory-mapped I/O bus
│ ├── Ram.cs # Main RAM
│ ├── Bios.cs # BIOS ROM
│ └── Scratchpad.cs # Fast scratchpad RAM
├── Timers/ # Root counters
│ └── Timer.cs
├── Interrupts/ # Interrupt controller
│ └── InterruptController.cs
├── Disassembly/ # Static analysis
│ ├── MipsDisassembler.cs
│ └── PseudoCDecompiler.cs
├── Analysis/ # Code analysis tools
│ ├── FunctionAnalyzer.cs
│ ├── CrossReferenceTracker.cs
│ └── SymbolManager.cs
└── Emulator.cs # Top-level emulator class
```
## Testing
The library includes over **400 comprehensive unit and integration tests**:
```bash
dotnet test
```
Tests cover:
- All 55+ MIPS R3000A instructions
- Exception handling and COP0 operations
- Load/branch delay slot behavior
- Memory operations and DMA transfers
- GPU command processing
- CD-ROM disc image parsing
- JIT compiler correctness (lockstep verification)
- Static analysis tools
## Technical Details
### Accuracy Features
- **Load Delay Slots** - Correctly implements MIPS load delay behavior where the loaded value is not available until after the next instruction
- **Branch Delay Slots** - Accurate handling of the instruction following a branch/jump
- **Exception Precision** - Proper exception timing, COP0 state management, and EPC calculation
- **Overflow Detection** - ADD/SUB/ADDI trigger overflow exceptions on signed overflow
- **Lockstep Verification** - JIT compiler output validated instruction-by-instruction against interpreter
### Design Principles
- **Clean Architecture** - Core emulation logic independent of UI or application code
- **Interface-Based** - `IBusDevice` abstraction for memory-mapped devices
- **Testable** - Comprehensive test coverage with deterministic execution
- **Well-Documented** - Implementation backed by PSX-SPX and MIPS R3000A specifications
### Performance Characteristics
- **Interpreter Mode** - ~1 cycle per instruction (variable timing not yet implemented)
- **JIT Mode** - Native code execution with verification overhead
- **Memory Access** - Direct array access for RAM, virtual dispatch for I/O devices
## Advanced Usage
### Custom Memory-Mapped Devices
```csharp
using Yaroze.Core.Interfaces;
public class CustomDevice : IBusDevice
{
public bool Contains(uint address) => address >= 0x1F802000 && address < 0x1F802100;
public uint Read32(uint address)
{
// Handle read
return 0;
}
public void Write32(uint address, uint value)
{
// Handle write
}
// Implement other IBusDevice methods...
}
var emulator = new Emulator();
emulator.Bus.AddDevice(new CustomDevice());
```
### Symbol Management
```csharp
var symbols = new SymbolManager();
// Add labels
symbols.AddLabel(0x80000000, "main");
symbols.AddLabel(0x80001000, "gameLoop");
// Add comments
symbols.AddComment(0x80000000, "Entry point");
// Export/Import
string json = symbols.ExportToJson();
SymbolManager.ImportFromJson(json);
```
## BIOS Requirement
**Legal Notice**: PlayStation BIOS files are copyrighted by Sony and cannot be distributed with this library.
To use BIOS-dependent features, you must:
1. Legally obtain a BIOS dump from your own PlayStation console
2. Load it using the `LoadBios()` method
The library provides BIOS interfaces but no BIOS data.
## Contributing
Contributions are welcome! Please ensure:
- All tests pass (`dotnet test`)
- New features include comprehensive tests
- Code follows existing architectural patterns
- Public APIs are documented with XML comments
## License
Who tf needs Licensing Comrade?
## Acknowledgments
- **Martin "nocash" Korth** - Comprehensive PSX-SPX documentation
- **MIPS Technologies** - MIPS R3000A architecture documentation
- **PS1 Emulation Community** - Collective reverse engineering knowledge
---
**Philosophy**: *Accuracy over performance. Testability over cleverness. Documentation over assumptions.*
+43
View File
@@ -0,0 +1,43 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{81CD06EC-354B-45F5-9177-9A19B1CB179B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yaroze.Core", "src\Yaroze.Core\Yaroze.Core.csproj", "{5A5A73A4-BC52-4BEB-B6D9-23143D583582}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yaroze.Frontend", "src\Yaroze.Frontend\Yaroze.Frontend.csproj", "{7CCAF0CA-0472-4339-8355-540FCB47B3EA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F1AED004-8730-4FEB-A71A-585BD1303816}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yaroze.Tests", "tests\Yaroze.Tests\Yaroze.Tests.csproj", "{7C2771EE-D0A6-4DB7-AB27-8058FB5FF3B3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5A5A73A4-BC52-4BEB-B6D9-23143D583582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A5A73A4-BC52-4BEB-B6D9-23143D583582}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A5A73A4-BC52-4BEB-B6D9-23143D583582}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A5A73A4-BC52-4BEB-B6D9-23143D583582}.Release|Any CPU.Build.0 = Release|Any CPU
{7CCAF0CA-0472-4339-8355-540FCB47B3EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CCAF0CA-0472-4339-8355-540FCB47B3EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CCAF0CA-0472-4339-8355-540FCB47B3EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CCAF0CA-0472-4339-8355-540FCB47B3EA}.Release|Any CPU.Build.0 = Release|Any CPU
{7C2771EE-D0A6-4DB7-AB27-8058FB5FF3B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C2771EE-D0A6-4DB7-AB27-8058FB5FF3B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C2771EE-D0A6-4DB7-AB27-8058FB5FF3B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C2771EE-D0A6-4DB7-AB27-8058FB5FF3B3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5A5A73A4-BC52-4BEB-B6D9-23143D583582} = {81CD06EC-354B-45F5-9177-9A19B1CB179B}
{7CCAF0CA-0472-4339-8355-540FCB47B3EA} = {81CD06EC-354B-45F5-9177-9A19B1CB179B}
{7C2771EE-D0A6-4DB7-AB27-8058FB5FF3B3} = {F1AED004-8730-4FEB-A71A-585BD1303816}
EndGlobalSection
EndGlobal
@@ -0,0 +1,286 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Yaroze.Core.CPU;
namespace Yaroze.Core.Analysis;
/// <summary>
/// Tracks cross-references between code locations (who calls whom, who jumps where).
/// </summary>
public class CrossReferenceTracker
{
private readonly byte[] _memory;
private readonly uint _baseAddress;
private readonly Dictionary<uint, List<XRef>> _xrefsTo = new();
private readonly Dictionary<uint, List<XRef>> _xrefsFrom = new();
public CrossReferenceTracker(byte[] memory, uint baseAddress)
{
_memory = memory;
_baseAddress = baseAddress;
}
/// <summary>
/// Gets all cross-references TO a specific address.
/// </summary>
public IReadOnlyList<XRef> GetXRefsTo(uint address)
{
return _xrefsTo.TryGetValue(address, out var xrefs) ? xrefs : Array.Empty<XRef>();
}
/// <summary>
/// Gets all cross-references FROM a specific address.
/// </summary>
public IReadOnlyList<XRef> GetXRefsFrom(uint address)
{
return _xrefsFrom.TryGetValue(address, out var xrefs) ? xrefs : Array.Empty<XRef>();
}
/// <summary>
/// All cross-references tracked.
/// </summary>
public IReadOnlyDictionary<uint, List<XRef>> XRefsTo => _xrefsTo;
/// <summary>
/// Analyzes a range of code to find all cross-references.
/// </summary>
public void AnalyzeRange(uint startAddress, uint endAddress)
{
for (uint address = startAddress; address < endAddress; address += 4)
{
AnalyzeInstruction(address);
}
}
/// <summary>
/// Analyzes instructions from a function analyzer.
/// </summary>
public void AnalyzeFromFunctionAnalyzer(FunctionAnalyzer analyzer)
{
foreach (var (_, function) in analyzer.Functions)
{
foreach (var instructionAddress in function.Instructions)
{
AnalyzeInstruction(instructionAddress);
}
}
}
private void AnalyzeInstruction(uint address)
{
uint instruction = ReadInstruction(address);
if (instruction == 0)
return;
var instr = new Instruction(instruction);
switch (instr.Opcode)
{
case Opcode.JAL:
{
uint target = instr.JumpTarget(address);
AddXRef(address, target, XRefType.Call);
}
break;
case Opcode.J:
{
uint target = instr.JumpTarget(address);
AddXRef(address, target, XRefType.Jump);
}
break;
case Opcode.SPECIAL:
// Check funct field for SPECIAL instructions
switch (instr.Funct)
{
case Funct.JALR:
// Indirect call - can't determine target statically
AddXRef(address, 0, XRefType.IndirectCall);
break;
case Funct.JR:
if (instr.Rs != 31) // Not a return
{
// Indirect jump - can't determine target statically
AddXRef(address, 0, XRefType.IndirectJump);
}
break;
}
break;
case Opcode.REGIMM:
// Check rt field for REGIMM branch instructions
{
uint target = instr.BranchTarget(address);
XRefType type = (instr.Rt == RegImmRt.BLTZAL || instr.Rt == RegImmRt.BGEZAL)
? XRefType.Call
: XRefType.Branch;
AddXRef(address, target, type);
}
break;
case Opcode.BEQ:
case Opcode.BNE:
case Opcode.BLEZ:
case Opcode.BGTZ:
{
uint target = instr.BranchTarget(address);
AddXRef(address, target, XRefType.Branch);
}
break;
case Opcode.LUI:
// Track potential address loads for data references
// This is speculative - we'd need to track the next instruction
break;
}
}
private void AddXRef(uint from, uint to, XRefType type)
{
var xref = new XRef
{
From = from,
To = to,
Type = type
};
// Add to "XRefs TO" dictionary
if (to != 0)
{
if (!_xrefsTo.ContainsKey(to))
{
_xrefsTo[to] = new List<XRef>();
}
_xrefsTo[to].Add(xref);
}
// Add to "XRefs FROM" dictionary
if (!_xrefsFrom.ContainsKey(from))
{
_xrefsFrom[from] = new List<XRef>();
}
_xrefsFrom[from].Add(xref);
}
private uint ReadInstruction(uint address)
{
int offset = (int)(address - _baseAddress);
if (offset < 0 || offset + 3 >= _memory.Length)
return 0;
return BitConverter.ToUInt32(_memory, offset);
}
/// <summary>
/// Generates a textual cross-reference report for an address.
/// </summary>
public string GenerateXRefReport(uint address, SymbolManager? symbolManager = null)
{
var sb = new System.Text.StringBuilder();
// XRefs TO this address
var xrefsTo = GetXRefsTo(address);
if (xrefsTo.Count > 0)
{
sb.AppendLine($"Cross-references TO 0x{address:X8}:");
foreach (var xref in xrefsTo.OrderBy(x => x.From))
{
string fromName = symbolManager?.GetSymbol(xref.From)?.Name ?? $"0x{xref.From:X8}";
string typeStr = xref.Type.ToString().ToLower();
sb.AppendLine($" {fromName} ({typeStr})");
}
sb.AppendLine();
}
// XRefs FROM this address
var xrefsFrom = GetXRefsFrom(address);
if (xrefsFrom.Count > 0)
{
sb.AppendLine($"Cross-references FROM 0x{address:X8}:");
foreach (var xref in xrefsFrom.OrderBy(x => x.To))
{
if (xref.To == 0)
{
sb.AppendLine($" <indirect> ({xref.Type.ToString().ToLower()})");
}
else
{
string toName = symbolManager?.GetSymbol(xref.To)?.Name ?? $"0x{xref.To:X8}";
string typeStr = xref.Type.ToString().ToLower();
sb.AppendLine($" {toName} ({typeStr})");
}
}
}
return sb.ToString();
}
/// <summary>
/// Gets statistics about cross-references.
/// </summary>
public XRefStats GetStats()
{
return new XRefStats
{
TotalXRefs = _xrefsTo.Values.Sum(list => list.Count),
AddressesWithXRefsTo = _xrefsTo.Count,
AddressesWithXRefsFrom = _xrefsFrom.Count,
CallCount = _xrefsTo.Values.SelectMany(list => list).Count(x => x.Type == XRefType.Call),
JumpCount = _xrefsTo.Values.SelectMany(list => list).Count(x => x.Type == XRefType.Jump),
BranchCount = _xrefsTo.Values.SelectMany(list => list).Count(x => x.Type == XRefType.Branch)
};
}
}
/// <summary>
/// Represents a cross-reference between two code locations.
/// </summary>
public class XRef
{
public uint From { get; set; }
public uint To { get; set; }
public XRefType Type { get; set; }
public override string ToString()
{
if (To == 0)
{
return $"0x{From:X8} -> <indirect> ({Type})";
}
return $"0x{From:X8} -> 0x{To:X8} ({Type})";
}
}
/// <summary>
/// Type of cross-reference.
/// </summary>
public enum XRefType
{
Call, // JAL, JALR, BLTZAL, BGEZAL
Jump, // J
Branch, // BEQ, BNE, BLEZ, BGTZ, etc.
IndirectCall, // JALR (target unknown)
IndirectJump, // JR (target unknown, not return)
DataReference // Load/store to specific address
}
/// <summary>
/// Statistics about cross-references.
/// </summary>
public class XRefStats
{
public int TotalXRefs { get; set; }
public int AddressesWithXRefsTo { get; set; }
public int AddressesWithXRefsFrom { get; set; }
public int CallCount { get; set; }
public int JumpCount { get; set; }
public int BranchCount { get; set; }
public override string ToString()
{
return $"XRefs: {TotalXRefs} total ({CallCount} calls, {JumpCount} jumps, {BranchCount} branches)";
}
}
@@ -0,0 +1,238 @@
using Yaroze.Core.CPU;
using Yaroze.Core.Disassembly;
namespace Yaroze.Core.Analysis;
/// <summary>
/// Analyzes code to discover functions, build call graphs, and analyze control flow.
/// </summary>
public class FunctionAnalyzer
{
private readonly Dictionary<uint, Function> _functions = new();
private readonly HashSet<uint> _visited = new();
private readonly byte[] _memory;
public FunctionAnalyzer(byte[] memory)
{
_memory = memory;
}
/// <summary>
/// Discover functions starting from an entry point.
/// </summary>
/// <param name="entryPoint">Entry point address</param>
public void AnalyzeFromEntryPoint(uint entryPoint)
{
// Start with entry point as a function
var entryFunc = GetOrCreateFunction(entryPoint);
entryFunc.Name = "entry";
entryFunc.IsEntryPoint = true;
// Analyze the entry function
AnalyzeFunction(entryPoint);
}
/// <summary>
/// Analyze a function starting at the given address.
/// </summary>
private void AnalyzeFunction(uint startAddress)
{
if (_visited.Contains(startAddress))
return;
var func = GetOrCreateFunction(startAddress);
var queue = new Queue<uint>();
queue.Enqueue(startAddress);
while (queue.Count > 0)
{
uint currentAddress = queue.Dequeue();
if (_visited.Contains(currentAddress))
continue;
_visited.Add(currentAddress);
// Read instruction
if (currentAddress - 0x80000000 + 4 > _memory.Length)
break;
uint instruction = BitConverter.ToUInt32(_memory, (int)(currentAddress - 0x80000000));
var instr = new Instruction(instruction);
// Add to function's instructions
func.Instructions.Add(currentAddress);
// Analyze control flow
uint opcode = instr.Opcode;
if (opcode == Opcode.JAL)
{
// Function call
uint target = instr.JumpTarget(currentAddress);
var targetFunc = GetOrCreateFunction(target);
// Add to call graph
if (!func.CallsTo.Contains(target))
{
func.CallsTo.Add(target);
targetFunc.CalledFrom.Add(startAddress);
}
// Queue target for analysis
if (!_visited.Contains(target))
{
AnalyzeFunction(target);
}
// Continue after call (fall-through)
queue.Enqueue(currentAddress + 8); // Skip delay slot
}
else if (opcode == Opcode.J)
{
// Unconditional jump
uint target = instr.JumpTarget(currentAddress);
queue.Enqueue(target);
// Don't continue after jump
}
else if (opcode == Opcode.SPECIAL && instr.Funct == Funct.JR)
{
// Jump register (usually return)
if (instr.Rs == 31) // JR $ra
{
func.HasReturn = true;
}
// Can't follow register jumps statically
}
else if (opcode >= Opcode.BEQ && opcode <= Opcode.BGTZ)
{
// Conditional branch
uint target = instr.BranchTarget(currentAddress);
queue.Enqueue(target); // Branch taken
queue.Enqueue(currentAddress + 8); // Branch not taken (skip delay slot)
}
else if (opcode == Opcode.REGIMM)
{
// BLTZ, BGEZ, BLTZAL, BGEZAL
uint target = instr.BranchTarget(currentAddress);
queue.Enqueue(target);
queue.Enqueue(currentAddress + 8);
// BLTZAL, BGEZAL are also function calls
if (instr.Rt == RegImmRt.BLTZAL || instr.Rt == RegImmRt.BGEZAL)
{
var targetFunc = GetOrCreateFunction(target);
if (!func.CallsTo.Contains(target))
{
func.CallsTo.Add(target);
targetFunc.CalledFrom.Add(startAddress);
}
}
}
else
{
// Normal instruction, continue to next
queue.Enqueue(currentAddress + 4);
}
}
}
private Function GetOrCreateFunction(uint address)
{
if (!_functions.ContainsKey(address))
{
_functions[address] = new Function
{
Address = address,
Name = $"func_{address:X8}"
};
}
return _functions[address];
}
/// <summary>
/// Get all discovered functions.
/// </summary>
public IReadOnlyDictionary<uint, Function> Functions => _functions;
/// <summary>
/// Get function at a specific address.
/// </summary>
public Function? GetFunction(uint address)
{
return _functions.GetValueOrDefault(address);
}
/// <summary>
/// Find which function contains a given address.
/// </summary>
public Function? FindContainingFunction(uint address)
{
foreach (var func in _functions.Values)
{
if (func.Instructions.Contains(address))
return func;
}
return null;
}
/// <summary>
/// Generate a call graph in DOT format.
/// </summary>
public string GenerateCallGraphDot()
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("digraph CallGraph {");
sb.AppendLine(" rankdir=LR;");
sb.AppendLine(" node [shape=box];");
foreach (var func in _functions.Values)
{
string label = func.Name ?? $"0x{func.Address:X8}";
string color = func.IsEntryPoint ? " fillcolor=lightblue style=filled" : "";
sb.AppendLine($" func_{func.Address:X8} [label=\"{label}\"{color}];");
foreach (var target in func.CallsTo)
{
sb.AppendLine($" func_{func.Address:X8} -> func_{target:X8};");
}
}
sb.AppendLine("}");
return sb.ToString();
}
}
/// <summary>
/// Represents a discovered function.
/// </summary>
public class Function
{
public uint Address { get; set; }
public string? Name { get; set; }
public bool IsEntryPoint { get; set; }
public bool HasReturn { get; set; }
public List<uint> Instructions { get; set; } = new();
public HashSet<uint> CallsTo { get; set; } = new();
public HashSet<uint> CalledFrom { get; set; } = new();
/// <summary>
/// Get the size of the function in bytes.
/// </summary>
public uint Size => (uint)(Instructions.Count * 4);
/// <summary>
/// Check if this function calls another function.
/// </summary>
public bool Calls(uint address) => CallsTo.Contains(address);
/// <summary>
/// Check if this function is called by another function.
/// </summary>
public bool IsCalledBy(uint address) => CalledFrom.Contains(address);
public override string ToString()
{
return $"{Name ?? $"0x{Address:X8}"} @ 0x{Address:X8} ({Instructions.Count} instructions)";
}
}
+170
View File
@@ -0,0 +1,170 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.Analysis;
/// <summary>
/// Simple analyzer that collects execution statistics.
/// Demonstrates the usage of IAnalysisSink for frontend integration.
/// </summary>
public class SimpleAnalyzer : IAnalysisSink
{
private readonly Dictionary<uint, int> _executionCounts = new();
private readonly HashSet<uint> _functions = new();
private readonly Dictionary<uint, List<uint>> _callGraph = new();
private readonly List<uint> _memoryAccesses = new();
public void OnInstructionExecute(InstructionInfo info)
{
// Count instruction executions
if (!_executionCounts.ContainsKey(info.PC))
{
_executionCounts[info.PC] = 0;
}
_executionCounts[info.PC]++;
}
public void OnMemoryAccess(MemoryAccessInfo access)
{
// Track unique memory addresses accessed
if (!_memoryAccesses.Contains(access.Address))
{
_memoryAccesses.Add(access.Address);
}
}
public void OnFunctionCall(uint fromPc, uint targetPc, bool isRegisterCall)
{
// Mark target as a function
_functions.Add(targetPc);
// Build call graph
if (!_callGraph.ContainsKey(fromPc))
{
_callGraph[fromPc] = new List<uint>();
}
if (!_callGraph[fromPc].Contains(targetPc))
{
_callGraph[fromPc].Add(targetPc);
}
}
public void OnFunctionReturn(uint fromPc, uint returnAddress)
{
// Function return tracking (could be used for stack analysis)
}
public void OnBranch(uint fromPc, uint targetPc, bool taken)
{
// Branch tracking (could be used for control flow analysis)
}
public void OnPossibleStringReference(uint address, string accessType)
{
// String reference tracking (useful for finding string constants)
}
/// <summary>
/// Get instruction execution counts.
/// </summary>
public IReadOnlyDictionary<uint, int> ExecutionCounts => _executionCounts;
/// <summary>
/// Get discovered function addresses.
/// </summary>
public IReadOnlySet<uint> Functions => _functions;
/// <summary>
/// Get call graph (caller → callees).
/// </summary>
public IReadOnlyDictionary<uint, List<uint>> CallGraph => _callGraph;
/// <summary>
/// Get unique memory addresses accessed.
/// </summary>
public IReadOnlyList<uint> MemoryAccesses => _memoryAccesses;
/// <summary>
/// Get statistics summary.
/// </summary>
public AnalysisStats GetStats()
{
return new AnalysisStats
{
InstructionsExecuted = _executionCounts.Values.Sum(),
UniqueInstructions = _executionCounts.Count,
FunctionsDiscovered = _functions.Count,
MemoryAddressesAccessed = _memoryAccesses.Count
};
}
/// <summary>
/// Reset all collected data.
/// </summary>
public void Reset()
{
_executionCounts.Clear();
_functions.Clear();
_callGraph.Clear();
_memoryAccesses.Clear();
}
}
/// <summary>
/// Analysis statistics.
/// </summary>
public class AnalysisStats
{
public int InstructionsExecuted { get; set; }
public int UniqueInstructions { get; set; }
public int FunctionsDiscovered { get; set; }
public int MemoryAddressesAccessed { get; set; }
public override string ToString()
{
return $"Instructions: {InstructionsExecuted}, Unique: {UniqueInstructions}, " +
$"Functions: {FunctionsDiscovered}, Memory: {MemoryAddressesAccessed}";
}
}
/// <summary>
/// Console logger that outputs trace information to stdout.
/// Useful for debugging and development.
/// </summary>
public class ConsoleTracer : ITraceSink
{
private readonly bool _verbose;
public ConsoleTracer(bool verbose = false)
{
_verbose = verbose;
}
public void TraceInstruction(uint pc, uint instruction, string? disassembly = null)
{
if (_verbose)
{
Console.WriteLine($"[0x{pc:X8}] {instruction:X8} {disassembly ?? ""}");
}
}
public void TraceMemoryRead(uint address, uint value, int size)
{
if (_verbose)
{
Console.WriteLine($" READ [0x{address:X8}] = 0x{value:X8} ({size} bytes)");
}
}
public void TraceMemoryWrite(uint address, uint value, int size)
{
if (_verbose)
{
Console.WriteLine($" WRITE [0x{address:X8}] = 0x{value:X8} ({size} bytes)");
}
}
public void TraceException(string exceptionType, uint pc)
{
Console.WriteLine($"[EXCEPTION] {exceptionType} at 0x{pc:X8}");
}
}
+227
View File
@@ -0,0 +1,227 @@
namespace Yaroze.Core.Analysis;
/// <summary>
/// Manages symbols, labels, and comments for disassembly and decompilation.
/// </summary>
public class SymbolManager
{
private readonly Dictionary<uint, Symbol> _symbols = new();
private readonly Dictionary<uint, string> _comments = new();
/// <summary>
/// Add or update a symbol.
/// </summary>
public void AddSymbol(uint address, string name, SymbolType type)
{
_symbols[address] = new Symbol
{
Address = address,
Name = name,
Type = type
};
}
/// <summary>
/// Get a symbol at a specific address.
/// </summary>
public Symbol? GetSymbol(uint address)
{
return _symbols.GetValueOrDefault(address);
}
/// <summary>
/// Remove a symbol.
/// </summary>
public bool RemoveSymbol(uint address)
{
return _symbols.Remove(address);
}
/// <summary>
/// Add or update a comment.
/// </summary>
public void AddComment(uint address, string comment)
{
_comments[address] = comment;
}
/// <summary>
/// Get a comment at a specific address.
/// </summary>
public string? GetComment(uint address)
{
return _comments.GetValueOrDefault(address);
}
/// <summary>
/// Remove a comment.
/// </summary>
public bool RemoveComment(uint address)
{
return _comments.Remove(address);
}
/// <summary>
/// Get all symbols.
/// </summary>
public IReadOnlyDictionary<uint, Symbol> Symbols => _symbols;
/// <summary>
/// Get all comments.
/// </summary>
public IReadOnlyDictionary<uint, string> Comments => _comments;
/// <summary>
/// Find symbols by name (case-insensitive).
/// </summary>
public List<Symbol> FindSymbolsByName(string name)
{
return _symbols.Values
.Where(s => s.Name.Contains(name, StringComparison.OrdinalIgnoreCase))
.ToList();
}
/// <summary>
/// Get all symbols of a specific type.
/// </summary>
public List<Symbol> GetSymbolsByType(SymbolType type)
{
return _symbols.Values
.Where(s => s.Type == type)
.OrderBy(s => s.Address)
.ToList();
}
/// <summary>
/// Import symbols from a function analyzer.
/// </summary>
public void ImportFromFunctionAnalyzer(FunctionAnalyzer analyzer)
{
foreach (var func in analyzer.Functions.Values)
{
if (!_symbols.ContainsKey(func.Address))
{
AddSymbol(func.Address, func.Name ?? $"func_{func.Address:X8}", SymbolType.Function);
}
}
}
/// <summary>
/// Export symbols to a text file format.
/// </summary>
public string ExportToText()
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("# Yaroze Symbol File");
sb.AppendLine();
sb.AppendLine("# Functions");
foreach (var symbol in GetSymbolsByType(SymbolType.Function))
{
sb.AppendLine($"F 0x{symbol.Address:X8} {symbol.Name}");
}
sb.AppendLine();
sb.AppendLine("# Labels");
foreach (var symbol in GetSymbolsByType(SymbolType.Label))
{
sb.AppendLine($"L 0x{symbol.Address:X8} {symbol.Name}");
}
sb.AppendLine();
sb.AppendLine("# Data");
foreach (var symbol in GetSymbolsByType(SymbolType.Data))
{
sb.AppendLine($"D 0x{symbol.Address:X8} {symbol.Name}");
}
if (_comments.Count > 0)
{
sb.AppendLine();
sb.AppendLine("# Comments");
foreach (var (address, comment) in _comments.OrderBy(kv => kv.Key))
{
sb.AppendLine($"C 0x{address:X8} {comment}");
}
}
return sb.ToString();
}
/// <summary>
/// Import symbols from a text file format.
/// </summary>
public void ImportFromText(string text)
{
var lines = text.Split('\n');
foreach (var line in lines)
{
var trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#'))
continue;
var parts = trimmed.Split(' ', 3);
if (parts.Length < 3)
continue;
var type = parts[0];
if (!uint.TryParse(parts[1].Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out uint address))
continue;
var name = parts[2];
switch (type)
{
case "F":
AddSymbol(address, name, SymbolType.Function);
break;
case "L":
AddSymbol(address, name, SymbolType.Label);
break;
case "D":
AddSymbol(address, name, SymbolType.Data);
break;
case "C":
AddComment(address, name);
break;
}
}
}
/// <summary>
/// Clear all symbols and comments.
/// </summary>
public void Clear()
{
_symbols.Clear();
_comments.Clear();
}
}
/// <summary>
/// Represents a symbol in the program.
/// </summary>
public class Symbol
{
public uint Address { get; set; }
public string Name { get; set; } = "";
public SymbolType Type { get; set; }
public override string ToString()
{
return $"{Name} @ 0x{Address:X8} ({Type})";
}
}
/// <summary>
/// Types of symbols.
/// </summary>
public enum SymbolType
{
Function,
Label,
Data,
String,
Unknown
}
+438
View File
@@ -0,0 +1,438 @@
using System;
using System.Collections.Generic;
using Yaroze.Core.Interfaces;
using Yaroze.Core.Interrupts;
namespace Yaroze.Core.CDROM;
/// <summary>
/// PlayStation 1 CD-ROM drive controller.
/// Handles disc reading, seeking, and command processing.
/// </summary>
public class CdRomDevice : IBusDevice
{
private readonly InterruptController _interruptController;
// CD-ROM state
private DiscImage? _disc;
private int _currentSector;
private byte _indexRegister;
// Response and data FIFOs
private readonly Queue<byte> _responseFifo = new();
private readonly Queue<byte> _dataFifo = new();
private readonly Queue<byte> _parameterFifo = new();
// Sector buffer (2352 bytes for full CD-ROM sector)
private readonly byte[] _sectorBuffer = new byte[2352];
private int _sectorBufferIndex;
// Status flags
private bool _busyReading;
private byte _interruptFlags;
private byte _interruptEnable;
public CdRomDevice(InterruptController interruptController)
{
_interruptController = interruptController;
Reset();
}
/// <summary>
/// Loads a disc image into the CD-ROM drive.
/// </summary>
public void LoadDisc(string path)
{
_disc?.Dispose();
_disc = DiscImage.Load(path);
_currentSector = 0;
}
/// <summary>
/// Resets the CD-ROM drive to power-on state.
/// </summary>
public void Reset()
{
_indexRegister = 0;
_responseFifo.Clear();
_dataFifo.Clear();
_parameterFifo.Clear();
_currentSector = 0;
_busyReading = false;
_interruptFlags = 0;
_interruptEnable = 0;
_sectorBufferIndex = 0;
}
#region IBusDevice Implementation
public bool Contains(uint address)
{
// CD-ROM registers at 0x1F801800-0x1F801803
return address >= 0x1F801800 && address <= 0x1F801803;
}
public uint Read32(uint address)
{
uint offset = address - 0x1F801800;
// For DMA transfers, reading from offset 2 (data FIFO) returns 32-bit word
if (offset == 2)
{
return ReadDmaWord();
}
// Other registers are byte-sized
return Read8(address);
}
public ushort Read16(uint address)
{
return Read8(address);
}
public byte Read8(uint address)
{
uint offset = address - 0x1F801800;
return offset switch
{
0 => ReadStatus(), // CD_REG0: Status register
1 => ReadResponseFifo(), // CD_REG1: Response FIFO
2 => ReadDataFifo(), // CD_REG2: Data FIFO
3 => ReadInterruptFlags(), // CD_REG3: Interrupt flags (when index=1)
_ => 0xFF
};
}
public void Write32(uint address, uint value)
{
Write8(address, (byte)value);
}
public void Write16(uint address, ushort value)
{
Write8(address, (byte)value);
}
public void Write8(uint address, byte value)
{
uint offset = address - 0x1F801800;
switch (offset)
{
case 0: // CD_REG0: Index/Status register
_indexRegister = (byte)(value & 0x03);
break;
case 1: // CD_REG1: Command register (when index=0)
if (_indexRegister == 0)
{
ExecuteCommand(value);
}
break;
case 2: // CD_REG2: Parameter FIFO (when index=0) / Interrupt enable (when index=1)
if (_indexRegister == 0)
{
_parameterFifo.Enqueue(value);
}
else if (_indexRegister == 1)
{
_interruptEnable = value;
}
break;
case 3: // CD_REG3: Request register (when index=0) / Interrupt ack (when index=1)
if (_indexRegister == 1)
{
// Acknowledge interrupts by writing 1 bits
_interruptFlags &= (byte)~value;
if (_interruptFlags == 0)
{
_interruptController.ClearInterrupt(InterruptType.CdRom);
}
}
break;
}
}
#endregion
#region Register Reads
private byte ReadStatus()
{
byte status = _indexRegister;
// Bit 3: Parameter FIFO empty (0=empty, 1=not empty)
if (_parameterFifo.Count > 0)
status |= 0x08;
// Bit 5: Response FIFO not empty (0=empty, 1=not empty)
if (_responseFifo.Count > 0)
status |= 0x20;
// Bit 6: Data FIFO not empty
if (_dataFifo.Count > 0)
status |= 0x40;
// Bit 7: Busy flag
if (_busyReading)
status |= 0x80;
return status;
}
private byte ReadResponseFifo()
{
if (_responseFifo.Count > 0)
return _responseFifo.Dequeue();
return 0xFF;
}
private byte ReadDataFifo()
{
if (_dataFifo.Count > 0)
return _dataFifo.Dequeue();
return 0;
}
private byte ReadInterruptFlags()
{
// Bits 0-2: Interrupt flags
// Bit 3-4: Unused
// Bit 5: Interrupt enable flag
return (byte)((_interruptFlags & 0x07) | 0xE0);
}
#endregion
#region Command Processing
private void ExecuteCommand(byte command)
{
switch (command)
{
case 0x01: // GetStat
CmdGetStat();
break;
case 0x02: // SetLoc
CmdSetLoc();
break;
case 0x06: // ReadN (with retry)
CmdReadN();
break;
case 0x0E: // SetMode
CmdSetMode();
break;
case 0x15: // SeekL (seek to location)
CmdSeekL();
break;
case 0x19: // Test
CmdTest();
break;
case 0x1A: // GetID
CmdGetID();
break;
default:
// Unknown command - send error response
_responseFifo.Enqueue(0x11); // Error, motor on
TriggerInterrupt(0x05); // Error
break;
}
_parameterFifo.Clear();
}
private void CmdGetStat()
{
// Return status byte: Motor on, not seeking, not reading, not playing audio
byte stat = 0x02; // Motor on
_responseFifo.Enqueue(stat);
TriggerInterrupt(0x03); // Complete
}
private void CmdSetLoc()
{
// Parameters: MM, SS, FF (BCD format)
if (_parameterFifo.Count >= 3)
{
byte mm = _parameterFifo.Dequeue();
byte ss = _parameterFifo.Dequeue();
byte ff = _parameterFifo.Dequeue();
// Convert BCD to binary
int minutes = BcdToBinary(mm);
int seconds = BcdToBinary(ss);
int frames = BcdToBinary(ff);
// Calculate LBA (Logical Block Address)
// CD-ROM sectors are addressed in MSF format, but internally we use LBA
_currentSector = (minutes * 60 + seconds) * 75 + frames - 150; // Subtract 2-second pregap
_responseFifo.Enqueue(0x02); // Motor on
TriggerInterrupt(0x03); // Complete
}
}
private void CmdReadN()
{
// Start reading sectors
_busyReading = true;
// Read first sector immediately
if (_disc != null)
{
ReadCurrentSector();
}
_responseFifo.Enqueue(0x02); // Motor on, reading
TriggerInterrupt(0x03); // Complete
}
private void CmdSetMode()
{
// Parameter: mode flags
if (_parameterFifo.Count > 0)
{
_parameterFifo.Dequeue(); // Mode - we'll ignore for now
}
_responseFifo.Enqueue(0x02); // Motor on
TriggerInterrupt(0x03); // Complete
}
private void CmdSeekL()
{
// Seek to previously set location (from SetLoc)
_responseFifo.Enqueue(0x02); // Motor on
TriggerInterrupt(0x03); // Complete
// Second response after seek complete
_responseFifo.Enqueue(0x02); // Motor on
TriggerInterrupt(0x02); // Seek complete
}
private void CmdTest()
{
// Test command - sub-function in parameter
if (_parameterFifo.Count > 0)
{
byte subFunction = _parameterFifo.Dequeue();
if (subFunction == 0x20) // Get BIOS version
{
// Return version bytes
_responseFifo.Enqueue(0x94); // Year 1994
_responseFifo.Enqueue(0x09); // Month September
_responseFifo.Enqueue(0x19); // Day 19
_responseFifo.Enqueue(0xC0); // Version
TriggerInterrupt(0x03); // Complete
}
}
}
private void CmdGetID()
{
// Return disc ID information
_responseFifo.Enqueue(0x02); // Motor on
TriggerInterrupt(0x03); // Complete
// Second response with disc type
_responseFifo.Enqueue(0x02); // Motor on, lid closed
_responseFifo.Enqueue(0x00); // Licensed, not audio
_responseFifo.Enqueue(0x20); // Disc type (PlayStation)
_responseFifo.Enqueue(0x00); // ATIP (session info)
_responseFifo.Enqueue(0x53); // SCEx string "SCEI" for Japan
_responseFifo.Enqueue(0x43);
_responseFifo.Enqueue(0x45);
_responseFifo.Enqueue(0x49);
TriggerInterrupt(0x02); // Complete with info
}
private int BcdToBinary(byte bcd)
{
return (bcd >> 4) * 10 + (bcd & 0x0F);
}
#endregion
#region Sector Reading
private void ReadCurrentSector()
{
if (_disc == null)
return;
try
{
// Read sector data
int bytesRead = _disc.ReadSectorUserData(_currentSector, _sectorBuffer, 0);
// Fill data FIFO
_dataFifo.Clear();
for (int i = 0; i < bytesRead && i < 2048; i++)
{
_dataFifo.Enqueue(_sectorBuffer[i]);
}
// Move to next sector
_currentSector++;
// Trigger data ready interrupt
TriggerInterrupt(0x01); // Data ready
}
catch
{
// Read error
_responseFifo.Enqueue(0x11); // Error
TriggerInterrupt(0x05); // Error
_busyReading = false;
}
}
/// <summary>
/// Called by DMA controller to read data from CD-ROM.
/// </summary>
public uint ReadDmaWord()
{
// Read 4 bytes from data FIFO
uint word = 0;
for (int i = 0; i < 4; i++)
{
byte b = _dataFifo.Count > 0 ? _dataFifo.Dequeue() : (byte)0;
word |= (uint)(b << (i * 8));
}
// If FIFO is getting low and we're still reading, load next sector
if (_dataFifo.Count < 512 && _busyReading && _disc != null)
{
ReadCurrentSector();
}
return word;
}
#endregion
#region Interrupts
private void TriggerInterrupt(byte interruptType)
{
_interruptFlags = interruptType;
if ((_interruptEnable & interruptType) != 0)
{
_interruptController.RaiseInterrupt(InterruptType.CdRom);
}
}
#endregion
}
+138
View File
@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace Yaroze.Core.CDROM;
/// <summary>
/// Parses and represents a CUE sheet file for CD-ROM images.
/// </summary>
public class CueSheet
{
/// <summary>
/// Gets the list of tracks defined in this cue sheet.
/// </summary>
public List<Track> Tracks { get; } = new();
/// <summary>
/// Gets the directory containing the cue sheet file.
/// </summary>
public string BaseDirectory { get; private set; } = string.Empty;
/// <summary>
/// Parses a CUE sheet from a file.
/// </summary>
/// <param name="cueFilePath">Path to the .cue file.</param>
/// <returns>A parsed CueSheet object.</returns>
public static CueSheet FromFile(string cueFilePath)
{
var cueSheet = new CueSheet
{
BaseDirectory = Path.GetDirectoryName(cueFilePath) ?? string.Empty
};
var lines = File.ReadAllLines(cueFilePath);
Track? currentTrack = null;
string? currentFile = null;
string? currentFileType = null;
long currentFileOffset = 0;
foreach (var rawLine in lines)
{
var line = rawLine.Trim();
if (string.IsNullOrWhiteSpace(line))
continue;
// FILE command
if (line.StartsWith("FILE", StringComparison.OrdinalIgnoreCase))
{
var match = Regex.Match(line, @"FILE\s+""([^""]+)""\s+(\w+)", RegexOptions.IgnoreCase);
if (match.Success)
{
currentFile = match.Groups[1].Value;
currentFileType = match.Groups[2].Value;
currentFileOffset = 0; // Reset offset for new file
}
continue;
}
// TRACK command
if (line.StartsWith("TRACK", StringComparison.OrdinalIgnoreCase))
{
var match = Regex.Match(line, @"TRACK\s+(\d+)\s+(.+)", RegexOptions.IgnoreCase);
if (match.Success)
{
int trackNumber = int.Parse(match.Groups[1].Value);
string modeString = match.Groups[2].Value;
TrackMode mode = ParseTrackMode(modeString);
if (currentFile == null || currentFileType == null)
throw new InvalidDataException("TRACK command found before FILE command in CUE sheet");
currentTrack = new Track(trackNumber, mode, currentFile, currentFileType, currentFileOffset);
cueSheet.Tracks.Add(currentTrack);
}
continue;
}
// INDEX command
if (line.StartsWith("INDEX", StringComparison.OrdinalIgnoreCase))
{
var match = Regex.Match(line, @"INDEX\s+(\d+)\s+(\d+):(\d+):(\d+)", RegexOptions.IgnoreCase);
if (match.Success && currentTrack != null)
{
int indexNumber = int.Parse(match.Groups[1].Value);
int minutes = int.Parse(match.Groups[2].Value);
int seconds = int.Parse(match.Groups[3].Value);
int frames = int.Parse(match.Groups[4].Value);
int totalFrames = (minutes * 60 + seconds) * 75 + frames;
if (indexNumber == 0)
{
currentTrack.Index00 = totalFrames;
}
else if (indexNumber == 1)
{
currentTrack.Index01 = totalFrames;
// Calculate file offset for this track
currentFileOffset = (long)totalFrames * currentTrack.SectorSize;
}
}
continue;
}
// Other commands (PREGAP, POSTGAP, TITLE, etc.) can be ignored for basic functionality
}
if (cueSheet.Tracks.Count == 0)
throw new InvalidDataException("No tracks found in CUE sheet");
return cueSheet;
}
/// <summary>
/// Parses a track mode string from the CUE file.
/// </summary>
private static TrackMode ParseTrackMode(string modeString)
{
return modeString.ToUpperInvariant() switch
{
"AUDIO" => TrackMode.Audio,
"MODE1/2048" => TrackMode.Mode1_2048,
"MODE1/2352" => TrackMode.Mode1_2352,
"MODE2/2336" => TrackMode.Mode2_2336,
"MODE2/2352" => TrackMode.Mode2_2352,
_ => throw new NotSupportedException($"Unsupported track mode: {modeString}")
};
}
/// <summary>
/// Gets the full path to a data file referenced in the cue sheet.
/// </summary>
public string GetFullPath(string fileName)
{
return Path.Combine(BaseDirectory, fileName);
}
}
+219
View File
@@ -0,0 +1,219 @@
using System;
using System.IO;
namespace Yaroze.Core.CDROM;
/// <summary>
/// Represents a PlayStation 1 CD-ROM disc image.
/// Supports ISO and BIN formats with optional CUE sheets.
/// </summary>
public class DiscImage : IDisposable
{
private CueSheet? _cueSheet;
private FileStream? _fileStream;
private bool _disposed;
/// <summary>
/// Gets the tracks in this disc image.
/// </summary>
public Track[] Tracks { get; private set; } = Array.Empty<Track>();
/// <summary>
/// Gets the path to the loaded disc image file.
/// </summary>
public string ImagePath { get; private set; } = string.Empty;
/// <summary>
/// Loads a disc image from a file.
/// Automatically detects and loads .cue files if present.
/// </summary>
/// <param name="path">Path to .iso, .bin, or .cue file.</param>
/// <returns>A loaded DiscImage.</returns>
public static DiscImage Load(string path)
{
if (!File.Exists(path))
throw new FileNotFoundException($"Disc image file not found: {path}");
var extension = Path.GetExtension(path).ToLowerInvariant();
var disc = new DiscImage();
if (extension == ".cue")
{
// Load from CUE sheet
disc.LoadFromCue(path);
}
else if (extension == ".bin" || extension == ".iso")
{
// Try to find matching CUE file
var cueFile = Path.ChangeExtension(path, ".cue");
if (File.Exists(cueFile))
{
disc.LoadFromCue(cueFile);
}
else
{
// Load as standalone raw disc image
disc.LoadRaw(path);
}
}
else
{
throw new NotSupportedException($"Unsupported disc image format: {extension}");
}
return disc;
}
/// <summary>
/// Loads a disc image from a CUE sheet.
/// </summary>
private void LoadFromCue(string cuePath)
{
_cueSheet = CueSheet.FromFile(cuePath);
Tracks = _cueSheet.Tracks.ToArray();
// Open the first data track's file (typically track 1)
if (Tracks.Length > 0)
{
var firstTrack = Tracks[0];
ImagePath = _cueSheet.GetFullPath(firstTrack.FileName);
_fileStream = new FileStream(ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
}
/// <summary>
/// Loads a raw disc image without a CUE sheet.
/// Assumes single track, MODE2/2352 format.
/// </summary>
private void LoadRaw(string path)
{
ImagePath = path;
_fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
// Create a single track spanning the entire file
var singleTrack = new Track(1, TrackMode.Mode2_2352, Path.GetFileName(path), "BINARY", 0)
{
Index01 = 0
};
Tracks = new[] { singleTrack };
}
/// <summary>
/// Reads a sector from the disc image.
/// </summary>
/// <param name="lba">Logical Block Address (sector number).</param>
/// <param name="buffer">Buffer to read into.</param>
/// <param name="offset">Offset in buffer to write to.</param>
/// <returns>Number of bytes read.</returns>
public int ReadSector(int lba, byte[] buffer, int offset = 0)
{
if (_fileStream == null)
throw new InvalidOperationException("No disc image loaded");
// Find the track containing this LBA
Track? track = FindTrackForLba(lba);
if (track == null)
throw new ArgumentOutOfRangeException(nameof(lba), $"LBA {lba} is outside the disc");
// Calculate file offset
long fileOffset = (long)lba * track.SectorSize;
_fileStream.Seek(fileOffset, SeekOrigin.Begin);
return _fileStream.Read(buffer, offset, track.SectorSize);
}
/// <summary>
/// Reads user data from a sector (extracts payload, skips headers/ECC).
/// </summary>
/// <param name="lba">Logical Block Address.</param>
/// <param name="buffer">Buffer to read into.</param>
/// <param name="offset">Offset in buffer.</param>
/// <returns>Number of bytes of user data read.</returns>
public int ReadSectorUserData(int lba, byte[] buffer, int offset = 0)
{
Track? track = FindTrackForLba(lba);
if (track == null)
throw new ArgumentOutOfRangeException(nameof(lba));
if (track.Mode == TrackMode.Mode2_2352)
{
// Read full sector into temp buffer
byte[] sectorBuffer = new byte[2352];
ReadSector(lba, sectorBuffer, 0);
// MODE2/2352: Skip 16-byte header (sync + address + mode)
// Some images may have 24-byte header (sync + header + subheader)
// For now, skip first 24 bytes to get to user data
int headerSize = 24;
int userDataSize = 2048; // Standard Mode 2 Form 1 user data
Array.Copy(sectorBuffer, headerSize, buffer, offset, userDataSize);
return userDataSize;
}
else if (track.Mode == TrackMode.Mode1_2352)
{
// Read full sector
byte[] sectorBuffer = new byte[2352];
ReadSector(lba, sectorBuffer, 0);
// MODE1/2352: Skip 16-byte header, take 2048 bytes user data
Array.Copy(sectorBuffer, 16, buffer, offset, 2048);
return 2048;
}
else if (track.Mode == TrackMode.Mode1_2048)
{
// Already just user data
return ReadSector(lba, buffer, offset);
}
else
{
// For other modes, return raw sector data
return ReadSector(lba, buffer, offset);
}
}
/// <summary>
/// Finds the track that contains the specified LBA.
/// </summary>
private Track? FindTrackForLba(int lba)
{
// For single-track images, always use track 1
if (Tracks.Length == 1)
return Tracks[0];
// For multi-track CUE sheets, find the appropriate track
for (int i = 0; i < Tracks.Length; i++)
{
var track = Tracks[i];
var nextTrackStart = i + 1 < Tracks.Length ? Tracks[i + 1].Index01 : int.MaxValue;
if (lba >= track.Index01 && lba < nextTrackStart)
return track;
}
return null;
}
/// <summary>
/// Gets the total number of sectors in the disc.
/// </summary>
public int GetSectorCount()
{
if (_fileStream == null)
return 0;
var lastTrack = Tracks[^1];
return (int)(_fileStream.Length / lastTrack.SectorSize);
}
public void Dispose()
{
if (!_disposed)
{
_fileStream?.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}
+77
View File
@@ -0,0 +1,77 @@
namespace Yaroze.Core.CDROM;
/// <summary>
/// Represents a single track in a CD-ROM image.
/// </summary>
public class Track
{
/// <summary>
/// Gets the track number (1-99).
/// </summary>
public int Number { get; init; }
/// <summary>
/// Gets the track mode (AUDIO, MODE1/2048, MODE2/2352, etc.).
/// </summary>
public TrackMode Mode { get; init; }
/// <summary>
/// Gets the file containing this track's data.
/// </summary>
public string FileName { get; init; }
/// <summary>
/// Gets the type of file (BINARY, WAVE, MP3, etc.).
/// </summary>
public string FileType { get; init; }
/// <summary>
/// Gets the index 00 position (pregap) in frames, if specified.
/// </summary>
public int? Index00 { get; set; }
/// <summary>
/// Gets the index 01 position (start of track) in frames.
/// </summary>
public int Index01 { get; set; }
/// <summary>
/// Gets the byte offset in the file where this track starts.
/// </summary>
public long FileOffset { get; init; }
/// <summary>
/// Gets the sector size for this track in bytes.
/// </summary>
public int SectorSize => Mode switch
{
TrackMode.Audio => 2352,
TrackMode.Mode1_2048 => 2048,
TrackMode.Mode1_2352 => 2352,
TrackMode.Mode2_2336 => 2336,
TrackMode.Mode2_2352 => 2352,
_ => 2352
};
/// <summary>
/// Gets the user data size per sector (excludes sync, header, EDC/ECC).
/// </summary>
public int UserDataSize => Mode switch
{
TrackMode.Mode1_2048 => 2048,
TrackMode.Mode1_2352 => 2048, // Extract from raw sector
TrackMode.Mode2_2336 => 2336,
TrackMode.Mode2_2352 => 2352,
TrackMode.Audio => 2352,
_ => 2048
};
public Track(int number, TrackMode mode, string fileName, string fileType, long fileOffset = 0)
{
Number = number;
Mode = mode;
FileName = fileName;
FileType = fileType;
FileOffset = fileOffset;
}
}
+34
View File
@@ -0,0 +1,34 @@
namespace Yaroze.Core.CDROM;
/// <summary>
/// Represents the data mode of a CD-ROM track.
/// </summary>
public enum TrackMode
{
/// <summary>
/// Audio track (Red Book audio, 2352 bytes/sector).
/// </summary>
Audio,
/// <summary>
/// CD-ROM Mode 1 data with 2048 bytes of user data per sector (cooked).
/// Includes sync, header, user data, EDC, and ECC (total 2352 bytes).
/// </summary>
Mode1_2048,
/// <summary>
/// CD-ROM Mode 1 data with full 2352 bytes per sector (raw).
/// </summary>
Mode1_2352,
/// <summary>
/// CD-ROM XA Mode 2 data with 2336 bytes per sector.
/// </summary>
Mode2_2336,
/// <summary>
/// CD-ROM XA Mode 2 data with full 2352 bytes per sector (raw).
/// Most common format for PlayStation 1 disc images.
/// </summary>
Mode2_2352
}
+281
View File
@@ -0,0 +1,281 @@
namespace Yaroze.Core.CPU;
/// <summary>
/// COP0 (System Control Coprocessor) for MIPS R3000A.
/// Handles exceptions, interrupts, and system status.
/// </summary>
public class Coprocessor0
{
// COP0 Registers
private readonly uint[] _registers = new uint[32];
// Register indices
private const int BPC = 3; // Breakpoint on execute (R/W)
private const int BDA = 5; // Breakpoint on data access (R/W)
private const int JUMPDEST = 6; // Randomly memorized jump address (R)
private const int DCIC = 7; // Breakpoint control (R/W)
private const int BADVADDR = 8; // Bad Virtual Address (R)
private const int BDAM = 9; // Data Access breakpoint mask (R/W)
private const int BPCM = 11; // Execute breakpoint mask (R/W)
private const int SR = 12; // Status Register (R/W)
private const int CAUSE = 13; // Cause Register (R)
private const int EPC = 14; // Exception Program Counter (R)
private const int PRID = 15; // Processor ID (R)
// Status Register (SR) bit layout
private const uint SR_IEc = 0x00000001; // Current Interrupt Enable
private const uint SR_KUc = 0x00000002; // Current Kernel/User mode
private const uint SR_IEp = 0x00000004; // Previous Interrupt Enable
private const uint SR_KUp = 0x00000008; // Previous Kernel/User mode
private const uint SR_IEo = 0x00000010; // Old Interrupt Enable
private const uint SR_KUo = 0x00000020; // Old Kernel/User mode
private const uint SR_Im = 0x0000FF00; // Interrupt Mask (bits 8-15)
private const uint SR_Isc = 0x00010000; // Isolate Cache
private const uint SR_BEV = 0x00400000; // Bootstrap Exception Vector
private const uint SR_CU = 0xF0000000; // Coprocessor Usable bits
// Cause Register (CAUSE) bit layout
private const uint CAUSE_ExcCode = 0x0000007C; // Exception Code (bits 2-6)
private const uint CAUSE_IP = 0x0000FF00; // Interrupt Pending (bits 8-15)
private const uint CAUSE_BD = 0x80000000; // Branch Delay
public Coprocessor0()
{
Reset();
}
/// <summary>
/// Reset COP0 to power-on state.
/// </summary>
public void Reset()
{
Array.Clear(_registers, 0, _registers.Length);
// Set initial SR: BEV=1 (use bootstrap vectors), CU0=1 (COP0 usable)
_registers[SR] = SR_BEV | 0x10000000; // CU0 = 1
// Set Processor ID (2 = R3000A)
_registers[PRID] = 0x00000002;
}
/// <summary>
/// Read a COP0 register.
/// </summary>
public uint ReadRegister(uint index)
{
if (index >= 32)
return 0;
// Some registers are read-only or have special behavior
return _registers[index];
}
/// <summary>
/// Write a COP0 register.
/// </summary>
public void WriteRegister(uint index, uint value)
{
if (index >= 32)
return;
switch (index)
{
case SR:
// Status Register is R/W
_registers[SR] = value;
break;
case CAUSE:
// Only software interrupt bits (IP0, IP1) are writable
_registers[CAUSE] = (_registers[CAUSE] & ~0x00000300u) | (value & 0x00000300u);
break;
case BPC:
case BDA:
case DCIC:
case BDAM:
case BPCM:
// Breakpoint registers are R/W
_registers[index] = value;
break;
// Read-only registers
case BADVADDR:
case EPC:
case PRID:
case JUMPDEST:
// Ignore writes
break;
default:
// Other registers: allow write for now
_registers[index] = value;
break;
}
}
/// <summary>
/// Get the Status Register.
/// </summary>
public uint StatusRegister => _registers[SR];
/// <summary>
/// Get the Cause Register.
/// </summary>
public uint CauseRegister => _registers[CAUSE];
/// <summary>
/// Get the Exception Program Counter.
/// </summary>
public uint ExceptionPC => _registers[EPC];
/// <summary>
/// Check if interrupts are enabled.
/// </summary>
public bool InterruptsEnabled => (StatusRegister & SR_IEc) != 0;
/// <summary>
/// Check if using bootstrap exception vectors.
/// </summary>
public bool BootstrapExceptionVectors => (StatusRegister & SR_BEV) != 0;
/// <summary>
/// Get the exception vector address for the given exception.
/// </summary>
public uint GetExceptionVector()
{
// All exceptions go to the same vector in PS1
// BEV=0: 0x80000080 (RAM), BEV=1: 0xBFC00180 (BIOS)
return BootstrapExceptionVectors ? 0xBFC00180u : 0x80000080u;
}
/// <summary>
/// Set an exception and update COP0 state.
/// </summary>
/// <param name="exceptionCode">Exception code (see ExceptionCode enum)</param>
/// <param name="pc">Current PC</param>
/// <param name="inDelaySlot">Whether exception occurred in branch delay slot</param>
/// <param name="badAddress">Bad virtual address (for address errors)</param>
public void SetException(ExceptionCode exceptionCode, uint pc, bool inDelaySlot, uint? badAddress = null)
{
// Save the PC (or PC-4 if in delay slot)
_registers[EPC] = inDelaySlot ? pc - 4 : pc;
// Set exception code
uint cause = _registers[CAUSE];
cause &= ~CAUSE_ExcCode;
cause |= ((uint)exceptionCode << 2) & CAUSE_ExcCode;
// Set BD flag if in delay slot
if (inDelaySlot)
cause |= CAUSE_BD;
else
cause &= ~CAUSE_BD;
_registers[CAUSE] = cause;
// Save bad address for address errors
if (badAddress.HasValue)
{
_registers[BADVADDR] = badAddress.Value;
}
// Push interrupt enable stack in SR
uint sr = _registers[SR];
// (KUc, IEc) → (KUp, IEp)
uint kuc = sr & SR_KUc;
uint iec = sr & SR_IEc;
sr = (sr & ~(SR_KUp | SR_IEp)) | ((kuc << 2) | (iec << 2));
// (KUp, IEp) → (KUo, IEo)
uint kup = (sr & SR_KUp) >> 2;
uint iep = (sr & SR_IEp) >> 2;
sr = (sr & ~(SR_KUo | SR_IEo)) | ((kup << 2) | (iep << 2));
// Set KUc=0 (kernel mode), IEc=0 (interrupts disabled)
sr &= ~(SR_KUc | SR_IEc);
_registers[SR] = sr;
}
/// <summary>
/// Return from exception (RFE instruction).
/// Pops the interrupt enable stack.
/// </summary>
public void ReturnFromException()
{
uint sr = _registers[SR];
// (KUp, IEp) → (KUc, IEc)
uint kup = sr & SR_KUp;
uint iep = sr & SR_IEp;
sr = (sr & ~(SR_KUc | SR_IEc)) | ((kup >> 2) | (iep >> 2));
// (KUo, IEo) → (KUp, IEp)
uint kuo = sr & SR_KUo;
uint ieo = sr & SR_IEo;
sr = (sr & ~(SR_KUp | SR_IEp)) | ((kuo >> 2) | (ieo >> 2));
_registers[SR] = sr;
}
/// <summary>
/// Raise an interrupt.
/// </summary>
/// <param name="irq">IRQ number (0-7 for hardware interrupts)</param>
public void RaiseInterrupt(int irq)
{
if (irq < 0 || irq >= 8)
return;
uint mask = 1u << (8 + irq);
_registers[CAUSE] |= mask;
}
/// <summary>
/// Clear an interrupt.
/// </summary>
public void ClearInterrupt(int irq)
{
if (irq < 0 || irq >= 8)
return;
uint mask = 1u << (8 + irq);
_registers[CAUSE] &= ~mask;
}
/// <summary>
/// Check if any interrupts are pending and enabled.
/// </summary>
public bool HasPendingInterrupt()
{
if (!InterruptsEnabled)
return false;
uint pending = (_registers[CAUSE] & CAUSE_IP) >> 8;
uint mask = (_registers[SR] & SR_Im) >> 8;
return (pending & mask) != 0;
}
}
/// <summary>
/// MIPS exception codes.
/// </summary>
public enum ExceptionCode
{
Interrupt = 0x00, // Int - External interrupt
TLBModification = 0x01, // Mod - TLB modification (not used in PS1)
TLBLoadMiss = 0x02, // TLBL - TLB miss on load (not used in PS1)
TLBStoreMiss = 0x03, // TLBS - TLB miss on store (not used in PS1)
AddressErrorLoad = 0x04, // AdEL - Address error on load
AddressErrorStore = 0x05, // AdES - Address error on store
BusErrorInstruction = 0x06, // IBE - Bus error on instruction fetch
BusErrorData = 0x07, // DBE - Bus error on data access
Syscall = 0x08, // Sys - SYSCALL instruction
Breakpoint = 0x09, // Bp - BREAK instruction
ReservedInstruction = 0x0A, // RI - Reserved/illegal instruction
CoprocessorUnusable = 0x0B, // CpU - Coprocessor unusable
Overflow = 0x0C // Ov - Arithmetic overflow
}
File diff suppressed because it is too large Load Diff
+210
View File
@@ -0,0 +1,210 @@
namespace Yaroze.Core.CPU;
/// <summary>
/// GTE (Geometry Transformation Engine) - COP2 for PlayStation 1.
///
/// This is a minimal implementation that provides register access and basic command
/// execution. Full GTE calculations (matrix transforms, perspective projection, lighting)
/// are deferred as they require complex fixed-point arithmetic and are not needed for
/// many homebrew games and basic emulation.
///
/// The current implementation allows games to:
/// - Read/write GTE registers without crashing
/// - Execute GTE commands which clear the FLAG register
/// - Use basic register transfer operations (MFC2, MTC2, CFC2, CTC2)
/// </summary>
public class Gte
{
// GTE has 32 data registers and 32 control registers
private readonly uint[] _dataRegisters = new uint[32];
private readonly uint[] _controlRegisters = new uint[32];
// FLAG register (data register 31) - stores operation flags
private const int FLAG_REGISTER = 31;
public Gte()
{
Reset();
}
/// <summary>
/// Reset GTE to initial state.
/// </summary>
public void Reset()
{
Array.Clear(_dataRegisters, 0, _dataRegisters.Length);
Array.Clear(_controlRegisters, 0, _controlRegisters.Length);
}
/// <summary>
/// Read a GTE data register.
/// </summary>
public uint ReadDataRegister(uint index)
{
if (index >= 32)
return 0;
return _dataRegisters[index];
}
/// <summary>
/// Write a GTE data register.
/// </summary>
public void WriteDataRegister(uint index, uint value)
{
if (index >= 32)
return;
_dataRegisters[index] = value;
}
/// <summary>
/// Read a GTE control register.
/// </summary>
public uint ReadControlRegister(uint index)
{
if (index >= 32)
return 0;
return _controlRegisters[index];
}
/// <summary>
/// Write a GTE control register.
/// </summary>
public void WriteControlRegister(uint index, uint value)
{
if (index >= 32)
return;
_controlRegisters[index] = value;
}
/// <summary>
/// Execute a GTE command.
///
/// Minimal implementation that clears the FLAG register to indicate successful
/// completion. This allows games to execute GTE commands without crashing.
///
/// Full implementations would include:
/// - RTPS/RTPT: Perspective transformation (3D to 2D projection)
/// - MVMVA: Matrix-vector multiply-add
/// - NCDS/NCDT/NCCS/NCCT: Normal color depth/triple shading
/// - DPCS/DPCT: Depth cue single/triple
/// - INTPL: Interpolation
/// - SQR: Square calculation
/// - AVSZ3/AVSZ4: Average Z calculation
/// - OP: Outer product
/// - GPF/GPL: General purpose interpolation
///
/// These operations involve complex fixed-point arithmetic and are typically only
/// needed for commercial 3D games. Homebrew and 2D games work fine without them.
/// </summary>
/// <param name="command">25-bit GTE command code</param>
public void ExecuteCommand(uint command)
{
// Extract command opcode from bits 0-5
uint opcode = command & 0x3F;
// Extract sf bit (bit 19) - shift fraction in calculation
bool sf = (command & 0x80000) != 0;
// Extract lm bit (bit 10) - limit negative results to 0
bool lm = (command & 0x400) != 0;
// Minimal implementation: Clear FLAG register to indicate no errors
// Games check FLAG after GTE operations to detect calculation errors
_dataRegisters[FLAG_REGISTER] = 0;
// Note: Full GTE implementation would perform the actual calculation here
// based on the opcode and store results in data registers. This minimal
// version allows basic compatibility without the complexity of fixed-point
// 3D math operations.
}
/// <summary>
/// Get GTE register name for debugging.
/// </summary>
public static string GetDataRegisterName(uint index)
{
return index switch
{
0 => "VXY0", // Vector 0 X,Y
1 => "VZ0", // Vector 0 Z
2 => "VXY1", // Vector 1 X,Y
3 => "VZ1", // Vector 1 Z
4 => "VXY2", // Vector 2 X,Y
5 => "VZ2", // Vector 2 Z
6 => "RGBC", // Color/code value
7 => "OTZ", // Ordering table Z
8 => "IR0", // Intermediate value 0
9 => "IR1", // Intermediate value 1
10 => "IR2", // Intermediate value 2
11 => "IR3", // Intermediate value 3
12 => "SXY0", // Screen XY coordinate 0
13 => "SXY1", // Screen XY coordinate 1
14 => "SXY2", // Screen XY coordinate 2
15 => "SXYP", // Screen XY coordinate P (mirror of SXY2)
16 => "SZ0", // Screen Z coordinate 0
17 => "SZ1", // Screen Z coordinate 1
18 => "SZ2", // Screen Z coordinate 2
19 => "SZ3", // Screen Z coordinate 3
20 => "RGB0", // Color FIFO 0
21 => "RGB1", // Color FIFO 1
22 => "RGB2", // Color FIFO 2
23 => "RES1", // Reserved
24 => "MAC0", // Multiply-accumulate 0
25 => "MAC1", // Multiply-accumulate 1
26 => "MAC2", // Multiply-accumulate 2
27 => "MAC3", // Multiply-accumulate 3
28 => "IRGB", // Input RGB
29 => "ORGB", // Output RGB
30 => "LZCS", // Leading zero count source
31 => "LZCR", // Leading zero count result / FLAG
_ => $"DR{index}"
};
}
/// <summary>
/// Get GTE control register name for debugging.
/// </summary>
public static string GetControlRegisterName(uint index)
{
return index switch
{
0 => "R11R12", // Rotation matrix
1 => "R13R21",
2 => "R22R23",
3 => "R31R32",
4 => "R33",
5 => "TRX", // Translation vector X
6 => "TRY", // Translation vector Y
7 => "TRZ", // Translation vector Z
8 => "L11L12", // Light source matrix
9 => "L13L21",
10 => "L22L23",
11 => "L31L32",
12 => "L33",
13 => "RBK", // Background color R
14 => "GBK", // Background color G
15 => "BBK", // Background color B
16 => "LR1LR2", // Light color matrix
17 => "LR3LG1",
18 => "LG2LG3",
19 => "LB1LB2",
20 => "LB3",
21 => "RFC", // Far color R
22 => "GFC", // Far color G
23 => "BFC", // Far color B
24 => "OFX", // Screen offset X
25 => "OFY", // Screen offset Y
26 => "H", // Projection plane distance
27 => "DQA", // Depth queue parameter A
28 => "DQB", // Depth queue parameter B
29 => "ZSF3", // Z scale factor 3
30 => "ZSF4", // Z scale factor 4
31 => "FLAG", // Error flags
_ => $"CR{index}"
};
}
}
+209
View File
@@ -0,0 +1,209 @@
using Yaroze.Core.Utilities;
namespace Yaroze.Core.CPU;
/// <summary>
/// Represents a decoded MIPS instruction.
/// Provides methods to extract fields from the 32-bit instruction word.
/// </summary>
public readonly struct Instruction
{
private readonly uint _raw;
public Instruction(uint raw)
{
_raw = raw;
}
/// <summary>
/// Raw 32-bit instruction word.
/// </summary>
public uint Raw => _raw;
/// <summary>
/// Opcode (bits 31-26).
/// </summary>
public uint Opcode => BitUtils.ExtractBits(_raw, 26, 6);
/// <summary>
/// RS register field (bits 25-21).
/// </summary>
public uint Rs => BitUtils.ExtractBits(_raw, 21, 5);
/// <summary>
/// RT register field (bits 20-16).
/// </summary>
public uint Rt => BitUtils.ExtractBits(_raw, 16, 5);
/// <summary>
/// RD register field (bits 15-11).
/// </summary>
public uint Rd => BitUtils.ExtractBits(_raw, 11, 5);
/// <summary>
/// Shift amount field (bits 10-6).
/// </summary>
public uint Shamt => BitUtils.ExtractBits(_raw, 6, 5);
/// <summary>
/// Function code (bits 5-0).
/// </summary>
public uint Funct => BitUtils.ExtractBits(_raw, 0, 6);
/// <summary>
/// 16-bit immediate value (bits 15-0).
/// </summary>
public ushort Imm16 => (ushort)BitUtils.ExtractBits(_raw, 0, 16);
/// <summary>
/// 26-bit jump target (bits 25-0).
/// </summary>
public uint Target26 => BitUtils.ExtractBits(_raw, 0, 26);
/// <summary>
/// Sign-extended immediate value (for arithmetic/load/store).
/// </summary>
public uint ImmSigned => SignExtension.SignExtend16(Imm16);
/// <summary>
/// Zero-extended immediate value (for logic operations).
/// </summary>
public uint ImmUnsigned => Imm16;
/// <summary>
/// Coprocessor opcode (bits 25-21) for COP instructions.
/// </summary>
public uint CopOp => BitUtils.ExtractBits(_raw, 21, 5);
/// <summary>
/// Calculate branch target address.
/// </summary>
public uint BranchTarget(uint pc)
{
// Sign-extend 16-bit offset, shift left 2 bits, add to PC+4
uint offset = SignExtension.SignExtend16(Imm16) << 2;
return (pc + 4) + offset;
}
/// <summary>
/// Calculate jump target address.
/// </summary>
public uint JumpTarget(uint pc)
{
// Take upper 4 bits of PC+4, concatenate with target26 << 2
return (pc & 0xF0000000) | (Target26 << 2);
}
/// <summary>
/// Check if this is a NOP instruction (SLL $0, $0, 0).
/// </summary>
public bool IsNop => _raw == 0x00000000;
public override string ToString()
{
return $"0x{_raw:X8} [op={Opcode:X2} rs={Rs} rt={Rt} rd={Rd} funct={Funct:X2}]";
}
}
/// <summary>
/// MIPS instruction opcodes.
/// </summary>
public static class Opcode
{
public const uint SPECIAL = 0x00; // R-type instructions (use funct field)
public const uint REGIMM = 0x01; // Branch instructions (use rt field)
public const uint J = 0x02;
public const uint JAL = 0x03;
public const uint BEQ = 0x04;
public const uint BNE = 0x05;
public const uint BLEZ = 0x06;
public const uint BGTZ = 0x07;
public const uint ADDI = 0x08;
public const uint ADDIU = 0x09;
public const uint SLTI = 0x0A;
public const uint SLTIU = 0x0B;
public const uint ANDI = 0x0C;
public const uint ORI = 0x0D;
public const uint XORI = 0x0E;
public const uint LUI = 0x0F;
public const uint COP0 = 0x10;
public const uint COP1 = 0x11;
public const uint COP2 = 0x12;
public const uint COP3 = 0x13;
public const uint LB = 0x20;
public const uint LH = 0x21;
public const uint LWL = 0x22;
public const uint LW = 0x23;
public const uint LBU = 0x24;
public const uint LHU = 0x25;
public const uint LWR = 0x26;
public const uint SB = 0x28;
public const uint SH = 0x29;
public const uint SWL = 0x2A;
public const uint SW = 0x2B;
public const uint SWR = 0x2E;
public const uint LWC0 = 0x30;
public const uint LWC1 = 0x31;
public const uint LWC2 = 0x32;
public const uint LWC3 = 0x33;
public const uint SWC0 = 0x38;
public const uint SWC1 = 0x39;
public const uint SWC2 = 0x3A;
public const uint SWC3 = 0x3B;
}
/// <summary>
/// MIPS SPECIAL (R-type) function codes.
/// </summary>
public static class Funct
{
public const uint SLL = 0x00;
public const uint SRL = 0x02;
public const uint SRA = 0x03;
public const uint SLLV = 0x04;
public const uint SRLV = 0x06;
public const uint SRAV = 0x07;
public const uint JR = 0x08;
public const uint JALR = 0x09;
public const uint SYSCALL = 0x0C;
public const uint BREAK = 0x0D;
public const uint MFHI = 0x10;
public const uint MTHI = 0x11;
public const uint MFLO = 0x12;
public const uint MTLO = 0x13;
public const uint MULT = 0x18;
public const uint MULTU = 0x19;
public const uint DIV = 0x1A;
public const uint DIVU = 0x1B;
public const uint ADD = 0x20;
public const uint ADDU = 0x21;
public const uint SUB = 0x22;
public const uint SUBU = 0x23;
public const uint AND = 0x24;
public const uint OR = 0x25;
public const uint XOR = 0x26;
public const uint NOR = 0x27;
public const uint SLT = 0x2A;
public const uint SLTU = 0x2B;
}
/// <summary>
/// REGIMM branch types (rt field determines type).
/// </summary>
public static class RegImmRt
{
public const uint BLTZ = 0x00;
public const uint BGEZ = 0x01;
public const uint BLTZAL = 0x10;
public const uint BGEZAL = 0x11;
}
/// <summary>
/// COP0 function codes (rs field determines type for COP0 instructions).
/// </summary>
public static class COP0Funct
{
public const uint MFC0 = 0x00; // Move From COP0
public const uint MTC0 = 0x04; // Move To COP0
public const uint RFE = 0x10; // Return From Exception
}
+205
View File
@@ -0,0 +1,205 @@
namespace Yaroze.Core.CPU;
/// <summary>
/// MIPS R3000A register file.
/// Contains 32 general-purpose registers, program counter, and HI/LO for multiply/divide.
/// </summary>
public class Registers
{
private readonly uint[] _gpr = new uint[32]; // General Purpose Registers
private uint _pc; // Program Counter
private uint _hi; // Multiply/Divide high result
private uint _lo; // Multiply/Divide low result
// Load delay slot tracking
private uint _loadTarget; // Register to write
private uint _loadValue; // Value to write
private bool _loadPending; // Load in delay slot
// Branch delay slot tracking
private uint _branchTarget;
private bool _branchPending;
private bool _branchTaken;
public Registers()
{
Reset();
}
/// <summary>
/// Reset all registers to power-on state.
/// </summary>
public void Reset()
{
Array.Clear(_gpr, 0, _gpr.Length);
_pc = 0xBFC00000; // BIOS entry point
_hi = 0;
_lo = 0;
_loadPending = false;
_branchPending = false;
}
/// <summary>
/// Program Counter.
/// </summary>
public uint PC
{
get => _pc;
set => _pc = value;
}
/// <summary>
/// High result register (multiply/divide).
/// </summary>
public uint HI
{
get => _hi;
set => _hi = value;
}
/// <summary>
/// Low result register (multiply/divide).
/// </summary>
public uint LO
{
get => _lo;
set => _lo = value;
}
/// <summary>
/// Read a general-purpose register.
/// Register 0 always returns 0.
/// </summary>
public uint ReadGPR(uint index)
{
if (index == 0)
return 0;
if (index >= 32)
throw new ArgumentOutOfRangeException(nameof(index), "Register index must be 0-31");
return _gpr[index];
}
/// <summary>
/// Write a general-purpose register.
/// Writes to register 0 are ignored.
/// </summary>
public void WriteGPR(uint index, uint value)
{
if (index == 0)
return; // $zero is always 0
if (index >= 32)
throw new ArgumentOutOfRangeException(nameof(index), "Register index must be 0-31");
_gpr[index] = value;
}
/// <summary>
/// Set a pending load (for load delay slot).
/// The value will be written to the target register after the next instruction.
/// </summary>
public void SetLoadDelay(uint registerIndex, uint value)
{
if (registerIndex == 0)
return; // Don't set delay for $zero
_loadTarget = registerIndex;
_loadValue = value;
_loadPending = true;
}
/// <summary>
/// Commit the pending load (called after each instruction).
/// </summary>
public void CommitLoadDelay()
{
if (_loadPending)
{
WriteGPR(_loadTarget, _loadValue);
_loadPending = false;
}
}
/// <summary>
/// Cancel any pending load (used when the delay slot instruction writes to the same register).
/// </summary>
public void CancelLoadDelay(uint registerIndex)
{
if (_loadPending && _loadTarget == registerIndex)
{
_loadPending = false;
}
}
/// <summary>
/// Set a pending branch (for branch delay slot).
/// The branch will be taken after the next instruction.
/// </summary>
public void SetBranch(uint targetAddress, bool taken)
{
_branchTarget = targetAddress;
_branchTaken = taken;
_branchPending = true;
}
/// <summary>
/// Commit the pending branch (called after delay slot instruction).
/// </summary>
public void CommitBranch()
{
if (_branchPending)
{
if (_branchTaken)
{
_pc = _branchTarget;
}
_branchPending = false;
}
}
/// <summary>
/// Check if we're currently in a branch delay slot.
/// </summary>
public bool InBranchDelaySlot => _branchPending;
/// <summary>
/// Get register name for debugging/disassembly.
/// </summary>
public static string GetRegisterName(uint index)
{
return index switch
{
0 => "$zero",
1 => "$at",
2 => "$v0",
3 => "$v1",
4 => "$a0",
5 => "$a1",
6 => "$a2",
7 => "$a3",
8 => "$t0",
9 => "$t1",
10 => "$t2",
11 => "$t3",
12 => "$t4",
13 => "$t5",
14 => "$t6",
15 => "$t7",
16 => "$s0",
17 => "$s1",
18 => "$s2",
19 => "$s3",
20 => "$s4",
21 => "$s5",
22 => "$s6",
23 => "$s7",
24 => "$t8",
25 => "$t9",
26 => "$k0",
27 => "$k1",
28 => "$gp",
29 => "$sp",
30 => "$fp",
31 => "$ra",
_ => $"${index}"
};
}
}
+426
View File
@@ -0,0 +1,426 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.DMA;
/// <summary>
/// PlayStation 1 DMA Controller.
/// Manages 7 DMA channels for high-speed data transfers.
/// </summary>
public class DmaController : IBusDevice
{
private readonly DmaChannel[] _channels = new DmaChannel[7];
private uint _dpcr; // DMA Priority Control Register
private uint _dicr; // DMA Interrupt Control Register
private readonly IBus _bus;
public DmaController(IBus bus)
{
_bus = bus;
for (int i = 0; i < 7; i++)
{
_channels[i] = new DmaChannel(i);
}
Reset();
}
public void Reset()
{
foreach (var channel in _channels)
{
channel.Reset();
}
_dpcr = 0x07654321; // Default priority
_dicr = 0;
}
#region IBusDevice Implementation
public bool Contains(uint address)
{
// DMA registers at 0x1F801080-0x1F8010FF
return address >= 0x1F801080 && address <= 0x1F8010FF;
}
public uint Read32(uint address)
{
uint offset = address - 0x1F801080;
// Check if it's a channel register
if (offset < 0x70) // 7 channels × 0x10 bytes each
{
int channelNum = (int)(offset / 0x10);
int regOffset = (int)(offset % 0x10);
return regOffset switch
{
0x00 => _channels[channelNum].MADR,
0x04 => _channels[channelNum].BCR,
0x08 => _channels[channelNum].CHCR,
_ => 0
};
}
// Global DMA registers
return offset switch
{
0x70 => _dpcr, // 0x1F8010F0
0x74 => _dicr, // 0x1F8010F4
_ => 0
};
}
public void Write32(uint address, uint value)
{
uint offset = address - 0x1F801080;
// Check if it's a channel register
if (offset < 0x70)
{
int channelNum = (int)(offset / 0x10);
int regOffset = (int)(offset % 0x10);
switch (regOffset)
{
case 0x00: // MADR
_channels[channelNum].MADR = value & 0x00FFFFFF; // 24-bit address
break;
case 0x04: // BCR
_channels[channelNum].BCR = value;
break;
case 0x08: // CHCR
_channels[channelNum].CHCR = value;
// If start bit is set, trigger transfer
if ((value & 0x01000000) != 0)
{
TriggerTransfer(channelNum);
}
break;
}
return;
}
// Global DMA registers
switch (offset)
{
case 0x70: // DPCR
_dpcr = value;
break;
case 0x74: // DICR
// Write to clear IRQ flags in bits 24-30
_dicr = (_dicr & ~0x7F000000u) | (value & 0x00FFFFFFu);
// Writing 1 to bits 24-30 clears them
_dicr &= ~(value & 0x7F000000u);
UpdateMasterFlag();
break;
}
}
public ushort Read16(uint address) => (ushort)(Read32(address & ~3u) >> (int)((address & 2) << 3));
public byte Read8(uint address) => (byte)(Read32(address & ~3u) >> (int)((address & 3) << 3));
public void Write16(uint address, ushort value) { /* Not used for DMA */ }
public void Write8(uint address, byte value) { /* Not used for DMA */ }
#endregion
#region DMA Transfer Logic
private void TriggerTransfer(int channelNum)
{
var channel = _channels[channelNum];
// Check if channel is enabled in DPCR
int enableBit = channelNum * 4 + 3;
if ((_dpcr & (1u << enableBit)) == 0)
{
return; // Channel disabled
}
// Execute transfer based on sync mode
int syncMode = (int)((channel.CHCR >> 9) & 0x3);
switch (syncMode)
{
case 0: // Burst (immediate)
ExecuteBurstTransfer(channelNum);
break;
case 1: // Slice (blocks)
ExecuteSliceTransfer(channelNum);
break;
case 2: // Linked list
ExecuteLinkedListTransfer(channelNum);
break;
}
// Clear start/busy bit
channel.CHCR &= ~0x01000000u;
// Raise interrupt if enabled
RaiseInterrupt(channelNum);
}
private void ExecuteBurstTransfer(int channelNum)
{
var channel = _channels[channelNum];
uint wordCount = channel.BCR & 0xFFFF;
if (wordCount == 0) wordCount = 0x10000;
uint address = channel.MADR;
bool fromRam = (channel.CHCR & 0x01) != 0;
bool stepBackward = (channel.CHCR & 0x02) != 0;
int step = stepBackward ? -4 : 4;
// Perform transfer
for (uint i = 0; i < wordCount; i++)
{
if (fromRam)
{
// RAM → Device
uint value = _bus.Read32(address);
WriteToDevice(channelNum, value);
}
else
{
// Device → RAM
uint value = ReadFromDevice(channelNum);
_bus.Write32(address, value);
}
address = (uint)((int)address + step);
}
// Update MADR
channel.MADR = address & 0x00FFFFFF;
}
private void ExecuteSliceTransfer(int channelNum)
{
var channel = _channels[channelNum];
uint blockSize = channel.BCR & 0xFFFF;
uint blockCount = (channel.BCR >> 16) & 0xFFFF;
if (blockSize == 0) blockSize = 0x10000;
if (blockCount == 0) blockCount = 0x10000;
uint address = channel.MADR;
bool fromRam = (channel.CHCR & 0x01) != 0;
bool stepBackward = (channel.CHCR & 0x02) != 0;
int step = stepBackward ? -4 : 4;
// Perform transfer
for (uint block = 0; block < blockCount; block++)
{
for (uint word = 0; word < blockSize; word++)
{
if (fromRam)
{
uint value = _bus.Read32(address);
WriteToDevice(channelNum, value);
}
else
{
uint value = ReadFromDevice(channelNum);
_bus.Write32(address, value);
}
address = (uint)((int)address + step);
}
}
// Update MADR
channel.MADR = address & 0x00FFFFFF;
}
private void ExecuteLinkedListTransfer(int channelNum)
{
var channel = _channels[channelNum];
uint address = channel.MADR;
// Linked list mode (used for GPU)
// Each node: [header word][data...]
// Header: bits 0-23 = next address, bits 24-31 = word count
int maxIterations = 100000; // Safety limit
int iterations = 0;
while ((address & 0x00FFFFFF) != 0x00FFFFFF && iterations < maxIterations)
{
// Read header
uint header = _bus.Read32(address & 0x00FFFFFF);
uint wordCount = (header >> 24) & 0xFF;
uint nextAddress = header & 0x00FFFFFF;
address += 4;
// Transfer data words
for (uint i = 0; i < wordCount; i++)
{
uint value = _bus.Read32(address & 0x00FFFFFF);
WriteToDevice(channelNum, value);
address += 4;
}
// Move to next node
address = nextAddress;
iterations++;
}
// Update MADR
channel.MADR = 0x00FFFFFF; // End of list marker
}
private uint ReadFromDevice(int channelNum)
{
return channelNum switch
{
2 => _bus.Read32(0x1F801810), // GPU GPUREAD
3 => _bus.Read32(0x1F801802), // CD-ROM data FIFO
// Other channels would read from their respective devices
_ => 0xFFFFFFFF
};
}
private void WriteToDevice(int channelNum, uint value)
{
switch (channelNum)
{
case 2: // GPU
_bus.Write32(0x1F801810, value); // GPU GP0
break;
case 6: // OTC (Ordering Table Clear)
// OTC writes to RAM in reverse order with linked list headers
// This is a special case handled in ExecuteOtcTransfer
break;
// Other channels would write to their respective devices
}
}
#endregion
#region Interrupt Handling
private void RaiseInterrupt(int channelNum)
{
// Check if interrupt is enabled for this channel (bit 16-22)
uint irqEnableBit = 1u << (16 + channelNum);
if ((_dicr & irqEnableBit) == 0)
return; // Interrupt not enabled for this channel
// Set interrupt flag (bit 24-30)
uint irqFlagBit = 1u << (24 + channelNum);
_dicr |= irqFlagBit;
UpdateMasterFlag();
}
private void UpdateMasterFlag()
{
// Master IRQ flag (bit 31) is set if:
// - Master enable (bit 23) is set AND
// - Any enabled interrupt flag is set
bool masterEnable = (_dicr & (1u << 23)) != 0;
uint enabledFlags = (_dicr >> 16) & 0x7F; // Bits 16-22
uint flags = (_dicr >> 24) & 0x7F; // Bits 24-30
bool anyEnabled = (flags & enabledFlags) != 0;
if (masterEnable && anyEnabled)
{
_dicr |= (1u << 31); // Set master flag
_dicr |= (1u << 15); // Set IRQ signal
}
else
{
_dicr &= ~(1u << 31); // Clear master flag
_dicr &= ~(1u << 15); // Clear IRQ signal
}
}
/// <summary>
/// Check if DMA interrupt is pending.
/// </summary>
public bool HasPendingInterrupt()
{
return (_dicr & (1u << 15)) != 0;
}
#endregion
/// <summary>
/// Get a specific DMA channel.
/// </summary>
public DmaChannel GetChannel(int index)
{
if (index < 0 || index >= 7)
throw new ArgumentOutOfRangeException(nameof(index));
return _channels[index];
}
/// <summary>
/// DMA Priority Control Register.
/// </summary>
public uint DPCR => _dpcr;
/// <summary>
/// DMA Interrupt Control Register.
/// </summary>
public uint DICR => _dicr;
}
/// <summary>
/// DMA channel state.
/// </summary>
public class DmaChannel
{
public int ChannelNumber { get; }
public uint MADR { get; set; } // Memory Address Register
public uint BCR { get; set; } // Block Control Register
public uint CHCR { get; set; } // Channel Control Register
public DmaChannel(int channelNumber)
{
ChannelNumber = channelNumber;
Reset();
}
public void Reset()
{
MADR = 0;
BCR = 0;
CHCR = 0;
}
/// <summary>
/// Check if channel is active (start/busy bit set).
/// </summary>
public bool IsActive => (CHCR & 0x01000000) != 0;
/// <summary>
/// Get sync mode (0=burst, 1=slice, 2=linked list).
/// </summary>
public int SyncMode => (int)((CHCR >> 9) & 0x3);
/// <summary>
/// Get transfer direction (false=to RAM, true=from RAM).
/// </summary>
public bool FromRam => (CHCR & 0x01) != 0;
}
@@ -0,0 +1,235 @@
using Yaroze.Core.CPU;
using Yaroze.Core.Utilities;
namespace Yaroze.Core.Disassembly;
/// <summary>
/// MIPS R3000A disassembler for PlayStation 1.
/// Converts raw instruction words into human-readable assembly.
/// </summary>
public class MipsDisassembler
{
/// <summary>
/// Disassemble a single instruction.
/// </summary>
/// <param name="pc">Program counter (address of instruction)</param>
/// <param name="instruction">Raw 32-bit instruction word</param>
/// <returns>Disassembled instruction text</returns>
public static string Disassemble(uint pc, uint instruction)
{
var instr = new Instruction(instruction);
uint opcode = instr.Opcode;
return opcode switch
{
Opcode.SPECIAL => DisassembleSpecial(instr),
Opcode.REGIMM => DisassembleRegImm(instr, pc),
Opcode.J => $"j 0x{instr.JumpTarget(pc):X8}",
Opcode.JAL => $"jal 0x{instr.JumpTarget(pc):X8}",
Opcode.BEQ => $"beq {Reg(instr.Rs)}, {Reg(instr.Rt)}, 0x{instr.BranchTarget(pc):X8}",
Opcode.BNE => $"bne {Reg(instr.Rs)}, {Reg(instr.Rt)}, 0x{instr.BranchTarget(pc):X8}",
Opcode.BLEZ => $"blez {Reg(instr.Rs)}, 0x{instr.BranchTarget(pc):X8}",
Opcode.BGTZ => $"bgtz {Reg(instr.Rs)}, 0x{instr.BranchTarget(pc):X8}",
Opcode.ADDI => $"addi {Reg(instr.Rt)}, {Reg(instr.Rs)}, {(short)instr.Imm16}",
Opcode.ADDIU => $"addiu {Reg(instr.Rt)}, {Reg(instr.Rs)}, {(short)instr.Imm16}",
Opcode.SLTI => $"slti {Reg(instr.Rt)}, {Reg(instr.Rs)}, {(short)instr.Imm16}",
Opcode.SLTIU => $"sltiu {Reg(instr.Rt)}, {Reg(instr.Rs)}, {(short)instr.Imm16}",
Opcode.ANDI => $"andi {Reg(instr.Rt)}, {Reg(instr.Rs)}, 0x{instr.Imm16:X}",
Opcode.ORI => $"ori {Reg(instr.Rt)}, {Reg(instr.Rs)}, 0x{instr.Imm16:X}",
Opcode.XORI => $"xori {Reg(instr.Rt)}, {Reg(instr.Rs)}, 0x{instr.Imm16:X}",
Opcode.LUI => $"lui {Reg(instr.Rt)}, 0x{instr.Imm16:X}",
Opcode.COP0 => DisassembleCOP0(instr),
Opcode.COP2 => DisassembleCOP2(instr),
Opcode.LB => $"lb {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.LH => $"lh {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.LWL => $"lwl {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.LW => $"lw {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.LBU => $"lbu {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.LHU => $"lhu {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.LWR => $"lwr {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.SB => $"sb {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.SH => $"sh {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.SWL => $"swl {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.SW => $"sw {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.SWR => $"swr {Reg(instr.Rt)}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.LWC2 => $"lwc2 ${instr.Rt}, {(short)instr.Imm16}({Reg(instr.Rs)})",
Opcode.SWC2 => $"swc2 ${instr.Rt}, {(short)instr.Imm16}({Reg(instr.Rs)})",
_ => $".word 0x{instruction:X8}"
};
}
private static string DisassembleSpecial(Instruction instr)
{
return instr.Funct switch
{
Funct.SLL when instr.Raw == 0 => "nop",
Funct.SLL => $"sll {Reg(instr.Rd)}, {Reg(instr.Rt)}, {instr.Shamt}",
Funct.SRL => $"srl {Reg(instr.Rd)}, {Reg(instr.Rt)}, {instr.Shamt}",
Funct.SRA => $"sra {Reg(instr.Rd)}, {Reg(instr.Rt)}, {instr.Shamt}",
Funct.SLLV => $"sllv {Reg(instr.Rd)}, {Reg(instr.Rt)}, {Reg(instr.Rs)}",
Funct.SRLV => $"srlv {Reg(instr.Rd)}, {Reg(instr.Rt)}, {Reg(instr.Rs)}",
Funct.SRAV => $"srav {Reg(instr.Rd)}, {Reg(instr.Rt)}, {Reg(instr.Rs)}",
Funct.JR => $"jr {Reg(instr.Rs)}",
Funct.JALR => instr.Rd == 31 ? $"jalr {Reg(instr.Rs)}" : $"jalr {Reg(instr.Rd)}, {Reg(instr.Rs)}",
Funct.SYSCALL => "syscall",
Funct.BREAK => "break",
Funct.MFHI => $"mfhi {Reg(instr.Rd)}",
Funct.MTHI => $"mthi {Reg(instr.Rs)}",
Funct.MFLO => $"mflo {Reg(instr.Rd)}",
Funct.MTLO => $"mtlo {Reg(instr.Rs)}",
Funct.MULT => $"mult {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.MULTU => $"multu {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.DIV => $"div {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.DIVU => $"divu {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.ADD => $"add {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.ADDU => $"addu {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.SUB => $"sub {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.SUBU => $"subu {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.AND => $"and {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.OR => $"or {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.XOR => $"xor {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.NOR => $"nor {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.SLT => $"slt {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
Funct.SLTU => $"sltu {Reg(instr.Rd)}, {Reg(instr.Rs)}, {Reg(instr.Rt)}",
_ => $".word 0x{instr.Raw:X8}"
};
}
private static string DisassembleRegImm(Instruction instr, uint pc)
{
return instr.Rt switch
{
RegImmRt.BLTZ => $"bltz {Reg(instr.Rs)}, 0x{instr.BranchTarget(pc):X8}",
RegImmRt.BGEZ => $"bgez {Reg(instr.Rs)}, 0x{instr.BranchTarget(pc):X8}",
RegImmRt.BLTZAL => $"bltzal {Reg(instr.Rs)}, 0x{instr.BranchTarget(pc):X8}",
RegImmRt.BGEZAL => $"bgezal {Reg(instr.Rs)}, 0x{instr.BranchTarget(pc):X8}",
_ => $".word 0x{instr.Raw:X8}"
};
}
private static string DisassembleCOP0(Instruction instr)
{
uint copOp = instr.CopOp;
return copOp switch
{
0x00 => $"mfc0 {Reg(instr.Rt)}, ${instr.Rd}",
0x04 => $"mtc0 {Reg(instr.Rt)}, ${instr.Rd}",
0x10 when instr.Funct == 0x10 => "rfe",
_ => $".word 0x{instr.Raw:X8}"
};
}
private static string DisassembleCOP2(Instruction instr)
{
uint copOp = instr.CopOp;
if ((copOp & 0x10) != 0)
{
// GTE command
uint cmd = instr.Raw & 0x1FFFFFF;
return $"cop2 0x{cmd:X7}";
}
return copOp switch
{
0x00 => $"mfc2 {Reg(instr.Rt)}, ${instr.Rd}",
0x02 => $"cfc2 {Reg(instr.Rt)}, ${instr.Rd}",
0x04 => $"mtc2 {Reg(instr.Rt)}, ${instr.Rd}",
0x06 => $"ctc2 {Reg(instr.Rt)}, ${instr.Rd}",
_ => $".word 0x{instr.Raw:X8}"
};
}
/// <summary>
/// Get register name for a register number.
/// </summary>
private static string Reg(uint regNum)
{
return regNum switch
{
0 => "$zero",
1 => "$at",
2 => "$v0",
3 => "$v1",
4 => "$a0",
5 => "$a1",
6 => "$a2",
7 => "$a3",
8 => "$t0",
9 => "$t1",
10 => "$t2",
11 => "$t3",
12 => "$t4",
13 => "$t5",
14 => "$t6",
15 => "$t7",
16 => "$s0",
17 => "$s1",
18 => "$s2",
19 => "$s3",
20 => "$s4",
21 => "$s5",
22 => "$s6",
23 => "$s7",
24 => "$t8",
25 => "$t9",
26 => "$k0",
27 => "$k1",
28 => "$gp",
29 => "$sp",
30 => "$fp",
31 => "$ra",
_ => $"${regNum}"
};
}
/// <summary>
/// Disassemble a block of instructions.
/// </summary>
/// <param name="startAddress">Starting address</param>
/// <param name="data">Raw instruction data</param>
/// <param name="count">Number of instructions to disassemble</param>
/// <returns>List of disassembled instructions with addresses</returns>
public static List<DisassembledInstruction> DisassembleBlock(uint startAddress, byte[] data, int count)
{
var result = new List<DisassembledInstruction>();
uint address = startAddress;
for (int i = 0; i < count && i * 4 < data.Length; i++)
{
uint instruction = BitConverter.ToUInt32(data, i * 4);
string disassembly = Disassemble(address, instruction);
result.Add(new DisassembledInstruction
{
Address = address,
InstructionWord = instruction,
Disassembly = disassembly
});
address += 4;
}
return result;
}
}
/// <summary>
/// Represents a disassembled instruction.
/// </summary>
public class DisassembledInstruction
{
public uint Address { get; set; }
public uint InstructionWord { get; set; }
public string Disassembly { get; set; } = "";
public string? Label { get; set; }
public string? Comment { get; set; }
public override string ToString()
{
var label = Label != null ? $"{Label}:\n" : "";
var comment = Comment != null ? $" ; {Comment}" : "";
return $"{label}0x{Address:X8}: {InstructionWord:X8} {Disassembly}{comment}";
}
}
@@ -0,0 +1,422 @@
using System;
using System.Collections.Generic;
using System.Text;
using Yaroze.Core.Analysis;
using Yaroze.Core.CPU;
namespace Yaroze.Core.Disassembly;
/// <summary>
/// Converts MIPS assembly to pseudo-C code for easier analysis.
///
/// This decompiler translates MIPS instructions into C-like pseudocode, making it
/// easier to understand program logic without reading raw assembly. It focuses on
/// readability over perfect C syntax, using intuitive variable names and control
/// flow structures.
///
/// Features:
/// - Register references converted to variable names (v0-v31, a0-a3, etc.)
/// - Branch instructions converted to if/while/goto statements
/// - Function calls identified and formatted
/// - Memory operations shown as pointer dereferences
/// - Comments with original assembly for reference
/// </summary>
public class PseudoCDecompiler
{
private readonly byte[] _memory;
private readonly uint _baseAddress;
private readonly SymbolManager? _symbolManager;
private readonly HashSet<uint> _processedAddresses = new();
private int _indentLevel = 0;
public PseudoCDecompiler(byte[] memory, uint baseAddress, SymbolManager? symbolManager = null)
{
_memory = memory;
_baseAddress = baseAddress;
_symbolManager = symbolManager;
}
/// <summary>
/// Decompiles a function starting at the given address.
/// </summary>
public string DecompileFunction(Function function)
{
var sb = new StringBuilder();
_processedAddresses.Clear();
_indentLevel = 0;
// Function signature
var symbol = _symbolManager?.GetSymbol(function.Address);
string functionName = symbol?.Name ?? function.Name ?? $"func_{function.Address:X8}";
sb.AppendLine($"void {functionName}()");
sb.AppendLine("{");
_indentLevel++;
// Process instructions
var sortedInstructions = new List<uint>(function.Instructions);
sortedInstructions.Sort();
foreach (var address in sortedInstructions)
{
if (_processedAddresses.Contains(address))
continue;
DecompileInstruction(sb, address);
}
_indentLevel--;
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// Decompiles a single instruction to pseudo-C.
/// </summary>
private void DecompileInstruction(StringBuilder sb, uint address)
{
_processedAddresses.Add(address);
uint instruction = ReadInstruction(address);
var instr = new Instruction(instruction);
// Get comment if available
var comment = _symbolManager?.GetComment(address);
string commentStr = comment != null ? $" // {comment}" : "";
string indent = new string(' ', _indentLevel * 4);
switch (instr.Opcode)
{
// SPECIAL instruction group (opcode 0x00) - uses funct field for decoding
case Opcode.SPECIAL:
switch (instr.Funct)
{
case Funct.ADDU:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rs)} + {RegName(instr.Rt)};{commentStr}");
break;
case Funct.SUBU:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rs)} - {RegName(instr.Rt)};{commentStr}");
break;
case Funct.AND:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rs)} & {RegName(instr.Rt)};{commentStr}");
break;
case Funct.OR:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rs)} | {RegName(instr.Rt)};{commentStr}");
break;
case Funct.XOR:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rs)} ^ {RegName(instr.Rt)};{commentStr}");
break;
case Funct.SLL:
if (instruction == 0) // NOP
{
sb.AppendLine($"{indent}// nop{commentStr}");
}
else
{
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rt)} << {instr.Shamt};{commentStr}");
}
break;
case Funct.SRL:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rt)} >> {instr.Shamt};{commentStr}");
break;
case Funct.SRA:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = (int){RegName(instr.Rt)} >> {instr.Shamt};{commentStr}");
break;
case Funct.SLLV:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rt)} << {RegName(instr.Rs)};{commentStr}");
break;
case Funct.SRLV:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = {RegName(instr.Rt)} >> {RegName(instr.Rs)};{commentStr}");
break;
case Funct.SRAV:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = (int){RegName(instr.Rt)} >> {RegName(instr.Rs)};{commentStr}");
break;
case Funct.JR:
if (instr.Rs == 31) // JR $ra -> return
{
sb.AppendLine($"{indent}return;{commentStr}");
}
else
{
sb.AppendLine($"{indent}goto *{RegName(instr.Rs)};{commentStr}");
}
break;
case Funct.JALR:
sb.AppendLine($"{indent}call({RegName(instr.Rs)});{commentStr}");
break;
case Funct.SLT:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = ({RegName(instr.Rs)} < {RegName(instr.Rt)}) ? 1 : 0;{commentStr}");
break;
case Funct.SLTU:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = ((uint){RegName(instr.Rs)} < (uint){RegName(instr.Rt)}) ? 1 : 0;{commentStr}");
break;
case Funct.MULT:
sb.AppendLine($"{indent}// mult {RegName(instr.Rs)}, {RegName(instr.Rt)}{commentStr}");
break;
case Funct.MULTU:
sb.AppendLine($"{indent}// multu {RegName(instr.Rs)}, {RegName(instr.Rt)}{commentStr}");
break;
case Funct.DIV:
sb.AppendLine($"{indent}// div {RegName(instr.Rs)}, {RegName(instr.Rt)}{commentStr}");
break;
case Funct.DIVU:
sb.AppendLine($"{indent}// divu {RegName(instr.Rs)}, {RegName(instr.Rt)}{commentStr}");
break;
case Funct.MFHI:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = HI;{commentStr}");
break;
case Funct.MFLO:
sb.AppendLine($"{indent}{RegName(instr.Rd)} = LO;{commentStr}");
break;
case Funct.MTHI:
sb.AppendLine($"{indent}HI = {RegName(instr.Rs)};{commentStr}");
break;
case Funct.MTLO:
sb.AppendLine($"{indent}LO = {RegName(instr.Rs)};{commentStr}");
break;
case Funct.SYSCALL:
sb.AppendLine($"{indent}syscall();{commentStr}");
break;
case Funct.BREAK:
sb.AppendLine($"{indent}break();{commentStr}");
break;
default:
// For unknown SPECIAL instructions, show disassembly
string specialDisasm = MipsDisassembler.Disassemble(address, instruction);
sb.AppendLine($"{indent}// {specialDisasm}{commentStr}");
break;
}
break;
// REGIMM instruction group (opcode 0x01) - uses rt field for decoding
case Opcode.REGIMM:
switch (instr.Rt)
{
case RegImmRt.BLTZ:
sb.AppendLine($"{indent}if ({RegName(instr.Rs)} < 0) goto label_{instr.BranchTarget(address):X8};{commentStr}");
break;
case RegImmRt.BGEZ:
sb.AppendLine($"{indent}if ({RegName(instr.Rs)} >= 0) goto label_{instr.BranchTarget(address):X8};{commentStr}");
break;
default:
// For unknown REGIMM instructions, show disassembly
string regimmDisasm = MipsDisassembler.Disassemble(address, instruction);
sb.AppendLine($"{indent}// {regimmDisasm}{commentStr}");
break;
}
break;
// COP0 instruction group (opcode 0x10) - uses cop field for decoding
case Opcode.COP0:
switch (instr.CopOp)
{
case COP0Funct.MFC0:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = COP0[{instr.Rd}];{commentStr}");
break;
case COP0Funct.MTC0:
sb.AppendLine($"{indent}COP0[{instr.Rd}] = {RegName(instr.Rt)};{commentStr}");
break;
default:
// For unknown COP0 instructions, show disassembly
string cop0Disasm = MipsDisassembler.Disassemble(address, instruction);
sb.AppendLine($"{indent}// {cop0Disasm}{commentStr}");
break;
}
break;
// Arithmetic operations
case Opcode.ADDIU:
if (instr.Rs == 0) // ADDIU $rt, $zero, imm -> $rt = imm
{
sb.AppendLine($"{indent}{RegName(instr.Rt)} = {(short)instr.Imm16};{commentStr}");
}
else
{
sb.AppendLine($"{indent}{RegName(instr.Rt)} = {RegName(instr.Rs)} + {(short)instr.Imm16};{commentStr}");
}
break;
case Opcode.ANDI:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = {RegName(instr.Rs)} & 0x{instr.Imm16:X};{commentStr}");
break;
case Opcode.ORI:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = {RegName(instr.Rs)} | 0x{instr.Imm16:X};{commentStr}");
break;
case Opcode.XORI:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = {RegName(instr.Rs)} ^ 0x{instr.Imm16:X};{commentStr}");
break;
// Load/Store operations
case Opcode.LW:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = *(int*)({RegName(instr.Rs)} + {(short)instr.Imm16});{commentStr}");
break;
case Opcode.LH:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = *(short*)({RegName(instr.Rs)} + {(short)instr.Imm16});{commentStr}");
break;
case Opcode.LHU:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = *(ushort*)({RegName(instr.Rs)} + {(short)instr.Imm16});{commentStr}");
break;
case Opcode.LB:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = *(sbyte*)({RegName(instr.Rs)} + {(short)instr.Imm16});{commentStr}");
break;
case Opcode.LBU:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = *(byte*)({RegName(instr.Rs)} + {(short)instr.Imm16});{commentStr}");
break;
case Opcode.SW:
sb.AppendLine($"{indent}*(int*)({RegName(instr.Rs)} + {(short)instr.Imm16}) = {RegName(instr.Rt)};{commentStr}");
break;
case Opcode.SH:
sb.AppendLine($"{indent}*(short*)({RegName(instr.Rs)} + {(short)instr.Imm16}) = {RegName(instr.Rt)};{commentStr}");
break;
case Opcode.SB:
sb.AppendLine($"{indent}*(byte*)({RegName(instr.Rs)} + {(short)instr.Imm16}) = {RegName(instr.Rt)};{commentStr}");
break;
case Opcode.LUI:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = 0x{instr.Imm16:X} << 16;{commentStr}");
break;
// Branches
case Opcode.BEQ:
if (instr.Rs == 0 && instr.Rt == 0) // Always true
{
sb.AppendLine($"{indent}goto label_{instr.BranchTarget(address):X8};{commentStr}");
}
else
{
sb.AppendLine($"{indent}if ({RegName(instr.Rs)} == {RegName(instr.Rt)}) goto label_{instr.BranchTarget(address):X8};{commentStr}");
}
break;
case Opcode.BNE:
sb.AppendLine($"{indent}if ({RegName(instr.Rs)} != {RegName(instr.Rt)}) goto label_{instr.BranchTarget(address):X8};{commentStr}");
break;
case Opcode.BLEZ:
sb.AppendLine($"{indent}if ({RegName(instr.Rs)} <= 0) goto label_{instr.BranchTarget(address):X8};{commentStr}");
break;
case Opcode.BGTZ:
sb.AppendLine($"{indent}if ({RegName(instr.Rs)} > 0) goto label_{instr.BranchTarget(address):X8};{commentStr}");
break;
// Jumps
case Opcode.JAL:
{
uint target = instr.JumpTarget(address);
var targetSymbol = _symbolManager?.GetSymbol(target);
string funcName = targetSymbol?.Name ?? $"func_{target:X8}";
sb.AppendLine($"{indent}{funcName}();{commentStr}");
}
break;
case Opcode.J:
sb.AppendLine($"{indent}goto label_{instr.JumpTarget(address):X8};{commentStr}");
break;
// Comparisons
case Opcode.SLTI:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = ({RegName(instr.Rs)} < {(short)instr.Imm16}) ? 1 : 0;{commentStr}");
break;
case Opcode.SLTIU:
sb.AppendLine($"{indent}{RegName(instr.Rt)} = ((uint){RegName(instr.Rs)} < {instr.Imm16}) ? 1 : 0;{commentStr}");
break;
default:
// For unknown instructions, show disassembly
string disasm = MipsDisassembler.Disassemble(address, instruction);
sb.AppendLine($"{indent}// {disasm}{commentStr}");
break;
}
}
private uint ReadInstruction(uint address)
{
int offset = (int)(address - _baseAddress);
if (offset < 0 || offset + 3 >= _memory.Length)
return 0;
return BitConverter.ToUInt32(_memory, offset);
}
private string RegName(uint regNum)
{
return regNum switch
{
0 => "zero",
1 => "at",
2 => "v0",
3 => "v1",
4 => "a0",
5 => "a1",
6 => "a2",
7 => "a3",
8 => "t0",
9 => "t1",
10 => "t2",
11 => "t3",
12 => "t4",
13 => "t5",
14 => "t6",
15 => "t7",
16 => "s0",
17 => "s1",
18 => "s2",
19 => "s3",
20 => "s4",
21 => "s5",
22 => "s6",
23 => "s7",
24 => "t8",
25 => "t9",
26 => "k0",
27 => "k1",
28 => "gp",
29 => "sp",
30 => "fp",
31 => "ra",
_ => $"r{regNum}"
};
}
}
+297
View File
@@ -0,0 +1,297 @@
using Yaroze.Core.CDROM;
using Yaroze.Core.CPU;
using Yaroze.Core.DMA;
using Yaroze.Core.GPU;
using Yaroze.Core.Interrupts;
using Yaroze.Core.Interfaces;
using Yaroze.Core.Loaders;
using Yaroze.Core.Memory;
using Yaroze.Core.Timers;
using Timer = Yaroze.Core.Timers.Timer;
namespace Yaroze.Core;
/// <summary>
/// Top-level PlayStation 1 emulator.
/// Coordinates CPU, memory, GPU, DMA, timers, and interrupt handling.
/// </summary>
public class Emulator
{
private readonly Bus _bus;
private readonly Cpu _cpu;
private readonly Gpu _gpu;
private readonly CdRomDevice _cdrom;
private readonly DmaController _dma;
private readonly InterruptController _interrupts;
private readonly Timer _timer0;
private readonly Timer _timer1;
private readonly Timer _timer2;
private ITraceSink? _traceSink;
private bool _running;
/// <summary>
/// Get the CPU.
/// </summary>
public Cpu Cpu => _cpu;
/// <summary>
/// Get the GPU.
/// </summary>
public Gpu Gpu => _gpu;
/// <summary>
/// Get the CD-ROM drive.
/// </summary>
public CdRomDevice CdRom => _cdrom;
/// <summary>
/// Get the DMA controller.
/// </summary>
public DmaController Dma => _dma;
/// <summary>
/// Get the interrupt controller.
/// </summary>
public InterruptController Interrupts => _interrupts;
/// <summary>
/// Get Timer 0 (dotclock).
/// </summary>
public Timer Timer0 => _timer0;
/// <summary>
/// Get Timer 1 (hblank).
/// </summary>
public Timer Timer1 => _timer1;
/// <summary>
/// Get Timer 2 (sysclock/8).
/// </summary>
public Timer Timer2 => _timer2;
/// <summary>
/// Get the memory bus.
/// </summary>
public Bus Bus => _bus;
/// <summary>
/// Check if the emulator is currently running.
/// </summary>
public bool IsRunning => _running;
public Emulator()
{
_bus = new Bus();
_gpu = new Gpu();
_interrupts = new InterruptController();
_cdrom = new CdRomDevice(_interrupts);
_dma = new DmaController(_bus);
_timer0 = new Timer(0);
_timer1 = new Timer(1);
_timer2 = new Timer(2);
// Wire up interrupt callbacks
_timer0.SetInterruptCallback(() => _interrupts.RaiseInterrupt(InterruptType.Timer0));
_timer1.SetInterruptCallback(() => _interrupts.RaiseInterrupt(InterruptType.Timer1));
_timer2.SetInterruptCallback(() => _interrupts.RaiseInterrupt(InterruptType.Timer2));
// Add devices to bus
_bus.AddDevice(_gpu);
_bus.AddDevice(_cdrom);
_bus.AddDevice(_dma);
_bus.AddDevice(_interrupts);
_bus.AddDevice(_timer0);
_bus.AddDevice(_timer1);
_bus.AddDevice(_timer2);
_cpu = new Cpu(_bus);
_running = false;
}
/// <summary>
/// Load a BIOS ROM image.
/// </summary>
/// <param name="biosData">512KB BIOS ROM data</param>
public void LoadBios(byte[] biosData)
{
if (biosData.Length != 512 * 1024)
{
throw new ArgumentException($"BIOS must be exactly 512KB, got {biosData.Length} bytes");
}
var bios = new Bios(biosData);
// Note: The Bus already has a Bios, but it's empty
// We'd need to modify Bus to allow replacing the BIOS
// For now, document that BIOS should be loaded before creating Emulator
// Or we create Emulator with optional BIOS parameter
}
/// <summary>
/// Load a PS-EXE file.
/// </summary>
/// <param name="exeData">Complete PS-EXE file data</param>
/// <returns>Parsed EXE header</returns>
public PsExeLoader.ExeHeader LoadExe(byte[] exeData)
{
return PsExeLoader.Load(exeData, _bus, _cpu);
}
/// <summary>
/// Load a PS-EXE file from disk.
/// </summary>
/// <param name="filePath">Path to PS-EXE file</param>
/// <returns>Parsed EXE header</returns>
public PsExeLoader.ExeHeader LoadExeFromFile(string filePath)
{
return PsExeLoader.LoadFromFile(filePath, _bus, _cpu);
}
/// <summary>
/// Load a CD-ROM disc image (.iso, .bin, or .cue file).
/// </summary>
/// <param name="filePath">Path to disc image file</param>
public void LoadDisc(string filePath)
{
_cdrom.LoadDisc(filePath);
}
/// <summary>
/// Reset the emulator to power-on state.
/// </summary>
public void Reset()
{
_cpu.Reset();
_gpu.Reset();
_cdrom.Reset();
_dma.Reset();
_interrupts.Reset();
_timer0.Reset();
_timer1.Reset();
_timer2.Reset();
_running = false;
}
/// <summary>
/// Execute a single CPU instruction.
/// </summary>
public void Step()
{
_cpu.Step();
// Tick timers by 1 cycle per instruction
// This matches the CPU's timing model (1 cycle per instruction)
_timer0.Tick(1);
_timer1.Tick(1);
_timer2.Tick(1);
// Update CPU interrupt state
UpdateInterrupts();
}
/// <summary>
/// Execute N CPU instructions.
/// </summary>
public void StepN(int count)
{
for (int i = 0; i < count; i++)
{
_cpu.Step();
}
}
/// <summary>
/// Run the emulator for a specified number of cycles.
/// </summary>
public void RunCycles(int cycles)
{
_cpu.Run(cycles);
}
/// <summary>
/// Run the emulator continuously until stopped.
/// This is a blocking call - use RunAsync for non-blocking execution.
/// </summary>
public void Run()
{
_running = true;
while (_running)
{
_cpu.Step();
}
}
/// <summary>
/// Stop continuous execution.
/// </summary>
public void Stop()
{
_running = false;
}
/// <summary>
/// Set a trace sink for execution logging.
/// </summary>
public void SetTraceSink(ITraceSink? traceSink)
{
_traceSink = traceSink;
_cpu.SetTraceSink(traceSink);
}
/// <summary>
/// Get execution statistics.
/// </summary>
public EmulatorStats GetStats()
{
return new EmulatorStats
{
TotalCycles = _cpu.TotalCycles,
CurrentPC = _cpu.Registers.PC,
InstructionsExecuted = _cpu.TotalCycles // 1:1 cycle:instruction ratio
};
}
/// <summary>
/// Update CPU interrupt state based on pending interrupts.
/// </summary>
private void UpdateInterrupts()
{
// Check if any hardware interrupts are pending
bool hardwareInterrupt = _interrupts.HasPendingInterrupt();
bool dmaInterrupt = _dma.HasPendingInterrupt();
// Set or clear IRQ0 (hardware interrupts) in COP0 CAUSE
if (hardwareInterrupt)
{
_cpu.Cop0.RaiseInterrupt(2); // IRQ2 (hardware interrupts)
}
else
{
_cpu.Cop0.ClearInterrupt(2);
}
// Set or clear IRQ1 (DMA interrupts) in COP0 CAUSE
if (dmaInterrupt)
{
_cpu.Cop0.RaiseInterrupt(3); // IRQ3 (DMA)
}
else
{
_cpu.Cop0.ClearInterrupt(3);
}
}
}
/// <summary>
/// Emulator execution statistics.
/// </summary>
public class EmulatorStats
{
public ulong TotalCycles { get; set; }
public uint CurrentPC { get; set; }
public ulong InstructionsExecuted { get; set; }
public override string ToString()
{
return $"Cycles: {TotalCycles}, PC: 0x{CurrentPC:X8}, Instructions: {InstructionsExecuted}";
}
}
+538
View File
@@ -0,0 +1,538 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.GPU;
/// <summary>
/// PlayStation 1 GPU (Graphics Processing Unit).
/// Handles display control, VRAM access, and drawing commands.
/// </summary>
public class Gpu : IBusDevice
{
// VRAM: 1024x512 pixels, 16 bits per pixel = 1MB
private readonly ushort[] _vram = new ushort[1024 * 512];
// Command FIFO
private readonly Queue<uint> _commandFifo = new();
// GPU state
private GpuState _state = new();
// Current GP0 command being processed
private uint _currentCommand;
private int _commandParametersRemaining;
private readonly List<uint> _commandParameters = new();
// GPUREAD response FIFO
private readonly Queue<uint> _gpuReadFifo = new();
public Gpu()
{
Reset();
}
/// <summary>
/// Reset GPU to power-on state.
/// </summary>
public void Reset()
{
Array.Clear(_vram, 0, _vram.Length);
_commandFifo.Clear();
_gpuReadFifo.Clear();
_commandParameters.Clear();
_currentCommand = 0;
_commandParametersRemaining = 0;
_state = new GpuState
{
DisplayEnabled = true,
DmaDirection = 0,
DrawMode = 0,
TextureWindowMaskX = 0,
TextureWindowMaskY = 0,
TextureWindowOffsetX = 0,
TextureWindowOffsetY = 0,
DrawAreaLeft = 0,
DrawAreaTop = 0,
DrawAreaRight = 0,
DrawAreaBottom = 0,
DrawOffsetX = 0,
DrawOffsetY = 0,
DisplayAreaX = 0,
DisplayAreaY = 0,
HorizontalStart = 0,
HorizontalEnd = 0,
VerticalStart = 0,
VerticalEnd = 0,
VideoMode = 0,
IrqRequested = false
};
}
#region IBusDevice Implementation
public bool Contains(uint address)
{
// GPU registers at 0x1F801810-0x1F801817
return address >= 0x1F801810 && address <= 0x1F801817;
}
public uint Read32(uint address)
{
uint offset = address - 0x1F801810;
return offset switch
{
0x00 => ReadGP0(), // GPUREAD
0x04 => ReadGP1(), // GPUSTAT
_ => 0xFFFFFFFF
};
}
public void Write32(uint address, uint value)
{
uint offset = address - 0x1F801810;
switch (offset)
{
case 0x00: // GP0 - Drawing commands and VRAM access
WriteGP0(value);
break;
case 0x04: // GP1 - Display control commands
WriteGP1(value);
break;
}
}
public ushort Read16(uint address) => (ushort)(Read32(address & ~3u) >> (int)((address & 2) << 3));
public byte Read8(uint address) => (byte)(Read32(address & ~3u) >> (int)((address & 3) << 3));
public void Write16(uint address, ushort value) { /* Not used for GPU */ }
public void Write8(uint address, byte value) { /* Not used for GPU */ }
#endregion
#region GP0 (Drawing Commands)
private uint ReadGP0()
{
// Return data from GPUREAD FIFO
if (_gpuReadFifo.Count > 0)
return _gpuReadFifo.Dequeue();
return 0xFFFFFFFF;
}
private void WriteGP0(uint value)
{
if (_commandParametersRemaining > 0)
{
// Accumulating parameters for current command
_commandParameters.Add(value);
_commandParametersRemaining--;
if (_commandParametersRemaining == 0)
{
// All parameters received, execute command
ExecuteGP0Command(_currentCommand, _commandParameters.ToArray());
_commandParameters.Clear();
}
}
else
{
// Start new command
uint command = value >> 24;
_currentCommand = value;
int paramCount = GetGP0ParameterCount(command);
if (paramCount > 0)
{
// Multi-word command - wait for parameters
_commandParametersRemaining = paramCount;
_commandParameters.Clear();
}
else
{
// Single-word command - execute immediately
ExecuteGP0Command(value, Array.Empty<uint>());
}
}
}
private int GetGP0ParameterCount(uint command)
{
return command switch
{
0x00 => 0, // NOP
0x01 => 0, // Clear cache
0x02 => 2, // Fill rectangle (2 additional words)
0xA0 => -1, // Copy CPU→VRAM (variable, first param has size)
0xC0 => 2, // Copy VRAM→CPU
0xE1 => 0, // Draw mode
0xE2 => 0, // Texture window
0xE3 => 0, // Draw area start
0xE4 => 0, // Draw area end
0xE5 => 0, // Draw offset
0xE6 => 0, // Mask bit
_ when (command >= 0x20 && command <= 0x3F) => GetPolygonParameterCount(command),
_ => 0
};
}
private int GetPolygonParameterCount(uint command)
{
bool textured = (command & 0x04) != 0;
bool quad = (command & 0x08) != 0;
bool gouraud = (command & 0x10) != 0;
int vertexCount = quad ? 4 : 3;
int wordsPerVertex = gouraud ? 2 : 1;
if (textured) wordsPerVertex++;
return vertexCount * wordsPerVertex - 1; // -1 because first word is command
}
private void ExecuteGP0Command(uint commandWord, uint[] parameters)
{
uint command = commandWord >> 24;
switch (command)
{
case 0x00: // NOP
break;
case 0x01: // Clear cache
// Nothing to do for now (no texture cache implemented)
break;
case 0x02: // Fill rectangle
ExecuteFillRectangle(commandWord, parameters);
break;
case 0xA0: // Copy rectangle CPU→VRAM
ExecuteCpuToVram(commandWord, parameters);
break;
case 0xC0: // Copy rectangle VRAM→CPU
ExecuteVramToCpu(commandWord, parameters);
break;
case 0xE1: // Draw mode
_state.DrawMode = commandWord & 0xFFFFFF;
break;
case 0xE2: // Texture window
{
_state.TextureWindowMaskX = (int)(commandWord & 0x1F);
_state.TextureWindowMaskY = (int)((commandWord >> 5) & 0x1F);
_state.TextureWindowOffsetX = (int)((commandWord >> 10) & 0x1F);
_state.TextureWindowOffsetY = (int)((commandWord >> 15) & 0x1F);
}
break;
case 0xE3: // Draw area start
_state.DrawAreaLeft = (int)(commandWord & 0x3FF);
_state.DrawAreaTop = (int)((commandWord >> 10) & 0x1FF);
break;
case 0xE4: // Draw area end
_state.DrawAreaRight = (int)(commandWord & 0x3FF);
_state.DrawAreaBottom = (int)((commandWord >> 10) & 0x1FF);
break;
case 0xE5: // Draw offset
_state.DrawOffsetX = (short)(((short)((commandWord & 0x7FF) << 5)) >> 5); // Sign extend 11 bits
_state.DrawOffsetY = (short)(((short)(((commandWord >> 11) & 0x7FF) << 5)) >> 5);
break;
case 0xE6: // Mask bit
_state.MaskWhileDrawing = (commandWord & 0x01) != 0;
_state.CheckMaskBeforeDraw = (commandWord & 0x02) != 0;
break;
default:
// Unknown command - ignore for now
break;
}
}
private void ExecuteFillRectangle(uint commandWord, uint[] parameters)
{
if (parameters.Length < 2) return;
// Extract color (RGB from command word)
ushort color = (ushort)(commandWord & 0xFFFF);
// Extract position and size
int x = (int)(parameters[0] & 0xFFFF);
int y = (int)((parameters[0] >> 16) & 0xFFFF);
int width = (int)(parameters[1] & 0xFFFF);
int height = (int)((parameters[1] >> 16) & 0xFFFF);
// Fill VRAM rectangle
for (int dy = 0; dy < height; dy++)
{
for (int dx = 0; dx < width; dx++)
{
int px = (x + dx) & 0x3FF; // Wrap at 1024
int py = (y + dy) & 0x1FF; // Wrap at 512
_vram[py * 1024 + px] = color;
}
}
}
private void ExecuteCpuToVram(uint commandWord, uint[] parameters)
{
// CPU→VRAM transfer - uploads textures and images to VRAM
// Command format: 0xA0000000
// Parameters: [0] = X,Y position, [1] = Width,Height, [2...] = pixel data
if (parameters.Length < 2) return;
// Extract position and size
int x = (int)(parameters[0] & 0xFFFF);
int y = (int)((parameters[0] >> 16) & 0xFFFF);
int width = (int)(parameters[1] & 0xFFFF);
int height = (int)((parameters[1] >> 16) & 0xFFFF);
// Calculate total pixels needed (2 pixels per 32-bit word)
int totalPixels = width * height;
int wordsNeeded = (totalPixels + 1) / 2; // Round up
// Check if we have enough data
if (parameters.Length < 2 + wordsNeeded)
return; // Not enough data yet
// Transfer pixels from parameters to VRAM
int paramIndex = 2;
for (int dy = 0; dy < height; dy++)
{
for (int dx = 0; dx < width; dx += 2)
{
if (paramIndex >= parameters.Length)
return; // Ran out of data
uint pixelWord = parameters[paramIndex++];
// Extract two 16-bit pixels from the 32-bit word
ushort pixel1 = (ushort)(pixelWord & 0xFFFF);
ushort pixel2 = (ushort)((pixelWord >> 16) & 0xFFFF);
// Write first pixel
int px1 = (x + dx) & 0x3FF; // Wrap at 1024
int py1 = (y + dy) & 0x1FF; // Wrap at 512
_vram[py1 * 1024 + px1] = pixel1;
// Write second pixel if within width
if (dx + 1 < width)
{
int px2 = (x + dx + 1) & 0x3FF;
_vram[py1 * 1024 + px2] = pixel2;
}
}
}
}
private void ExecuteVramToCpu(uint commandWord, uint[] parameters)
{
if (parameters.Length < 2) return;
// Extract position and size
int x = (int)(parameters[0] & 0xFFFF);
int y = (int)((parameters[0] >> 16) & 0xFFFF);
int width = (int)(parameters[1] & 0xFFFF);
int height = (int)((parameters[1] >> 16) & 0xFFFF);
// Copy VRAM to GPUREAD FIFO
for (int dy = 0; dy < height; dy++)
{
for (int dx = 0; dx < width; dx += 2)
{
int px1 = (x + dx) & 0x3FF;
int py1 = (y + dy) & 0x1FF;
int px2 = (x + dx + 1) & 0x3FF;
// Pack two 16-bit pixels into one 32-bit word
ushort pixel1 = _vram[py1 * 1024 + px1];
ushort pixel2 = (dx + 1 < width) ? _vram[py1 * 1024 + px2] : (ushort)0;
uint packed = (uint)(pixel1 | (pixel2 << 16));
_gpuReadFifo.Enqueue(packed);
}
}
}
#endregion
#region GP1 (Display Control)
private uint ReadGP1()
{
// Return GPUSTAT
uint gpustat = 0;
// Bit 26: Ready to receive command (always ready for now)
gpustat |= (1u << 26);
// Bit 27: Ready to send VRAM to CPU
gpustat |= (_gpuReadFifo.Count > 0 ? 1u : 0u) << 27;
// Bit 28: Ready to receive DMA block (always ready for now)
gpustat |= (1u << 28);
// Bits 19-20: DMA direction
gpustat |= ((uint)_state.DmaDirection & 0x3) << 29;
// Bit 23: Display enabled
gpustat |= (_state.DisplayEnabled ? 0u : 1u) << 23;
// Bit 31: IRQ requested
gpustat |= (_state.IrqRequested ? 1u : 0u) << 31;
return gpustat;
}
private void WriteGP1(uint value)
{
uint command = value >> 24;
uint param = value & 0xFFFFFF;
switch (command)
{
case 0x00: // Reset GPU
Reset();
break;
case 0x01: // Reset command buffer
_commandFifo.Clear();
_commandParameters.Clear();
_commandParametersRemaining = 0;
break;
case 0x02: // Acknowledge IRQ
_state.IrqRequested = false;
break;
case 0x03: // Display enable
_state.DisplayEnabled = (param & 0x01) == 0;
break;
case 0x04: // DMA direction
_state.DmaDirection = (int)(param & 0x03);
break;
case 0x05: // Display area start
_state.DisplayAreaX = (int)(param & 0x3FF);
_state.DisplayAreaY = (int)((param >> 10) & 0x1FF);
break;
case 0x06: // Horizontal display range
_state.HorizontalStart = (int)(param & 0xFFF);
_state.HorizontalEnd = (int)((param >> 12) & 0xFFF);
break;
case 0x07: // Vertical display range
_state.VerticalStart = (int)(param & 0x3FF);
_state.VerticalEnd = (int)((param >> 10) & 0x3FF);
break;
case 0x08: // Display mode
_state.VideoMode = (int)(param & 0xFF);
break;
default:
if (command >= 0x10 && command <= 0x1F)
{
// Get GPU info - push response to GPUREAD
_gpuReadFifo.Enqueue(GetGpuInfo(command));
}
break;
}
}
private uint GetGpuInfo(uint infoType)
{
return infoType switch
{
0x10 => 0x02, // GPU version (dummy value)
0x11 => 0x00, // Texture disable
0x12 => (uint)((_state.TextureWindowMaskX & 0x1F) |
((_state.TextureWindowMaskY & 0x1F) << 5) |
((_state.TextureWindowOffsetX & 0x1F) << 10) |
((_state.TextureWindowOffsetY & 0x1F) << 15)),
0x13 => (uint)((_state.DrawAreaLeft & 0x3FF) | ((_state.DrawAreaTop & 0x1FF) << 10)),
0x14 => (uint)((_state.DrawAreaRight & 0x3FF) | ((_state.DrawAreaBottom & 0x1FF) << 10)),
0x15 => (uint)((ushort)_state.DrawOffsetX | ((ushort)_state.DrawOffsetY << 11)),
0x16 => 0x02, // GPU type (dummy value)
_ => 0x00
};
}
#endregion
#region VRAM Access
/// <summary>
/// Read a pixel from VRAM.
/// </summary>
public ushort ReadVram(int x, int y)
{
if (x < 0 || x >= 1024 || y < 0 || y >= 512)
return 0;
return _vram[y * 1024 + x];
}
/// <summary>
/// Write a pixel to VRAM.
/// </summary>
public void WriteVram(int x, int y, ushort color)
{
if (x < 0 || x >= 1024 || y < 0 || y >= 512)
return;
_vram[y * 1024 + x] = color;
}
#endregion
/// <summary>
/// GPU state information.
/// </summary>
public GpuState State => _state;
}
/// <summary>
/// GPU state structure.
/// </summary>
public class GpuState
{
public bool DisplayEnabled { get; set; }
public int DmaDirection { get; set; }
// Drawing environment
public uint DrawMode { get; set; }
public int TextureWindowMaskX { get; set; }
public int TextureWindowMaskY { get; set; }
public int TextureWindowOffsetX { get; set; }
public int TextureWindowOffsetY { get; set; }
public int DrawAreaLeft { get; set; }
public int DrawAreaTop { get; set; }
public int DrawAreaRight { get; set; }
public int DrawAreaBottom { get; set; }
public short DrawOffsetX { get; set; }
public short DrawOffsetY { get; set; }
public bool MaskWhileDrawing { get; set; }
public bool CheckMaskBeforeDraw { get; set; }
// Display configuration
public int DisplayAreaX { get; set; }
public int DisplayAreaY { get; set; }
public int HorizontalStart { get; set; }
public int HorizontalEnd { get; set; }
public int VerticalStart { get; set; }
public int VerticalEnd { get; set; }
public int VideoMode { get; set; }
// Interrupts
public bool IrqRequested { get; set; }
}
+187
View File
@@ -0,0 +1,187 @@
namespace Yaroze.Core.Interfaces;
/// <summary>
/// Extended interface for code analysis and frontend integration.
/// Provides hooks for function discovery, call tracking, and cross-reference building.
/// </summary>
public interface IAnalysisSink
{
/// <summary>
/// Called when an instruction is executed.
/// Provides detailed instruction information for analysis.
/// </summary>
/// <param name="info">Detailed instruction execution information</param>
void OnInstructionExecute(InstructionInfo info);
/// <summary>
/// Called when a memory access occurs (read or write).
/// </summary>
/// <param name="access">Memory access information</param>
void OnMemoryAccess(MemoryAccessInfo access);
/// <summary>
/// Called when a function call is detected (JAL, JALR).
/// </summary>
/// <param name="fromPc">Address of the call instruction</param>
/// <param name="targetPc">Target function address</param>
/// <param name="isRegisterCall">True if JALR (register indirect), false if JAL (direct)</param>
void OnFunctionCall(uint fromPc, uint targetPc, bool isRegisterCall);
/// <summary>
/// Called when a function return is detected (JR $ra).
/// </summary>
/// <param name="fromPc">Address of the return instruction</param>
/// <param name="returnAddress">Address being returned to</param>
void OnFunctionReturn(uint fromPc, uint returnAddress);
/// <summary>
/// Called when a branch is taken.
/// </summary>
/// <param name="fromPc">Address of the branch instruction</param>
/// <param name="targetPc">Branch target address</param>
/// <param name="taken">True if branch was taken, false if not taken</param>
void OnBranch(uint fromPc, uint targetPc, bool taken);
/// <summary>
/// Called when execution reaches a potential string address.
/// </summary>
/// <param name="address">Address of the potential string</param>
/// <param name="accessType">How the string was accessed (load, store, etc.)</param>
void OnPossibleStringReference(uint address, string accessType);
}
/// <summary>
/// Detailed instruction execution information.
/// </summary>
public class InstructionInfo
{
public uint PC { get; set; }
public uint InstructionWord { get; set; }
public string? Disassembly { get; set; }
public InstructionType Type { get; set; }
public uint[]? RegistersRead { get; set; }
public uint[]? RegistersWritten { get; set; }
public uint? MemoryAddress { get; set; }
public uint? BranchTarget { get; set; }
public bool InDelaySlot { get; set; }
}
/// <summary>
/// Memory access information.
/// </summary>
public class MemoryAccessInfo
{
public uint Address { get; set; }
public uint Value { get; set; }
public int Size { get; set; }
public bool IsWrite { get; set; }
public uint PC { get; set; }
}
/// <summary>
/// Instruction type classification.
/// </summary>
public enum InstructionType
{
Arithmetic,
Logic,
Shift,
Load,
Store,
Branch,
Jump,
Multiply,
Divide,
Coprocessor,
Exception,
Other
}
/// <summary>
/// Composite sink that forwards to both ITraceSink and IAnalysisSink.
/// </summary>
public class CompositeAnalysisSink : ITraceSink
{
private readonly ITraceSink? _traceSink;
private readonly IAnalysisSink? _analysisSink;
private uint _currentPC;
public CompositeAnalysisSink(ITraceSink? traceSink = null, IAnalysisSink? analysisSink = null)
{
_traceSink = traceSink;
_analysisSink = analysisSink;
}
public void TraceInstruction(uint pc, uint instruction, string? disassembly = null)
{
// Track current PC for memory access events
_currentPC = pc;
_traceSink?.TraceInstruction(pc, instruction, disassembly);
// Also forward to analysis sink with more detail
if (_analysisSink != null)
{
var info = new InstructionInfo
{
PC = pc,
InstructionWord = instruction,
Disassembly = disassembly,
Type = ClassifyInstruction(instruction)
};
_analysisSink.OnInstructionExecute(info);
}
}
public void TraceMemoryRead(uint address, uint value, int size)
{
_traceSink?.TraceMemoryRead(address, value, size);
_analysisSink?.OnMemoryAccess(new MemoryAccessInfo
{
Address = address,
Value = value,
Size = size,
IsWrite = false,
PC = _currentPC
});
}
public void TraceMemoryWrite(uint address, uint value, int size)
{
_traceSink?.TraceMemoryWrite(address, value, size);
_analysisSink?.OnMemoryAccess(new MemoryAccessInfo
{
Address = address,
Value = value,
Size = size,
IsWrite = true,
PC = _currentPC
});
}
public void TraceException(string exceptionType, uint pc)
{
_traceSink?.TraceException(exceptionType, pc);
}
private InstructionType ClassifyInstruction(uint instruction)
{
uint opcode = (instruction >> 26) & 0x3F;
return opcode switch
{
0x00 => InstructionType.Arithmetic, // SPECIAL (R-type)
0x02 or 0x03 => InstructionType.Jump, // J, JAL
0x04 or 0x05 or 0x06 or 0x07 => InstructionType.Branch, // BEQ, BNE, BLEZ, BGTZ
0x08 or 0x09 or 0x0A or 0x0B => InstructionType.Arithmetic, // ADDI, ADDIU, SLTI, SLTIU
0x0C or 0x0D or 0x0E => InstructionType.Logic, // ANDI, ORI, XORI
0x0F => InstructionType.Load, // LUI
0x10 or 0x12 => InstructionType.Coprocessor, // COP0, COP2
0x20 or 0x21 or 0x22 or 0x23 or 0x24 or 0x25 or 0x26 => InstructionType.Load,
0x28 or 0x29 or 0x2A or 0x2B or 0x2E => InstructionType.Store,
_ => InstructionType.Other
};
}
}
+37
View File
@@ -0,0 +1,37 @@
namespace Yaroze.Core.Interfaces;
/// <summary>
/// Memory bus interface for DMA and other devices that need to access memory.
/// </summary>
public interface IBus
{
/// <summary>
/// Read a 32-bit word from the specified virtual address.
/// </summary>
uint Read32(uint address);
/// <summary>
/// Read a 16-bit halfword from the specified virtual address.
/// </summary>
ushort Read16(uint address);
/// <summary>
/// Read an 8-bit byte from the specified virtual address.
/// </summary>
byte Read8(uint address);
/// <summary>
/// Write a 32-bit word to the specified virtual address.
/// </summary>
void Write32(uint address, uint value);
/// <summary>
/// Write a 16-bit halfword to the specified virtual address.
/// </summary>
void Write16(uint address, ushort value);
/// <summary>
/// Write an 8-bit byte to the specified virtual address.
/// </summary>
void Write8(uint address, byte value);
}
+57
View File
@@ -0,0 +1,57 @@
namespace Yaroze.Core.Interfaces;
/// <summary>
/// Interface for devices that can be mapped to the memory bus.
/// All memory-mapped hardware (RAM, ROM, I/O registers) implement this interface.
/// </summary>
public interface IBusDevice
{
/// <summary>
/// Read a 32-bit word from the device.
/// </summary>
/// <param name="address">Physical address within the device's address range</param>
/// <returns>32-bit value at the address</returns>
uint Read32(uint address);
/// <summary>
/// Read a 16-bit halfword from the device.
/// </summary>
/// <param name="address">Physical address within the device's address range</param>
/// <returns>16-bit value at the address</returns>
ushort Read16(uint address);
/// <summary>
/// Read an 8-bit byte from the device.
/// </summary>
/// <param name="address">Physical address within the device's address range</param>
/// <returns>8-bit value at the address</returns>
byte Read8(uint address);
/// <summary>
/// Write a 32-bit word to the device.
/// </summary>
/// <param name="address">Physical address within the device's address range</param>
/// <param name="value">32-bit value to write</param>
void Write32(uint address, uint value);
/// <summary>
/// Write a 16-bit halfword to the device.
/// </summary>
/// <param name="address">Physical address within the device's address range</param>
/// <param name="value">16-bit value to write</param>
void Write16(uint address, ushort value);
/// <summary>
/// Write an 8-bit byte to the device.
/// </summary>
/// <param name="address">Physical address within the device's address range</param>
/// <param name="value">8-bit value to write</param>
void Write8(uint address, byte value);
/// <summary>
/// Check if this device contains the specified physical address.
/// </summary>
/// <param name="address">Physical address to check</param>
/// <returns>True if this device handles this address</returns>
bool Contains(uint address);
}
+39
View File
@@ -0,0 +1,39 @@
namespace Yaroze.Core.Interfaces;
/// <summary>
/// Interface for receiving execution traces and analysis data.
/// Used by the frontend for disassembly, decompilation, and debugging.
/// </summary>
public interface ITraceSink
{
/// <summary>
/// Called when an instruction is about to be executed.
/// </summary>
/// <param name="pc">Program counter (address of the instruction)</param>
/// <param name="instruction">Raw 32-bit instruction word</param>
/// <param name="disassembly">Human-readable disassembly text (optional)</param>
void TraceInstruction(uint pc, uint instruction, string? disassembly = null);
/// <summary>
/// Called when memory is read.
/// </summary>
/// <param name="address">Physical address being read</param>
/// <param name="value">Value that was read</param>
/// <param name="size">Size of the read in bytes (1, 2, or 4)</param>
void TraceMemoryRead(uint address, uint value, int size);
/// <summary>
/// Called when memory is written.
/// </summary>
/// <param name="address">Physical address being written</param>
/// <param name="value">Value being written</param>
/// <param name="size">Size of the write in bytes (1, 2, or 4)</param>
void TraceMemoryWrite(uint address, uint value, int size);
/// <summary>
/// Called when an exception occurs.
/// </summary>
/// <param name="exceptionType">Type of exception</param>
/// <param name="pc">Program counter where exception occurred</param>
void TraceException(string exceptionType, uint pc);
}
@@ -0,0 +1,129 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.Interrupts;
/// <summary>
/// PlayStation 1 Interrupt Controller.
/// Manages hardware interrupts from various devices.
/// </summary>
public class InterruptController : IBusDevice
{
private ushort _istat; // Interrupt Status Register (0x1F801070)
private ushort _imask; // Interrupt Mask Register (0x1F801074)
public InterruptController()
{
Reset();
}
public void Reset()
{
_istat = 0;
_imask = 0;
}
#region IBusDevice Implementation
public bool Contains(uint address)
{
// Interrupt registers at 0x1F801070 and 0x1F801074
return address == 0x1F801070 || address == 0x1F801074;
}
public uint Read32(uint address)
{
return address switch
{
0x1F801070 => _istat,
0x1F801074 => _imask,
_ => 0
};
}
public void Write32(uint address, uint value)
{
switch (address)
{
case 0x1F801070: // I_STAT
// Writing 0 bits clears them, writing 1 bits has no effect
_istat &= (ushort)(value & 0xFFFF);
break;
case 0x1F801074: // I_MASK
_imask = (ushort)(value & 0xFFFF);
break;
}
}
public ushort Read16(uint address) => (ushort)Read32(address);
public byte Read8(uint address) => (byte)Read32(address);
public void Write16(uint address, ushort value) => Write32(address, value);
public void Write8(uint address, byte value) => Write32(address, value);
#endregion
#region Interrupt Management
/// <summary>
/// Raise a hardware interrupt.
/// </summary>
public void RaiseInterrupt(InterruptType type)
{
_istat |= (ushort)(1 << (int)type);
}
/// <summary>
/// Clear a hardware interrupt.
/// </summary>
public void ClearInterrupt(InterruptType type)
{
_istat &= (ushort)~(1 << (int)type);
}
/// <summary>
/// Check if any interrupts are pending (unmasked interrupts that are active).
/// </summary>
public bool HasPendingInterrupt()
{
ushort pending = (ushort)(_istat & _imask);
return pending != 0;
}
/// <summary>
/// Get pending interrupt bits (masked).
/// </summary>
public ushort GetPendingInterrupts()
{
return (ushort)(_istat & _imask);
}
#endregion
/// <summary>
/// Interrupt Status Register.
/// </summary>
public ushort ISTAT => _istat;
/// <summary>
/// Interrupt Mask Register.
/// </summary>
public ushort IMASK => _imask;
}
/// <summary>
/// PlayStation 1 interrupt types.
/// </summary>
public enum InterruptType
{
VBlank = 0, // IRQ0: Vertical blank
Gpu = 1, // IRQ1: GPU (command completion)
CdRom = 2, // IRQ2: CD-ROM
Dma = 3, // IRQ3: DMA
Timer0 = 4, // IRQ4: Timer 0 (dotclock)
Timer1 = 5, // IRQ5: Timer 1 (hblank)
Timer2 = 6, // IRQ6: Timer 2 (sysclock/8)
Controller = 7, // IRQ7: Controller and memory card
Sio = 8, // IRQ8: SIO
Spu = 9, // IRQ9: SPU
Pio = 10 // IRQ10: PIO (lightgun)
}
+324
View File
@@ -0,0 +1,324 @@
using System;
using System.Collections.Generic;
using Yaroze.Core.CPU;
namespace Yaroze.Core.JIT;
/// <summary>
/// Identifies and caches basic blocks for JIT compilation.
/// A basic block is a sequence of instructions with:
/// - Single entry point (first instruction)
/// - Single exit point (last instruction)
/// - No branches except at the end
/// </summary>
public class BasicBlockScanner
{
private readonly byte[] _memory;
private readonly uint _baseAddress;
private readonly HashSet<uint> _blockStarts = new();
private readonly Dictionary<uint, BasicBlock> _blocks = new();
public BasicBlockScanner(byte[] memory, uint baseAddress)
{
_memory = memory;
_baseAddress = baseAddress;
}
/// <summary>
/// All identified basic blocks.
/// </summary>
public IReadOnlyDictionary<uint, BasicBlock> Blocks => _blocks;
/// <summary>
/// Identifies a basic block starting at the given address.
/// </summary>
public BasicBlock IdentifyBlock(uint startAddress)
{
// Return cached block if already identified
if (_blocks.TryGetValue(startAddress, out var cached))
{
return cached;
}
var block = new BasicBlock
{
StartAddress = startAddress,
Instructions = new List<uint>()
};
uint currentAddress = startAddress;
bool blockEnded = false;
while (!blockEnded)
{
uint instruction = ReadInstruction(currentAddress);
if (instruction == 0)
{
// Invalid memory region - end block
break;
}
block.Instructions.Add(currentAddress);
var instr = new Instruction(instruction);
// Check if this instruction ends the block
switch (instr.Opcode)
{
// Unconditional jumps end blocks
case Opcode.J:
case Opcode.JAL:
block.EndAddress = currentAddress + 4; // Include delay slot
block.ExitType = BlockExitType.Jump;
blockEnded = true;
// Add delay slot instruction
currentAddress += 4;
if (currentAddress < _baseAddress + _memory.Length)
{
block.Instructions.Add(currentAddress);
}
break;
// SPECIAL opcode - check funct field
case Opcode.SPECIAL:
switch (instr.Funct)
{
case Funct.JR:
case Funct.JALR:
block.EndAddress = currentAddress + 4; // Include delay slot
block.ExitType = BlockExitType.Jump;
blockEnded = true;
// Add delay slot instruction
currentAddress += 4;
if (currentAddress < _baseAddress + _memory.Length)
{
block.Instructions.Add(currentAddress);
}
break;
case Funct.SYSCALL:
case Funct.BREAK:
block.EndAddress = currentAddress;
block.ExitType = BlockExitType.Exception;
blockEnded = true;
break;
default:
// Other SPECIAL instructions - continue
currentAddress += 4;
break;
}
break;
// REGIMM opcode - check rt field for branch type
case Opcode.REGIMM:
switch (instr.Rt)
{
case RegImmRt.BLTZ:
case RegImmRt.BGEZ:
case RegImmRt.BLTZAL:
case RegImmRt.BGEZAL:
block.EndAddress = currentAddress + 4; // Include delay slot
block.ExitType = BlockExitType.ConditionalBranch;
block.BranchTarget = instr.BranchTarget(currentAddress);
blockEnded = true;
// Add delay slot instruction
currentAddress += 4;
if (currentAddress < _baseAddress + _memory.Length)
{
block.Instructions.Add(currentAddress);
}
break;
default:
// Other REGIMM instructions - continue
currentAddress += 4;
break;
}
break;
// Conditional branches can end blocks (conservative approach)
case Opcode.BEQ:
case Opcode.BNE:
case Opcode.BLEZ:
case Opcode.BGTZ:
block.EndAddress = currentAddress + 4; // Include delay slot
block.ExitType = BlockExitType.ConditionalBranch;
block.BranchTarget = instr.BranchTarget(currentAddress);
blockEnded = true;
// Add delay slot instruction
currentAddress += 4;
if (currentAddress < _baseAddress + _memory.Length)
{
block.Instructions.Add(currentAddress);
}
break;
default:
// Continue to next instruction
currentAddress += 4;
// Also end block if we've hit another known block start
if (_blockStarts.Contains(currentAddress))
{
block.EndAddress = currentAddress - 4;
block.ExitType = BlockExitType.FallThrough;
blockEnded = true;
}
// Safety limit: max 100 instructions per block
if (block.Instructions.Count >= 100)
{
block.EndAddress = currentAddress - 4;
block.ExitType = BlockExitType.FallThrough;
blockEnded = true;
}
break;
}
}
// If we haven't set EndAddress, set it now
if (block.EndAddress == 0 && block.Instructions.Count > 0)
{
block.EndAddress = block.Instructions[block.Instructions.Count - 1];
block.ExitType = BlockExitType.FallThrough;
}
// Cache the block
_blocks[startAddress] = block;
_blockStarts.Add(startAddress);
return block;
}
/// <summary>
/// Marks an address as a potential block start (e.g., branch target).
/// </summary>
public void MarkBlockStart(uint address)
{
_blockStarts.Add(address);
}
/// <summary>
/// Scans a range of code to identify all basic blocks.
/// </summary>
public void ScanRange(uint startAddress, uint endAddress)
{
var queue = new Queue<uint>();
var visited = new HashSet<uint>();
queue.Enqueue(startAddress);
while (queue.Count > 0)
{
uint address = queue.Dequeue();
if (visited.Contains(address))
continue;
visited.Add(address);
var block = IdentifyBlock(address);
// Enqueue successor blocks
switch (block.ExitType)
{
case BlockExitType.ConditionalBranch:
// Branch can go to target or fall through
if (block.BranchTarget.HasValue && block.BranchTarget.Value <= endAddress)
{
queue.Enqueue(block.BranchTarget.Value);
}
if (block.EndAddress + 4 <= endAddress)
{
queue.Enqueue(block.EndAddress + 4); // Fall through
}
break;
case BlockExitType.FallThrough:
if (block.EndAddress + 4 <= endAddress)
{
queue.Enqueue(block.EndAddress + 4);
}
break;
// Jump and Exception don't have obvious successors to queue
}
}
}
private uint ReadInstruction(uint address)
{
int offset = (int)(address - _baseAddress);
if (offset < 0 || offset + 3 >= _memory.Length)
return 0;
return BitConverter.ToUInt32(_memory, offset);
}
/// <summary>
/// Gets statistics about identified blocks.
/// </summary>
public BlockScanStats GetStats()
{
int totalInstructions = 0;
int maxBlockSize = 0;
foreach (var block in _blocks.Values)
{
totalInstructions += block.Instructions.Count;
maxBlockSize = Math.Max(maxBlockSize, block.Instructions.Count);
}
return new BlockScanStats
{
BlockCount = _blocks.Count,
TotalInstructions = totalInstructions,
AverageBlockSize = _blocks.Count > 0 ? (double)totalInstructions / _blocks.Count : 0,
MaxBlockSize = maxBlockSize
};
}
}
/// <summary>
/// Represents a basic block of code.
/// </summary>
public class BasicBlock
{
public uint StartAddress { get; set; }
public uint EndAddress { get; set; }
public List<uint> Instructions { get; set; } = new();
public BlockExitType ExitType { get; set; }
public uint? BranchTarget { get; set; } // For conditional branches
public override string ToString()
{
return $"Block 0x{StartAddress:X8}-0x{EndAddress:X8} ({Instructions.Count} instructions, exit: {ExitType})";
}
}
/// <summary>
/// How a basic block exits.
/// </summary>
public enum BlockExitType
{
FallThrough, // Continues to next instruction
Jump, // Unconditional jump (J, JR, JAL, JALR)
ConditionalBranch, // Conditional branch (BEQ, BNE, etc.)
Exception // Syscall, break, or exception
}
/// <summary>
/// Statistics about scanned basic blocks.
/// </summary>
public class BlockScanStats
{
public int BlockCount { get; set; }
public int TotalInstructions { get; set; }
public double AverageBlockSize { get; set; }
public int MaxBlockSize { get; set; }
public override string ToString()
{
return $"Blocks: {BlockCount}, Instructions: {TotalInstructions}, Avg size: {AverageBlockSize:F1}, Max size: {MaxBlockSize}";
}
}
File diff suppressed because it is too large Load Diff
+363
View File
@@ -0,0 +1,363 @@
using System;
using System.Linq;
using Yaroze.Core.CPU;
using Yaroze.Core.Memory;
namespace Yaroze.Core.JIT;
/// <summary>
/// Verifies JIT compilation correctness by running interpreter and JIT side-by-side
/// and comparing results. This ensures the JIT produces identical results to the
/// accurate interpreter.
/// </summary>
public class LockstepVerifier
{
private readonly Cpu _interpreterCpu;
private readonly Bus _interpreterBus;
private readonly Cpu _jitCpu;
private readonly Bus _jitBus;
private readonly JitCompiler _jitCompiler;
private readonly byte[] _memory;
private readonly uint _baseAddress;
private int _blocksVerified;
private int _mismatches;
private int _instructionsVerified;
public LockstepVerifier(byte[] memory, uint baseAddress)
{
_memory = memory;
_baseAddress = baseAddress;
// Create interpreter CPU and bus
_interpreterBus = new Bus();
_interpreterCpu = new Cpu(_interpreterBus);
// Create JIT CPU and bus
_jitBus = new Bus();
_jitCpu = new Cpu(_jitBus);
_jitCompiler = new JitCompiler(_jitCpu, _jitBus, memory, baseAddress);
}
/// <summary>
/// Number of blocks successfully verified.
/// </summary>
public int BlocksVerified => _blocksVerified;
/// <summary>
/// Number of mismatches detected.
/// </summary>
public int Mismatches => _mismatches;
/// <summary>
/// Total instructions verified.
/// </summary>
public int InstructionsVerified => _instructionsVerified;
/// <summary>
/// Verifies a block at the given address by executing it with both
/// interpreter and JIT, then comparing the results.
/// </summary>
public VerificationResult VerifyBlock(uint startAddress)
{
// Identify the block
var scanner = new BasicBlockScanner(_memory, _baseAddress);
var block = scanner.IdentifyBlock(startAddress);
// Save initial state
var initialState = CpuState.Capture(_interpreterCpu, _interpreterBus);
// Execute with interpreter
_interpreterCpu.LoadState(initialState);
CopyBusState(initialState, _interpreterBus);
foreach (var instructionAddress in block.Instructions)
{
_interpreterCpu.Registers.PC = instructionAddress;
_interpreterCpu.Step();
}
var interpreterFinalState = CpuState.Capture(_interpreterCpu, _interpreterBus);
// Execute with JIT
_jitCpu.LoadState(initialState);
CopyBusState(initialState, _jitBus);
_jitCpu.Registers.PC = startAddress;
var compiledBlock = _jitCompiler.GetOrCompile(startAddress);
compiledBlock.Execute(_jitCpu, _jitBus);
var jitFinalState = CpuState.Capture(_jitCpu, _jitBus);
// Compare states
var comparison = CompareStates(interpreterFinalState, jitFinalState);
_blocksVerified++;
_instructionsVerified += block.Instructions.Count;
if (!comparison.IsMatch)
{
_mismatches++;
}
return new VerificationResult
{
StartAddress = startAddress,
InstructionCount = block.Instructions.Count,
IsMatch = comparison.IsMatch,
Differences = comparison.Differences,
InterpreterState = interpreterFinalState,
JitState = jitFinalState
};
}
/// <summary>
/// Verifies multiple blocks in a range.
/// </summary>
public VerificationSummary VerifyRange(uint startAddress, uint endAddress)
{
var results = new System.Collections.Generic.List<VerificationResult>();
var scanner = new BasicBlockScanner(_memory, _baseAddress);
scanner.ScanRange(startAddress, endAddress);
foreach (var block in scanner.Blocks.Values.OrderBy(b => b.StartAddress))
{
if (block.StartAddress >= startAddress && block.StartAddress <= endAddress)
{
try
{
var result = VerifyBlock(block.StartAddress);
results.Add(result);
}
catch (Exception ex)
{
results.Add(new VerificationResult
{
StartAddress = block.StartAddress,
IsMatch = false,
Differences = new[] { $"Exception: {ex.Message}" }
});
_mismatches++;
}
}
}
return new VerificationSummary
{
TotalBlocks = results.Count,
PassedBlocks = results.Count(r => r.IsMatch),
FailedBlocks = results.Count(r => !r.IsMatch),
TotalInstructions = results.Sum(r => r.InstructionCount),
Results = results
};
}
private StateComparison CompareStates(CpuState state1, CpuState state2)
{
var differences = new System.Collections.Generic.List<string>();
// Compare registers
for (int i = 0; i < 32; i++)
{
if (state1.Registers[i] != state2.Registers[i])
{
differences.Add($"Register ${i}: 0x{state1.Registers[i]:X8} vs 0x{state2.Registers[i]:X8}");
}
}
// Compare PC
if (state1.PC != state2.PC)
{
differences.Add($"PC: 0x{state1.PC:X8} vs 0x{state2.PC:X8}");
}
// Compare HI/LO
if (state1.HI != state2.HI)
{
differences.Add($"HI: 0x{state1.HI:X8} vs 0x{state2.HI:X8}");
}
if (state1.LO != state2.LO)
{
differences.Add($"LO: 0x{state1.LO:X8} vs 0x{state2.LO:X8}");
}
// Compare memory changes (sample key addresses)
foreach (var addr in state1.MemoryChanges.Keys)
{
if (state2.MemoryChanges.TryGetValue(addr, out var value2))
{
if (state1.MemoryChanges[addr] != value2)
{
differences.Add($"Memory[0x{addr:X8}]: 0x{state1.MemoryChanges[addr]:X8} vs 0x{value2:X8}");
}
}
else
{
differences.Add($"Memory[0x{addr:X8}]: 0x{state1.MemoryChanges[addr]:X8} vs <not written>");
}
}
// Check for memory writes in state2 not in state1
foreach (var addr in state2.MemoryChanges.Keys)
{
if (!state1.MemoryChanges.ContainsKey(addr))
{
differences.Add($"Memory[0x{addr:X8}]: <not written> vs 0x{state2.MemoryChanges[addr]:X8}");
}
}
return new StateComparison
{
IsMatch = differences.Count == 0,
Differences = differences.ToArray()
};
}
private void CopyBusState(CpuState state, Bus bus)
{
// Apply any memory changes from the saved state
foreach (var (address, value) in state.MemoryChanges)
{
bus.Write32(address, value);
}
}
/// <summary>
/// Gets statistics about verification.
/// </summary>
public VerificationStats GetStats()
{
return new VerificationStats
{
BlocksVerified = _blocksVerified,
InstructionsVerified = _instructionsVerified,
Mismatches = _mismatches,
SuccessRate = _blocksVerified > 0 ? (double)(_blocksVerified - _mismatches) / _blocksVerified : 0
};
}
}
/// <summary>
/// Captures the complete state of a CPU for comparison.
/// </summary>
public class CpuState
{
public uint[] Registers { get; set; } = new uint[32];
public uint PC { get; set; }
public uint HI { get; set; }
public uint LO { get; set; }
public System.Collections.Generic.Dictionary<uint, uint> MemoryChanges { get; set; } = new();
public static CpuState Capture(Cpu cpu, Bus bus)
{
var state = new CpuState();
// Copy registers
for (uint i = 0; i < 32; i++)
{
state.Registers[i] = cpu.Registers.ReadGPR(i);
}
state.PC = cpu.Registers.PC;
state.HI = cpu.Registers.HI;
state.LO = cpu.Registers.LO;
// Note: We don't capture all memory, only track changes during execution
// This is handled by the verifier tracking writes
return state;
}
}
/// <summary>
/// Extension methods for CPU state management.
/// </summary>
public static class CpuStateExtensions
{
public static void LoadState(this Cpu cpu, CpuState state)
{
for (uint i = 0; i < 32; i++)
{
cpu.Registers.WriteGPR(i, state.Registers[i]);
}
cpu.Registers.PC = state.PC;
cpu.Registers.HI = state.HI;
cpu.Registers.LO = state.LO;
}
}
/// <summary>
/// Result of comparing two CPU states.
/// </summary>
public class StateComparison
{
public bool IsMatch { get; set; }
public string[] Differences { get; set; } = Array.Empty<string>();
public override string ToString()
{
if (IsMatch)
{
return "States match";
}
return $"States differ: {string.Join(", ", Differences)}";
}
}
/// <summary>
/// Result of verifying a single block.
/// </summary>
public class VerificationResult
{
public uint StartAddress { get; set; }
public int InstructionCount { get; set; }
public bool IsMatch { get; set; }
public string[] Differences { get; set; } = Array.Empty<string>();
public CpuState? InterpreterState { get; set; }
public CpuState? JitState { get; set; }
public override string ToString()
{
var status = IsMatch ? "PASS" : "FAIL";
var diffInfo = Differences.Length > 0 ? $" ({Differences.Length} differences)" : "";
return $"Block 0x{StartAddress:X8}: {status}{diffInfo}";
}
}
/// <summary>
/// Summary of verifying multiple blocks.
/// </summary>
public class VerificationSummary
{
public int TotalBlocks { get; set; }
public int PassedBlocks { get; set; }
public int FailedBlocks { get; set; }
public int TotalInstructions { get; set; }
public System.Collections.Generic.List<VerificationResult> Results { get; set; } = new();
public double SuccessRate => TotalBlocks > 0 ? (double)PassedBlocks / TotalBlocks : 0;
public override string ToString()
{
return $"Verification: {PassedBlocks}/{TotalBlocks} passed ({SuccessRate:P0}), {TotalInstructions} instructions";
}
}
/// <summary>
/// Statistics about verification runs.
/// </summary>
public class VerificationStats
{
public int BlocksVerified { get; set; }
public int InstructionsVerified { get; set; }
public int Mismatches { get; set; }
public double SuccessRate { get; set; }
public override string ToString()
{
return $"Verified {BlocksVerified} blocks ({InstructionsVerified} instructions), {Mismatches} mismatches, {SuccessRate:P0} success rate";
}
}
+231
View File
@@ -0,0 +1,231 @@
using Yaroze.Core.CPU;
using Yaroze.Core.Memory;
namespace Yaroze.Core.Loaders;
/// <summary>
/// PlayStation PS-EXE file format loader.
/// Loads PS-X EXE executables into memory and initializes CPU state.
/// </summary>
public class PsExeLoader
{
/// <summary>
/// PS-EXE file header structure.
/// </summary>
public class ExeHeader
{
public string Magic { get; set; } = "";
public uint InitialPC { get; set; }
public uint InitialGP { get; set; }
public uint LoadAddress { get; set; }
public uint FileSize { get; set; }
public uint DataAddress { get; set; }
public uint DataSize { get; set; }
public uint BssAddress { get; set; }
public uint BssSize { get; set; }
public uint StackBase { get; set; }
public uint StackOffset { get; set; }
public byte[] Reserved { get; set; } = new byte[20];
public string MarkerText { get; set; } = "";
}
private const int HeaderSize = 0x800;
private const string ExpectedMagic = "PS-X EXE";
/// <summary>
/// Load a PS-EXE file from a byte array.
/// </summary>
/// <param name="data">Complete file data</param>
/// <param name="bus">Memory bus to load into</param>
/// <param name="cpu">CPU to initialize</param>
/// <returns>Parsed header information</returns>
public static ExeHeader Load(byte[] data, Bus bus, Cpu cpu)
{
if (data.Length < HeaderSize)
{
throw new InvalidDataException($"PS-EXE file too small: {data.Length} bytes (minimum {HeaderSize})");
}
// Parse header
var header = ParseHeader(data);
// Validate header
ValidateHeader(header, data.Length);
// Load executable code/data into RAM
int codeOffset = HeaderSize;
int codeSize = (int)header.FileSize;
if (codeOffset + codeSize > data.Length)
{
throw new InvalidDataException($"File size mismatch: header says {codeSize} bytes, but only {data.Length - codeOffset} available");
}
byte[] codeData = new byte[codeSize];
Array.Copy(data, codeOffset, codeData, 0, codeSize);
// Write to RAM at load address
bus.Ram.WriteBlock(header.LoadAddress, codeData);
// Initialize CPU registers from header
InitializeCpu(cpu, header);
return header;
}
/// <summary>
/// Load a PS-EXE file from a file path.
/// </summary>
public static ExeHeader LoadFromFile(string filePath, Bus bus, Cpu cpu)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"PS-EXE file not found: {filePath}");
}
byte[] data = File.ReadAllBytes(filePath);
return Load(data, bus, cpu);
}
/// <summary>
/// Parse the PS-EXE header from file data.
/// </summary>
private static ExeHeader ParseHeader(byte[] data)
{
var header = new ExeHeader();
// Parse magic string (offset 0x000, 8 bytes)
header.Magic = System.Text.Encoding.ASCII.GetString(data, 0, 8).TrimEnd('\0');
// Parse main header fields (all little-endian)
header.InitialPC = ReadUInt32LE(data, 0x010);
header.InitialGP = ReadUInt32LE(data, 0x014);
header.LoadAddress = ReadUInt32LE(data, 0x018);
header.FileSize = ReadUInt32LE(data, 0x01C);
header.DataAddress = ReadUInt32LE(data, 0x020);
header.DataSize = ReadUInt32LE(data, 0x024);
header.BssAddress = ReadUInt32LE(data, 0x028);
header.BssSize = ReadUInt32LE(data, 0x02C);
header.StackBase = ReadUInt32LE(data, 0x030);
header.StackOffset = ReadUInt32LE(data, 0x034);
// Parse reserved area
Array.Copy(data, 0x038, header.Reserved, 0, 20);
// Parse marker text (offset 0x04C onwards, until end of header)
int markerLength = Math.Min(HeaderSize - 0x04C, 256);
header.MarkerText = System.Text.Encoding.ASCII.GetString(data, 0x04C, markerLength).TrimEnd('\0');
return header;
}
/// <summary>
/// Validate PS-EXE header.
/// </summary>
private static void ValidateHeader(ExeHeader header, int fileSize)
{
// Check magic
if (header.Magic != ExpectedMagic)
{
throw new InvalidDataException($"Invalid PS-EXE magic: expected '{ExpectedMagic}', got '{header.Magic}'");
}
// Check file size
if (header.FileSize == 0)
{
throw new InvalidDataException("PS-EXE has zero file size");
}
if (HeaderSize + header.FileSize > fileSize)
{
throw new InvalidDataException($"PS-EXE file size mismatch: header says {header.FileSize} bytes, but file only has {fileSize - HeaderSize} after header");
}
// Check load address is in RAM (typically 0x80000000 - 0x801FFFFF)
if (header.LoadAddress < 0x80000000 || header.LoadAddress >= 0x80200000)
{
// Warning: unusual load address, but allow it
Console.WriteLine($"Warning: Unusual load address 0x{header.LoadAddress:X8}");
}
// Check initial PC is within loaded range
uint loadEnd = header.LoadAddress + header.FileSize;
if (header.InitialPC < header.LoadAddress || header.InitialPC >= loadEnd)
{
throw new InvalidDataException($"Initial PC 0x{header.InitialPC:X8} is outside loaded region (0x{header.LoadAddress:X8} - 0x{loadEnd:X8})");
}
}
/// <summary>
/// Initialize CPU state from PS-EXE header.
/// </summary>
private static void InitializeCpu(Cpu cpu, ExeHeader header)
{
// Set program counter to entry point
cpu.Registers.PC = header.InitialPC;
// Set global pointer ($gp / $28)
cpu.Registers.WriteGPR(28, header.InitialGP);
// Set stack pointer ($sp / $29)
// Stack pointer = base + offset
// If both are zero, use default stack (top of RAM minus some space)
uint stackPointer;
if (header.StackBase == 0 && header.StackOffset == 0)
{
// Default: top of 2MB RAM minus 4KB
stackPointer = 0x801FFF00;
}
else
{
stackPointer = header.StackBase + header.StackOffset;
}
cpu.Registers.WriteGPR(29, stackPointer);
// Set frame pointer ($fp / $30) to same as stack pointer initially
cpu.Registers.WriteGPR(30, stackPointer);
// Clear return address ($ra / $31) - no return from main
cpu.Registers.WriteGPR(31, 0);
// Note: BSS section (uninitialized data) handling
// The BIOS typically clears BSS to zero, but since we're not using BIOS,
// we should clear it ourselves if BssAddress and BssSize are specified
if (header.BssSize > 0 && header.BssAddress != 0)
{
// Clear BSS section to zero
byte[] zeros = new byte[header.BssSize];
cpu.Bus.Ram.WriteBlock(header.BssAddress, zeros);
}
}
/// <summary>
/// Read a little-endian 32-bit unsigned integer from byte array.
/// </summary>
private static uint ReadUInt32LE(byte[] data, int offset)
{
return (uint)(data[offset] |
(data[offset + 1] << 8) |
(data[offset + 2] << 16) |
(data[offset + 3] << 24));
}
/// <summary>
/// Get a human-readable summary of the PS-EXE header.
/// </summary>
public static string GetHeaderSummary(ExeHeader header)
{
return $@"PS-EXE Header Information:
Entry Point (PC): 0x{header.InitialPC:X8}
Global Pointer: 0x{header.InitialGP:X8}
Load Address: 0x{header.LoadAddress:X8}
File Size: {header.FileSize} bytes (0x{header.FileSize:X} bytes)
Stack Base: 0x{header.StackBase:X8}
Stack Offset: 0x{header.StackOffset:X8}
Stack Pointer: 0x{header.StackBase + header.StackOffset:X8}
BSS Address: 0x{header.BssAddress:X8}
BSS Size: {header.BssSize} bytes
Marker: {header.MarkerText.Trim()}";
}
}
+90
View File
@@ -0,0 +1,90 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.Memory;
/// <summary>
/// PlayStation 1 BIOS ROM.
/// Physical address: 0x1FC00000 - 0x1FC7FFFF (512 KB)
/// Read-only memory containing system firmware.
/// </summary>
public class Bios : IBusDevice
{
private readonly byte[]? _data;
private const uint BaseAddress = 0x1FC00000;
private const uint Size = 0x00080000; // 512 KB
/// <summary>
/// Create an empty BIOS (returns 0 for all reads).
/// Used when no BIOS image is provided.
/// </summary>
public Bios()
{
_data = null;
}
/// <summary>
/// Create a BIOS from a byte array (user-provided BIOS image).
/// </summary>
public Bios(byte[] biosData)
{
if (biosData.Length != Size)
throw new ArgumentException($"BIOS must be exactly {Size} bytes (512 KB)");
_data = new byte[Size];
Array.Copy(biosData, _data, Size);
}
public bool Contains(uint address)
{
return address >= BaseAddress && address < BaseAddress + Size;
}
public uint Read32(uint address)
{
if (_data == null)
return 0x00000000; // Return 0 if no BIOS loaded
uint offset = address - BaseAddress;
if (offset + 3 >= Size)
return 0xFFFFFFFF;
return (uint)(_data[offset] |
(_data[offset + 1] << 8) |
(_data[offset + 2] << 16) |
(_data[offset + 3] << 24));
}
public ushort Read16(uint address)
{
if (_data == null)
return 0x0000;
uint offset = address - BaseAddress;
if (offset + 1 >= Size)
return 0xFFFF;
return (ushort)(_data[offset] | (_data[offset + 1] << 8));
}
public byte Read8(uint address)
{
if (_data == null)
return 0x00;
uint offset = address - BaseAddress;
if (offset >= Size)
return 0xFF;
return _data[offset];
}
// ROM is read-only, writes are ignored
public void Write32(uint address, uint value) { }
public void Write16(uint address, ushort value) { }
public void Write8(uint address, byte value) { }
/// <summary>
/// Check if a BIOS image is loaded.
/// </summary>
public bool IsLoaded => _data != null;
}
+165
View File
@@ -0,0 +1,165 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.Memory;
/// <summary>
/// Memory bus that coordinates access to all memory-mapped devices.
/// Handles address translation from virtual (MIPS segments) to physical addresses.
/// </summary>
public class Bus : IBus
{
private readonly List<IBusDevice> _devices;
private readonly Ram _ram;
private readonly Scratchpad _scratchpad;
private readonly Bios _bios;
private ITraceSink? _traceSink;
public Bus()
{
_ram = new Ram();
_scratchpad = new Scratchpad();
_bios = new Bios();
_devices = new List<IBusDevice>
{
_bios, // Check BIOS first (higher address)
_scratchpad, // Then scratchpad
_ram // Then main RAM (lowest address)
};
}
/// <summary>
/// Get direct access to RAM (for EXE loading, etc.).
/// </summary>
public Ram Ram => _ram;
/// <summary>
/// Get direct access to BIOS.
/// </summary>
public Bios Bios => _bios;
/// <summary>
/// Set a trace sink for memory access logging.
/// </summary>
public void SetTraceSink(ITraceSink? traceSink)
{
_traceSink = traceSink;
}
/// <summary>
/// Translate virtual address to physical address.
/// PS1 uses simple address translation:
/// - KUSEG (0x00000000-0x7FFFFFFF): user segment, cached
/// - KSEG0 (0x80000000-0x9FFFFFFF): kernel cached
/// - KSEG1 (0xA0000000-0xBFFFFFFF): kernel uncached
/// All map to physical address = virtual & 0x1FFFFFFF
/// </summary>
private static uint TranslateAddress(uint virtualAddress)
{
return virtualAddress & 0x1FFFFFFF;
}
/// <summary>
/// Find the device that contains the specified physical address.
/// </summary>
private IBusDevice? FindDevice(uint physicalAddress)
{
foreach (var device in _devices)
{
if (device.Contains(physicalAddress))
return device;
}
return null;
}
/// <summary>
/// Read a 32-bit word from the specified virtual address.
/// </summary>
public uint Read32(uint virtualAddress)
{
uint physicalAddress = TranslateAddress(virtualAddress);
var device = FindDevice(physicalAddress);
uint value = device?.Read32(physicalAddress) ?? 0xFFFFFFFF;
_traceSink?.TraceMemoryRead(physicalAddress, value, 4);
return value;
}
/// <summary>
/// Read a 16-bit halfword from the specified virtual address.
/// </summary>
public ushort Read16(uint virtualAddress)
{
uint physicalAddress = TranslateAddress(virtualAddress);
var device = FindDevice(physicalAddress);
ushort value = device?.Read16(physicalAddress) ?? 0xFFFF;
_traceSink?.TraceMemoryRead(physicalAddress, value, 2);
return value;
}
/// <summary>
/// Read an 8-bit byte from the specified virtual address.
/// </summary>
public byte Read8(uint virtualAddress)
{
uint physicalAddress = TranslateAddress(virtualAddress);
var device = FindDevice(physicalAddress);
byte value = device?.Read8(physicalAddress) ?? 0xFF;
_traceSink?.TraceMemoryRead(physicalAddress, value, 1);
return value;
}
/// <summary>
/// Write a 32-bit word to the specified virtual address.
/// </summary>
public void Write32(uint virtualAddress, uint value)
{
uint physicalAddress = TranslateAddress(virtualAddress);
var device = FindDevice(physicalAddress);
device?.Write32(physicalAddress, value);
_traceSink?.TraceMemoryWrite(physicalAddress, value, 4);
}
/// <summary>
/// Write a 16-bit halfword to the specified virtual address.
/// </summary>
public void Write16(uint virtualAddress, ushort value)
{
uint physicalAddress = TranslateAddress(virtualAddress);
var device = FindDevice(physicalAddress);
device?.Write16(physicalAddress, value);
_traceSink?.TraceMemoryWrite(physicalAddress, value, 2);
}
/// <summary>
/// Write an 8-bit byte to the specified virtual address.
/// </summary>
public void Write8(uint virtualAddress, byte value)
{
uint physicalAddress = TranslateAddress(virtualAddress);
var device = FindDevice(physicalAddress);
device?.Write8(physicalAddress, value);
_traceSink?.TraceMemoryWrite(physicalAddress, value, 1);
}
/// <summary>
/// Check if an address is aligned for the specified access size.
/// </summary>
public static bool IsAligned(uint address, int size)
{
return (address & (uint)(size - 1)) == 0;
}
/// <summary>
/// Add a custom device to the bus (for I/O registers, GPU, etc.).
/// </summary>
public void AddDevice(IBusDevice device)
{
_devices.Insert(0, device); // Add at front for priority
}
}
+115
View File
@@ -0,0 +1,115 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.Memory;
/// <summary>
/// PlayStation 1 main RAM (2 MB).
/// Physical address: 0x00000000 - 0x001FFFFF
/// </summary>
public class Ram : IBusDevice
{
private readonly byte[] _data;
private const uint BaseAddress = 0x00000000;
private const uint Size = 0x00200000; // 2 MB
public Ram()
{
_data = new byte[Size];
}
public bool Contains(uint address)
{
return address >= BaseAddress && address < BaseAddress + Size;
}
public uint Read32(uint address)
{
uint offset = address - BaseAddress;
if (offset + 3 >= Size)
return 0xFFFFFFFF;
// Little-endian read
return (uint)(_data[offset] |
(_data[offset + 1] << 8) |
(_data[offset + 2] << 16) |
(_data[offset + 3] << 24));
}
public ushort Read16(uint address)
{
uint offset = address - BaseAddress;
if (offset + 1 >= Size)
return 0xFFFF;
return (ushort)(_data[offset] | (_data[offset + 1] << 8));
}
public byte Read8(uint address)
{
uint offset = address - BaseAddress;
if (offset >= Size)
return 0xFF;
return _data[offset];
}
public void Write32(uint address, uint value)
{
uint offset = address - BaseAddress;
if (offset + 3 >= Size)
return;
// Little-endian write
_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);
}
public void Write16(uint address, ushort value)
{
uint offset = address - BaseAddress;
if (offset + 1 >= Size)
return;
_data[offset] = (byte)(value & 0xFF);
_data[offset + 1] = (byte)((value >> 8) & 0xFF);
}
public void Write8(uint address, byte value)
{
uint offset = address - BaseAddress;
if (offset >= Size)
return;
_data[offset] = value;
}
/// <summary>
/// Write a block of data to RAM (used by EXE loader).
/// </summary>
public void WriteBlock(uint address, byte[] data)
{
uint offset = address - BaseAddress;
if (offset >= Size)
return;
uint length = Math.Min((uint)data.Length, Size - offset);
Array.Copy(data, 0, _data, offset, length);
}
/// <summary>
/// Read a block of data from RAM.
/// </summary>
public byte[] ReadBlock(uint address, uint length)
{
uint offset = address - BaseAddress;
if (offset >= Size)
return Array.Empty<byte>();
uint actualLength = Math.Min(length, Size - offset);
byte[] result = new byte[actualLength];
Array.Copy(_data, offset, result, 0, actualLength);
return result;
}
}
+86
View File
@@ -0,0 +1,86 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.Memory;
/// <summary>
/// PlayStation 1 scratchpad (data cache mapped as fast RAM).
/// Physical address: 0x1F800000 - 0x1F8003FF (1 KB)
/// Note: NOT executable - attempts to execute from here cause bus errors.
/// </summary>
public class Scratchpad : IBusDevice
{
private readonly byte[] _data;
private const uint BaseAddress = 0x1F800000;
private const uint Size = 0x400; // 1 KB
public Scratchpad()
{
_data = new byte[Size];
}
public bool Contains(uint address)
{
return address >= BaseAddress && address < BaseAddress + Size;
}
public uint Read32(uint address)
{
uint offset = address - BaseAddress;
if (offset + 3 >= Size)
return 0xFFFFFFFF;
return (uint)(_data[offset] |
(_data[offset + 1] << 8) |
(_data[offset + 2] << 16) |
(_data[offset + 3] << 24));
}
public ushort Read16(uint address)
{
uint offset = address - BaseAddress;
if (offset + 1 >= Size)
return 0xFFFF;
return (ushort)(_data[offset] | (_data[offset + 1] << 8));
}
public byte Read8(uint address)
{
uint offset = address - BaseAddress;
if (offset >= Size)
return 0xFF;
return _data[offset];
}
public void Write32(uint address, uint value)
{
uint offset = address - BaseAddress;
if (offset + 3 >= Size)
return;
_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);
}
public void Write16(uint address, ushort value)
{
uint offset = address - BaseAddress;
if (offset + 1 >= Size)
return;
_data[offset] = (byte)(value & 0xFF);
_data[offset + 1] = (byte)((value >> 8) & 0xFF);
}
public void Write8(uint address, byte value)
{
uint offset = address - BaseAddress;
if (offset >= Size)
return;
_data[offset] = value;
}
}
+269
View File
@@ -0,0 +1,269 @@
using Yaroze.Core.Interfaces;
namespace Yaroze.Core.Timers;
/// <summary>
/// PlayStation 1 Timer (Root Counter).
/// Each timer can count system clocks or special sources (dotclock, hblank, etc.).
/// </summary>
public class Timer : IBusDevice
{
private readonly int _timerNumber;
private ushort _counter;
private ushort _target;
private uint _mode;
// Interrupt callback
private Action? _onInterrupt;
public Timer(int timerNumber)
{
_timerNumber = timerNumber;
Reset();
}
public void Reset()
{
_counter = 0;
_target = 0;
_mode = 0;
}
/// <summary>
/// Set interrupt callback.
/// </summary>
public void SetInterruptCallback(Action callback)
{
_onInterrupt = callback;
}
#region IBusDevice Implementation
public bool Contains(uint address)
{
uint baseAddr = (uint)(0x1F801100 + _timerNumber * 0x10);
return address >= baseAddr && address < baseAddr + 0x10;
}
public uint Read32(uint address)
{
uint baseAddr = (uint)(0x1F801100 + _timerNumber * 0x10);
uint offset = address - baseAddr;
return offset switch
{
0x00 => _counter, // Counter value
0x04 => _mode, // Counter mode
0x08 => _target, // Counter target
_ => 0
};
}
public void Write32(uint address, uint value)
{
uint baseAddr = (uint)(0x1F801100 + _timerNumber * 0x10);
uint offset = address - baseAddr;
switch (offset)
{
case 0x00: // Counter value
_counter = (ushort)(value & 0xFFFF);
break;
case 0x04: // Counter mode
_mode = value;
// Writing to mode resets counter to 0
_counter = 0;
// Clear IRQ flags (bits 10-12) if written as 1
if ((value & (1u << 10)) != 0) _mode &= ~(1u << 10);
if ((value & (1u << 11)) != 0) _mode &= ~(1u << 11);
if ((value & (1u << 12)) != 0) _mode &= ~(1u << 12);
break;
case 0x08: // Counter target
_target = (ushort)(value & 0xFFFF);
break;
}
}
public ushort Read16(uint address) => (ushort)Read32(address);
public byte Read8(uint address) => (byte)Read32(address);
public void Write16(uint address, ushort value) => Write32(address, value);
public void Write8(uint address, byte value) => Write32(address, value);
#endregion
#region Timer Logic
/// <summary>
/// Tick the timer by a number of cycles.
/// </summary>
public void Tick(int cycles)
{
// Check if timer is paused or in sync mode
bool syncEnable = (_mode & 0x01) != 0;
if (syncEnable)
{
// Sync mode - more complex behavior depending on timer and mode
// For now, treat as paused (proper sync modes require GPU signals)
return;
}
// Get clock source
int clockSource = GetClockSource();
int divisor = GetClockDivisor(clockSource);
// Increment counter
int increment = cycles / divisor;
if (increment == 0 && cycles > 0)
increment = 1; // Always increment by at least 1 if cycles > 0
for (int i = 0; i < increment; i++)
{
_counter++;
// Check for target match
if (_counter == _target)
{
HandleTargetReached();
}
// Check for overflow (0xFFFF → 0x0000)
if (_counter == 0)
{
HandleOverflow();
}
}
}
private int GetClockSource()
{
return (int)((_mode >> 8) & 0x3);
}
private int GetClockDivisor(int clockSource)
{
// Clock divisors depend on timer number and source
return _timerNumber switch
{
0 => clockSource switch
{
// Dotclock: Real hardware uses ~53.222 MHz / sysclock ratio
// Using 1:1 divisor as accurate dotclock requires GPU synchronization
1 => 1,
_ => 1 // System clock (33.8688 MHz)
},
1 => clockSource switch
{
// H-blank: Real hardware ticks at horizontal scanline rate (~15.7 kHz)
// Using 1:1 divisor as accurate hblank requires GPU synchronization
1 => 1,
_ => 1 // System clock
},
2 => clockSource switch
{
2 or 3 => 8, // System clock ÷ 8 (accurate)
_ => 1 // System clock
},
_ => 1
};
}
private void HandleTargetReached()
{
// Set reached target flag (bit 11)
_mode |= (1u << 11);
// Reset counter if reset-on-target is enabled (bit 3)
bool resetOnTarget = (_mode & (1u << 3)) != 0;
if (resetOnTarget)
{
_counter = 0;
}
// Trigger IRQ if enabled (bit 4)
bool irqOnTarget = (_mode & (1u << 4)) != 0;
if (irqOnTarget)
{
TriggerInterrupt();
}
}
private void HandleOverflow()
{
// Set reached 0xFFFF flag (bit 12)
_mode |= (1u << 12);
// Trigger IRQ if enabled (bit 5)
bool irqOnOverflow = (_mode & (1u << 5)) != 0;
if (irqOnOverflow)
{
TriggerInterrupt();
}
}
private void TriggerInterrupt()
{
// Check IRQ repeat mode (bit 6)
bool repeatMode = (_mode & (1u << 6)) != 0;
bool irqToggleMode = (_mode & (1u << 7)) != 0;
if (repeatMode)
{
// Repeat mode - IRQ triggers every time
if (irqToggleMode)
{
// Toggle IRQ bit (bit 10)
_mode ^= (1u << 10);
}
else
{
// Pulse mode - set IRQ bit
_mode |= (1u << 10);
}
}
else
{
// One-shot mode - only trigger once
if ((_mode & (1u << 10)) == 0)
{
_mode |= (1u << 10);
}
}
// Call interrupt callback
_onInterrupt?.Invoke();
}
#endregion
/// <summary>
/// Get current counter value.
/// </summary>
public ushort Counter => _counter;
/// <summary>
/// Get counter target value.
/// </summary>
public ushort Target => _target;
/// <summary>
/// Get counter mode register.
/// </summary>
public uint Mode => _mode;
/// <summary>
/// Check if IRQ is pending (bit 10).
/// </summary>
public bool IrqPending => (_mode & (1u << 10)) != 0;
/// <summary>
/// Check if reached target (bit 11).
/// </summary>
public bool ReachedTarget => (_mode & (1u << 11)) != 0;
/// <summary>
/// Check if reached overflow (bit 12).
/// </summary>
public bool ReachedOverflow => (_mode & (1u << 12)) != 0;
}
+76
View File
@@ -0,0 +1,76 @@
namespace Yaroze.Core.Utilities;
/// <summary>
/// Utility methods for bit manipulation and extraction.
/// </summary>
public static class BitUtils
{
/// <summary>
/// Extract a bit field from a value.
/// </summary>
/// <param name="value">Source value</param>
/// <param name="start">Start bit position (0-indexed, LSB = 0)</param>
/// <param name="length">Number of bits to extract</param>
/// <returns>Extracted bits shifted to LSB position</returns>
public static uint ExtractBits(uint value, int start, int length)
{
uint mask = (1u << length) - 1;
return (value >> start) & mask;
}
/// <summary>
/// Check if a specific bit is set.
/// </summary>
/// <param name="value">Value to test</param>
/// <param name="bit">Bit position (0-indexed)</param>
/// <returns>True if the bit is set</returns>
public static bool IsBitSet(uint value, int bit)
{
return (value & (1u << bit)) != 0;
}
/// <summary>
/// Set a specific bit to 1.
/// </summary>
/// <param name="value">Value to modify</param>
/// <param name="bit">Bit position (0-indexed)</param>
/// <returns>Value with bit set</returns>
public static uint SetBit(uint value, int bit)
{
return value | (1u << bit);
}
/// <summary>
/// Clear a specific bit to 0.
/// </summary>
/// <param name="value">Value to modify</param>
/// <param name="bit">Bit position (0-indexed)</param>
/// <returns>Value with bit cleared</returns>
public static uint ClearBit(uint value, int bit)
{
return value & ~(1u << bit);
}
/// <summary>
/// Toggle a specific bit.
/// </summary>
/// <param name="value">Value to modify</param>
/// <param name="bit">Bit position (0-indexed)</param>
/// <returns>Value with bit toggled</returns>
public static uint ToggleBit(uint value, int bit)
{
return value ^ (1u << bit);
}
/// <summary>
/// Create a bit mask with specified bits set.
/// </summary>
/// <param name="start">Start bit position</param>
/// <param name="length">Number of bits</param>
/// <returns>Bit mask</returns>
public static uint CreateMask(int start, int length)
{
uint mask = (1u << length) - 1;
return mask << start;
}
}
@@ -0,0 +1,62 @@
namespace Yaroze.Core.Utilities;
/// <summary>
/// Utility methods for sign extension operations.
/// Critical for MIPS load instructions and immediate value handling.
/// </summary>
public static class SignExtension
{
/// <summary>
/// Sign-extend an 8-bit value to 32 bits.
/// </summary>
/// <param name="value">8-bit value</param>
/// <returns>32-bit sign-extended value</returns>
public static uint SignExtend8(byte value)
{
// If bit 7 is set, fill upper bits with 1s
if ((value & 0x80) != 0)
return 0xFFFFFF00u | value;
return value;
}
/// <summary>
/// Sign-extend a 16-bit value to 32 bits.
/// </summary>
/// <param name="value">16-bit value</param>
/// <returns>32-bit sign-extended value</returns>
public static uint SignExtend16(ushort value)
{
// If bit 15 is set, fill upper bits with 1s
if ((value & 0x8000) != 0)
return 0xFFFF0000u | value;
return value;
}
/// <summary>
/// Sign-extend an arbitrary bit field to 32 bits.
/// </summary>
/// <param name="value">Value to extend</param>
/// <param name="bits">Number of bits in the value (e.g., 16 for 16-bit)</param>
/// <returns>32-bit sign-extended value</returns>
public static uint SignExtend(uint value, int bits)
{
int signBit = bits - 1;
if ((value & (1u << signBit)) != 0)
{
// Create mask of 1s for upper bits
uint mask = ~((1u << bits) - 1);
return value | mask;
}
return value;
}
/// <summary>
/// Zero-extend an 8-bit value to 32 bits (for completeness).
/// </summary>
public static uint ZeroExtend8(byte value) => value;
/// <summary>
/// Zero-extend a 16-bit value to 32 bits (for completeness).
/// </summary>
public static uint ZeroExtend16(ushort value) => value;
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,363 @@
using Xunit;
using Yaroze.Core.Analysis;
namespace Yaroze.Tests.Analysis;
public class CrossReferenceTrackerTests
{
[Fact]
public void AnalyzeRange_FindsJALReferences()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// main:
// jal sub_func (0x80000000 -> 0x80000010)
// nop
// sub_func:
// jr $ra
// nop
WriteInstruction(memory, 0, 0x0C000004); // JAL 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 16, 0x03E00008); // JR $ra
WriteInstruction(memory, 20, 0x00000000); // NOP
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeRange(baseAddress, baseAddress + 24);
// Assert
var xrefsTo = tracker.GetXRefsTo(0x80000010);
Assert.Single(xrefsTo);
Assert.Equal(baseAddress, xrefsTo[0].From);
Assert.Equal(0x80000010u, xrefsTo[0].To);
Assert.Equal(XRefType.Call, xrefsTo[0].Type);
}
[Fact]
public void AnalyzeRange_FindsBranchReferences()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// beq $t0, $zero, target (0x80000000 -> 0x8000000C)
// nop
// addiu $v0, $zero, 1
// target:
// jr $ra
WriteInstruction(memory, 0, 0x11000002); // BEQ $t0, $zero, +2
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeRange(baseAddress, baseAddress + 16);
// Assert
var xrefsTo = tracker.GetXRefsTo(0x8000000C);
Assert.Single(xrefsTo);
Assert.Equal(baseAddress, xrefsTo[0].From);
Assert.Equal(XRefType.Branch, xrefsTo[0].Type);
}
[Fact]
public void AnalyzeRange_FindsJumpReferences()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// j target (0x80000000 -> 0x80000010)
// nop
WriteInstruction(memory, 0, 0x08000004); // J 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeRange(baseAddress, baseAddress + 8);
// Assert
var xrefsTo = tracker.GetXRefsTo(0x80000010);
Assert.Single(xrefsTo);
Assert.Equal(XRefType.Jump, xrefsTo[0].Type);
}
[Fact]
public void AnalyzeRange_FindsMultipleReferences()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func1:
// jal common_func
// nop
// func2:
// jal common_func
// nop
WriteInstruction(memory, 0, 0x0C000008); // JAL 0x80000020
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 16, 0x0C000008); // JAL 0x80000020
WriteInstruction(memory, 20, 0x00000000); // NOP
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeRange(baseAddress, baseAddress + 24);
// Assert
var xrefsTo = tracker.GetXRefsTo(0x80000020);
Assert.Equal(2, xrefsTo.Count);
Assert.Contains(xrefsTo, x => x.From == baseAddress);
Assert.Contains(xrefsTo, x => x.From == baseAddress + 16);
}
[Fact]
public void GetXRefsFrom_ReturnsOutgoingReferences()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// main:
// jal func1
// nop
// jal func2
// nop
WriteInstruction(memory, 0, 0x0C000004); // JAL 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x0C000008); // JAL 0x80000020
WriteInstruction(memory, 12, 0x00000000); // NOP
var tracker = new CrossReferenceTracker(memory, baseAddress);
tracker.AnalyzeRange(baseAddress, baseAddress + 16);
// Act
var xrefsFrom = tracker.GetXRefsFrom(baseAddress);
// Assert
Assert.Single(xrefsFrom);
Assert.Equal(0x80000010u, xrefsFrom[0].To);
}
[Fact]
public void GetXRefsTo_ReturnsEmptyForUnreferencedAddress()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
var tracker = new CrossReferenceTracker(memory, baseAddress);
tracker.AnalyzeRange(baseAddress, baseAddress + 16);
// Act
var xrefsTo = tracker.GetXRefsTo(0x80000100);
// Assert
Assert.Empty(xrefsTo);
}
[Fact]
public void AnalyzeFromFunctionAnalyzer_FindsAllReferences()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// main:
// jal sub
// nop
// jr $ra
// nop
// sub:
// jr $ra
// nop
WriteInstruction(memory, 0, 0x0C000004); // JAL 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
WriteInstruction(memory, 16, 0x03E00008); // JR $ra
WriteInstruction(memory, 20, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
analyzer.AnalyzeFromEntryPoint(baseAddress);
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeFromFunctionAnalyzer(analyzer);
// Assert
var xrefsTo = tracker.GetXRefsTo(0x80000010);
Assert.Single(xrefsTo);
Assert.Equal(XRefType.Call, xrefsTo[0].Type);
}
[Fact]
public void GenerateXRefReport_ProducesReadableOutput()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
WriteInstruction(memory, 0, 0x0C000004); // JAL 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
var tracker = new CrossReferenceTracker(memory, baseAddress);
tracker.AnalyzeRange(baseAddress, baseAddress + 8);
var symbolManager = new SymbolManager();
symbolManager.AddSymbol(baseAddress, "main", SymbolType.Function);
symbolManager.AddSymbol(0x80000010, "sub_func", SymbolType.Function);
// Act
string report = tracker.GenerateXRefReport(0x80000010, symbolManager);
// Assert
Assert.Contains("Cross-references TO", report);
Assert.Contains("main", report);
Assert.Contains("call", report);
}
[Fact]
public void GetStats_ReturnsCorrectCounts()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// jal, j, beq
WriteInstruction(memory, 0, 0x0C000004); // JAL (call)
WriteInstruction(memory, 4, 0x08000004); // J (jump)
WriteInstruction(memory, 8, 0x11000002); // BEQ (branch)
var tracker = new CrossReferenceTracker(memory, baseAddress);
tracker.AnalyzeRange(baseAddress, baseAddress + 12);
// Act
var stats = tracker.GetStats();
// Assert
Assert.Equal(3, stats.TotalXRefs);
Assert.Equal(1, stats.CallCount);
Assert.Equal(1, stats.JumpCount);
Assert.Equal(1, stats.BranchCount);
}
[Fact]
public void AnalyzeRange_HandlesIndirectCalls()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// jalr $t0
WriteInstruction(memory, 0, 0x0100F809); // JALR $t0
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeRange(baseAddress, baseAddress + 4);
// Assert
var xrefsFrom = tracker.GetXRefsFrom(baseAddress);
Assert.Single(xrefsFrom);
Assert.Equal(XRefType.IndirectCall, xrefsFrom[0].Type);
Assert.Equal(0u, xrefsFrom[0].To); // Target unknown
}
[Fact]
public void AnalyzeRange_HandlesIndirectJumps()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// jr $t0 (not $ra)
WriteInstruction(memory, 0, 0x01000008); // JR $t0
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeRange(baseAddress, baseAddress + 4);
// Assert
var xrefsFrom = tracker.GetXRefsFrom(baseAddress);
Assert.Single(xrefsFrom);
Assert.Equal(XRefType.IndirectJump, xrefsFrom[0].Type);
}
[Fact]
public void AnalyzeRange_IgnoresReturnInstructions()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// jr $ra (return)
WriteInstruction(memory, 0, 0x03E00008); // JR $ra
var tracker = new CrossReferenceTracker(memory, baseAddress);
// Act
tracker.AnalyzeRange(baseAddress, baseAddress + 4);
// Assert
var xrefsFrom = tracker.GetXRefsFrom(baseAddress);
Assert.Empty(xrefsFrom); // Returns don't create xrefs
}
[Fact]
public void XRef_ToString_FormatsCorrectly()
{
// Arrange
var xref = new XRef
{
From = 0x80000000,
To = 0x80000010,
Type = XRefType.Call
};
// Act
string result = xref.ToString();
// Assert
Assert.Contains("0x80000000", result);
Assert.Contains("0x80000010", result);
Assert.Contains("Call", result);
}
[Fact]
public void XRefStats_ToString_FormatsCorrectly()
{
// Arrange
var stats = new XRefStats
{
TotalXRefs = 10,
CallCount = 5,
JumpCount = 3,
BranchCount = 2
};
// Act
string result = stats.ToString();
// Assert
Assert.Contains("10 total", result);
Assert.Contains("5 calls", result);
Assert.Contains("3 jumps", result);
Assert.Contains("2 branches", result);
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
@@ -0,0 +1,366 @@
using Xunit;
using Yaroze.Core.Analysis;
using Yaroze.Core.CPU;
namespace Yaroze.Tests.Analysis;
public class FunctionAnalyzerTests
{
[Fact]
public void FunctionAnalyzer_DiscoversSingleFunction()
{
// Arrange - Simple function with just a return
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// main:
// jr $ra
// nop
WriteInstruction(memory, 0, 0x03E00008); // JR $ra
WriteInstruction(memory, 4, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert
Assert.Single(analyzer.Functions);
Assert.True(analyzer.Functions.ContainsKey(baseAddress));
}
[Fact]
public void FunctionAnalyzer_DiscoversCalledFunction()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
uint subAddress = baseAddress + 0x10;
// main:
// jal sub
// nop
// jr $ra
// nop
// sub:
// jr $ra
// nop
WriteInstruction(memory, 0, 0x0C000004); // JAL 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
WriteInstruction(memory, 16, 0x03E00008); // JR $ra (sub)
WriteInstruction(memory, 20, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert
Assert.Equal(2, analyzer.Functions.Count);
Assert.True(analyzer.Functions.ContainsKey(baseAddress));
Assert.True(analyzer.Functions.ContainsKey(subAddress));
}
[Fact]
public void FunctionAnalyzer_BuildsCallGraph()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
uint subAddress = baseAddress + 0x10;
// main calls sub
WriteInstruction(memory, 0, 0x0C000004); // JAL 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
WriteInstruction(memory, 16, 0x03E00008); // JR $ra (sub)
WriteInstruction(memory, 20, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert
var mainFunc = analyzer.Functions[baseAddress];
Assert.Contains(subAddress, mainFunc.CallsTo);
var subFunc = analyzer.Functions[subAddress];
Assert.Contains(baseAddress, subFunc.CalledFrom);
}
[Fact]
public void FunctionAnalyzer_HandlesConditionalBranches()
{
// Arrange - Function with branch
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// main:
// beq $t0, $zero, +2
// nop
// addiu $v0, $zero, 1
// jr $ra
// nop
WriteInstruction(memory, 0, 0x11000002); // BEQ $8, $0, +2
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert - Should have one function with all instructions
Assert.Single(analyzer.Functions);
var func = analyzer.Functions[baseAddress];
Assert.Contains(baseAddress + 0, func.Instructions); // BEQ
Assert.Contains(baseAddress + 4, func.Instructions); // NOP
Assert.Contains(baseAddress + 8, func.Instructions); // ADDIU
Assert.Contains(baseAddress + 12, func.Instructions); // JR
}
[Fact]
public void FunctionAnalyzer_AssignsDefaultNames()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
WriteInstruction(memory, 0, 0x03E00008); // JR $ra
WriteInstruction(memory, 4, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert
var func = analyzer.Functions[baseAddress];
Assert.Equal("func_80000000", func.Name);
}
[Fact(Skip = "ExportCallGraphToDot method removed")]
public void FunctionAnalyzer_ExportsToDotFormat()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
uint subAddress = baseAddress + 0x10;
// main calls sub
WriteInstruction(memory, 0, 0x0C000004); // JAL 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
WriteInstruction(memory, 16, 0x03E00008); // JR $ra (sub)
WriteInstruction(memory, 20, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Act
// string dot = analyzer.ExportCallGraphToDot();
// Assert
// Assert.Contains("digraph", dot);
// Assert.Contains("func_80000000", dot);
// Assert.Contains("func_80000010", dot);
// Assert.Contains("->", dot);
}
[Fact]
public void FunctionAnalyzer_HandlesUnconditionalJump()
{
// Arrange - Function with unconditional jump
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// main:
// j 0x80000010
// nop
// ...
// addiu $v0, $zero, 1
// jr $ra
// nop
WriteInstruction(memory, 0, 0x08000004); // J 0x80000010
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x00000000); // NOP (unreachable)
WriteInstruction(memory, 12, 0x00000000); // NOP (unreachable)
WriteInstruction(memory, 16, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 20, 0x03E00008); // JR $ra
WriteInstruction(memory, 24, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert
var func = analyzer.Functions[baseAddress];
Assert.Contains(baseAddress + 0, func.Instructions); // J
Assert.Contains(baseAddress + 16, func.Instructions); // ADDIU
Assert.Contains(baseAddress + 20, func.Instructions); // JR
Assert.DoesNotContain(baseAddress + 8, func.Instructions); // Unreachable
}
[Fact]
public void FunctionAnalyzer_HandlesRecursiveCalls()
{
// Arrange - Recursive factorial function
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// factorial:
// beq $a0, $zero, done
// nop
// addiu $a0, $a0, -1
// jal factorial (recursive)
// nop
// done:
// jr $ra
// nop
WriteInstruction(memory, 0, 0x10800003); // BEQ $a0, $zero, +3
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x2484FFFF); // ADDIU $a0, $a0, -1
WriteInstruction(memory, 12, 0x0C000000); // JAL 0x80000000 (self)
WriteInstruction(memory, 16, 0x00000000); // NOP
WriteInstruction(memory, 20, 0x03E00008); // JR $ra
WriteInstruction(memory, 24, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert - Should have one function that calls itself
Assert.Single(analyzer.Functions);
var func = analyzer.Functions[baseAddress];
Assert.Contains(baseAddress, func.CallsTo); // Calls itself
Assert.Contains(baseAddress, func.CalledFrom); // Called by itself
}
[Fact(Skip = "GetOrCreateFunction method removed")]
public void FunctionAnalyzer_GetOrCreateFunction_CreatesNewFunction()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
var analyzer = new FunctionAnalyzer(memory);
// Act
// var func1 = analyzer.GetOrCreateFunction(baseAddress);
// var func2 = analyzer.GetOrCreateFunction(baseAddress);
// Assert - Should return same instance
// Assert.Same(func1, func2);
// Assert.Single(analyzer.Functions);
}
[Fact]
public void FunctionAnalyzer_HandlesMultipleFunctions()
{
// Arrange - Program with 3 functions
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
uint func1 = baseAddress + 0x20;
uint func2 = baseAddress + 0x40;
// main:
// jal func1
// nop
// jal func2
// nop
// jr $ra
// nop
WriteInstruction(memory, 0, 0x0C000008); // JAL func1
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x0C000010); // JAL func2
WriteInstruction(memory, 12, 0x00000000); // NOP
WriteInstruction(memory, 16, 0x03E00008); // JR $ra
WriteInstruction(memory, 20, 0x00000000); // NOP
// func1:
// jr $ra
// nop
WriteInstruction(memory, 32, 0x03E00008); // JR $ra
WriteInstruction(memory, 36, 0x00000000); // NOP
// func2:
// jr $ra
// nop
WriteInstruction(memory, 64, 0x03E00008); // JR $ra
WriteInstruction(memory, 68, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
// Act
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert
Assert.Equal(3, analyzer.Functions.Count);
Assert.True(analyzer.Functions.ContainsKey(baseAddress));
Assert.True(analyzer.Functions.ContainsKey(func1));
Assert.True(analyzer.Functions.ContainsKey(func2));
var mainFunc = analyzer.Functions[baseAddress];
Assert.Contains(func1, mainFunc.CallsTo);
Assert.Contains(func2, mainFunc.CallsTo);
}
[Fact]
public void Function_ToString_FormatsCorrectly()
{
// Arrange
var func = new Function
{
Address = 0x80000000,
Name = "main"
};
func.Instructions.Add(0x80000000);
func.Instructions.Add(0x80000004);
func.CallsTo.Add(0x80000100);
// Act
string result = func.ToString();
// Assert
Assert.Contains("main", result);
Assert.Contains("0x80000000", result);
Assert.Contains("2", result); // instruction count
Assert.Contains("1", result); // calls count
}
[Fact]
public void FunctionAnalyzer_StopsAtSafetyLimit()
{
// Arrange - Create a scenario that could loop infinitely
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// Infinite loop: j 0x80000000
for (int i = 0; i < 1000; i += 4)
{
WriteInstruction(memory, i, 0x08000000); // J 0x80000000
}
var analyzer = new FunctionAnalyzer(memory);
// Act - Should not hang or throw
analyzer.AnalyzeFromEntryPoint(baseAddress);
// Assert - Should have analyzed something without hanging
Assert.NotEmpty(analyzer.Functions);
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
@@ -0,0 +1,513 @@
using Xunit;
using Yaroze.Core.Analysis;
namespace Yaroze.Tests.Analysis;
public class SymbolManagerTests
{
[Fact]
public void AddSymbol_CreatesNewSymbol()
{
// Arrange
var manager = new SymbolManager();
// Act
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
// Assert
var symbol = manager.GetSymbol(0x80000000);
Assert.NotNull(symbol);
Assert.Equal("main", symbol.Name);
Assert.Equal(SymbolType.Function, symbol.Type);
Assert.Equal(0x80000000u, symbol.Address);
}
[Fact]
public void AddSymbol_UpdatesExistingSymbol()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "func_80000000", SymbolType.Function);
// Act - Update with better name
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
// Assert
var symbol = manager.GetSymbol(0x80000000);
Assert.Equal("main", symbol.Name);
Assert.Single(manager.Symbols); // Should still be one symbol
}
[Fact]
public void GetSymbol_ReturnsNullForMissingAddress()
{
// Arrange
var manager = new SymbolManager();
// Act
var symbol = manager.GetSymbol(0x80000000);
// Assert
Assert.Null(symbol);
}
[Fact]
public void RemoveSymbol_DeletesSymbol()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
// Act
bool removed = manager.RemoveSymbol(0x80000000);
// Assert
Assert.True(removed);
Assert.Null(manager.GetSymbol(0x80000000));
Assert.Empty(manager.Symbols);
}
[Fact]
public void RemoveSymbol_ReturnsFalseForMissingAddress()
{
// Arrange
var manager = new SymbolManager();
// Act
bool removed = manager.RemoveSymbol(0x80000000);
// Assert
Assert.False(removed);
}
[Fact]
public void AddComment_CreatesComment()
{
// Arrange
var manager = new SymbolManager();
// Act
manager.AddComment(0x80000000, "Entry point");
// Assert
var comment = manager.GetComment(0x80000000);
Assert.Equal("Entry point", comment);
}
[Fact]
public void AddComment_UpdatesExistingComment()
{
// Arrange
var manager = new SymbolManager();
manager.AddComment(0x80000000, "Old comment");
// Act
manager.AddComment(0x80000000, "New comment");
// Assert
Assert.Equal("New comment", manager.GetComment(0x80000000));
Assert.Single(manager.Comments);
}
[Fact]
public void GetComment_ReturnsNullForMissingAddress()
{
// Arrange
var manager = new SymbolManager();
// Act
var comment = manager.GetComment(0x80000000);
// Assert
Assert.Null(comment);
}
[Fact]
public void RemoveComment_DeletesComment()
{
// Arrange
var manager = new SymbolManager();
manager.AddComment(0x80000000, "Test comment");
// Act
bool removed = manager.RemoveComment(0x80000000);
// Assert
Assert.True(removed);
Assert.Null(manager.GetComment(0x80000000));
Assert.Empty(manager.Comments);
}
[Fact]
public void FindSymbolsByName_ReturnsCaseInsensitiveMatches()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
manager.AddSymbol(0x80000100, "MainLoop", SymbolType.Function);
manager.AddSymbol(0x80000200, "Initialize", SymbolType.Function);
// Act
var results = manager.FindSymbolsByName("main");
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(results, s => s.Name == "main");
Assert.Contains(results, s => s.Name == "MainLoop");
}
[Fact]
public void FindSymbolsByName_ReturnsEmptyForNoMatches()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
// Act
var results = manager.FindSymbolsByName("test");
// Assert
Assert.Empty(results);
}
[Fact]
public void GetSymbolsByType_ReturnsOnlyMatchingType()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
manager.AddSymbol(0x80000100, "sub", SymbolType.Function);
manager.AddSymbol(0x80001000, "loop", SymbolType.Label);
manager.AddSymbol(0x80002000, "data", SymbolType.Data);
// Act
var functions = manager.GetSymbolsByType(SymbolType.Function);
var labels = manager.GetSymbolsByType(SymbolType.Label);
var data = manager.GetSymbolsByType(SymbolType.Data);
// Assert
Assert.Equal(2, functions.Count);
Assert.Single(labels);
Assert.Single(data);
Assert.All(functions, s => Assert.Equal(SymbolType.Function, s.Type));
}
[Fact]
public void GetSymbolsByType_ReturnsSortedByAddress()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000200, "func3", SymbolType.Function);
manager.AddSymbol(0x80000000, "func1", SymbolType.Function);
manager.AddSymbol(0x80000100, "func2", SymbolType.Function);
// Act
var functions = manager.GetSymbolsByType(SymbolType.Function);
// Assert
Assert.Equal(0x80000000u, functions[0].Address);
Assert.Equal(0x80000100u, functions[1].Address);
Assert.Equal(0x80000200u, functions[2].Address);
}
[Fact]
public void ImportFromFunctionAnalyzer_AddsAllFunctions()
{
// Arrange
var manager = new SymbolManager();
byte[] memory = new byte[4096];
// Create simple function
WriteInstruction(memory, 0, 0x03E00008); // JR $ra
WriteInstruction(memory, 4, 0x00000000); // NOP
var analyzer = new FunctionAnalyzer(memory);
analyzer.AnalyzeFromEntryPoint(0x80000000);
// Act
manager.ImportFromFunctionAnalyzer(analyzer);
// Assert
Assert.Single(manager.Symbols);
var symbol = manager.GetSymbol(0x80000000);
Assert.NotNull(symbol);
Assert.Equal(SymbolType.Function, symbol.Type);
}
[Fact]
public void ImportFromFunctionAnalyzer_DoesNotOverwriteExisting()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
byte[] memory = new byte[4096];
WriteInstruction(memory, 0, 0x03E00008);
WriteInstruction(memory, 4, 0x00000000);
var analyzer = new FunctionAnalyzer(memory);
analyzer.AnalyzeFromEntryPoint(0x80000000);
// Act
manager.ImportFromFunctionAnalyzer(analyzer);
// Assert - Should keep user-provided name
var symbol = manager.GetSymbol(0x80000000);
Assert.Equal("main", symbol.Name);
}
[Fact]
public void ExportToText_FormatsCorrectly()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
manager.AddSymbol(0x80000100, "loop", SymbolType.Label);
manager.AddSymbol(0x80001000, "data_buffer", SymbolType.Data);
manager.AddComment(0x80000000, "Entry point");
// Act
string text = manager.ExportToText();
// Assert
Assert.Contains("# Yaroze Symbol File", text);
Assert.Contains("# Functions", text);
Assert.Contains("F 0x80000000 main", text);
Assert.Contains("# Labels", text);
Assert.Contains("L 0x80000100 loop", text);
Assert.Contains("# Data", text);
Assert.Contains("D 0x80001000 data_buffer", text);
Assert.Contains("# Comments", text);
Assert.Contains("C 0x80000000 Entry point", text);
}
[Fact]
public void ImportFromText_ParsesCorrectly()
{
// Arrange
var manager = new SymbolManager();
string text = @"# Yaroze Symbol File
# Functions
F 0x80000000 main
F 0x80000100 sub
# Labels
L 0x80000200 loop
# Data
D 0x80001000 data_buffer
# Comments
C 0x80000000 Entry point
C 0x80000100 Helper function
";
// Act
manager.ImportFromText(text);
// Assert
Assert.Equal(4, manager.Symbols.Count);
Assert.Equal(2, manager.Comments.Count);
var main = manager.GetSymbol(0x80000000);
Assert.NotNull(main);
Assert.Equal("main", main.Name);
Assert.Equal(SymbolType.Function, main.Type);
var loop = manager.GetSymbol(0x80000200);
Assert.NotNull(loop);
Assert.Equal("loop", loop.Name);
Assert.Equal(SymbolType.Label, loop.Type);
Assert.Equal("Entry point", manager.GetComment(0x80000000));
}
[Fact]
public void ImportFromText_IgnoresCommentLines()
{
// Arrange
var manager = new SymbolManager();
string text = @"# This is a comment
# Another comment
F 0x80000000 main
# More comments
";
// Act
manager.ImportFromText(text);
// Assert
Assert.Single(manager.Symbols);
}
[Fact]
public void ImportFromText_IgnoresMalformedLines()
{
// Arrange
var manager = new SymbolManager();
string text = @"
F 0x80000000 main
INVALID LINE
F 0xINVALID test
F 0x80000100
F 0x80000200 valid_function
";
// Act
manager.ImportFromText(text);
// Assert - Should only parse valid lines
Assert.Equal(2, manager.Symbols.Count);
Assert.NotNull(manager.GetSymbol(0x80000000));
Assert.NotNull(manager.GetSymbol(0x80000200));
}
[Fact]
public void Clear_RemovesAllSymbolsAndComments()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
manager.AddSymbol(0x80000100, "sub", SymbolType.Function);
manager.AddComment(0x80000000, "Entry point");
// Act
manager.Clear();
// Assert
Assert.Empty(manager.Symbols);
Assert.Empty(manager.Comments);
}
[Fact]
public void Symbols_ReturnsReadOnlyDictionary()
{
// Arrange
var manager = new SymbolManager();
manager.AddSymbol(0x80000000, "main", SymbolType.Function);
// Act
var symbols = manager.Symbols;
// Assert
Assert.Single(symbols);
Assert.IsAssignableFrom<IReadOnlyDictionary<uint, Symbol>>(symbols);
}
[Fact]
public void Comments_ReturnsReadOnlyDictionary()
{
// Arrange
var manager = new SymbolManager();
manager.AddComment(0x80000000, "Test");
// Act
var comments = manager.Comments;
// Assert
Assert.Single(comments);
Assert.IsAssignableFrom<IReadOnlyDictionary<uint, string>>(comments);
}
[Fact]
public void Symbol_ToString_FormatsCorrectly()
{
// Arrange
var symbol = new Symbol
{
Address = 0x80000000,
Name = "main",
Type = SymbolType.Function
};
// Act
string result = symbol.ToString();
// Assert
Assert.Contains("main", result);
Assert.Contains("0x80000000", result);
Assert.Contains("Function", result);
}
[Fact]
public void SymbolType_HasAllExpectedValues()
{
// Assert
Assert.True(Enum.IsDefined(typeof(SymbolType), SymbolType.Function));
Assert.True(Enum.IsDefined(typeof(SymbolType), SymbolType.Label));
Assert.True(Enum.IsDefined(typeof(SymbolType), SymbolType.Data));
Assert.True(Enum.IsDefined(typeof(SymbolType), SymbolType.String));
Assert.True(Enum.IsDefined(typeof(SymbolType), SymbolType.Unknown));
}
[Fact]
public void ExportToText_HandlesEmptyManager()
{
// Arrange
var manager = new SymbolManager();
// Act
string text = manager.ExportToText();
// Assert
Assert.Contains("# Yaroze Symbol File", text);
Assert.Contains("# Functions", text);
Assert.Contains("# Labels", text);
Assert.Contains("# Data", text);
// Should not crash or throw
}
[Fact]
public void ImportFromText_HandlesEmptyString()
{
// Arrange
var manager = new SymbolManager();
// Act
manager.ImportFromText("");
// Assert
Assert.Empty(manager.Symbols);
Assert.Empty(manager.Comments);
}
[Fact]
public void ExportImport_RoundTrip_PreservesData()
{
// Arrange
var manager1 = new SymbolManager();
manager1.AddSymbol(0x80000000, "main", SymbolType.Function);
manager1.AddSymbol(0x80000100, "loop", SymbolType.Label);
manager1.AddSymbol(0x80001000, "data", SymbolType.Data);
manager1.AddComment(0x80000000, "Entry point");
// Act - Export and reimport
string exported = manager1.ExportToText();
var manager2 = new SymbolManager();
manager2.ImportFromText(exported);
// Assert - Should have same data
Assert.Equal(manager1.Symbols.Count, manager2.Symbols.Count);
Assert.Equal(manager1.Comments.Count, manager2.Comments.Count);
foreach (var (address, symbol) in manager1.Symbols)
{
var imported = manager2.GetSymbol(address);
Assert.NotNull(imported);
Assert.Equal(symbol.Name, imported.Name);
Assert.Equal(symbol.Type, imported.Type);
}
foreach (var (address, comment) in manager1.Comments)
{
Assert.Equal(comment, manager2.GetComment(address));
}
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
+270
View File
@@ -0,0 +1,270 @@
using Xunit;
using Yaroze.Core;
using Yaroze.Core.Analysis;
using Yaroze.Core.Interfaces;
using Yaroze.Core.Loaders;
namespace Yaroze.Tests.Analysis;
public class TraceTests
{
[Fact]
public void Trace_InstructionExecution_RecordsCalls()
{
// Arrange
var emu = new Emulator();
var tracer = new TestTracer();
emu.SetTraceSink(tracer);
// Write simple program
emu.Bus.Ram.Write32(0, 0x00000000); // NOP
emu.Bus.Ram.Write32(4, 0x00000000); // NOP
// Act
emu.Step();
emu.Step();
// Assert
Assert.Equal(2, tracer.InstructionCount);
}
[Fact]
public void Trace_MemoryAccess_RecordsReadsAndWrites()
{
// Arrange
var emu = new Emulator();
var tracer = new TestTracer();
emu.SetTraceSink(tracer);
// LW $1, 0x1000($0) - Load from memory
emu.Bus.Ram.Write32(0, 0x8C010000 | 0x1000); // LW $1, 0x1000
emu.Bus.Ram.Write32(0x1000, 0x12345678);
// Act
emu.Step();
emu.Step(); // Delay slot
// Assert - Should have memory reads (instruction fetch + data load)
Assert.True(tracer.MemoryReads > 0);
}
[Fact]
public void Analyzer_CollectsExecutionStats()
{
// Arrange
var emu = new Emulator();
var analyzer = new SimpleAnalyzer();
var composite = new CompositeAnalysisSink(analysisSink: analyzer);
emu.SetTraceSink(composite);
// Write program
for (int i = 0; i < 10; i++)
{
emu.Bus.Ram.Write32((uint)(i * 4), 0x00000000); // NOP
}
// Act
emu.StepN(10);
// Assert
var stats = analyzer.GetStats();
Assert.Equal(10, stats.InstructionsExecuted);
Assert.True(stats.UniqueInstructions > 0);
}
[Fact]
public void Analyzer_TracksUniqueInstructions()
{
// Arrange
var emu = new Emulator();
var analyzer = new SimpleAnalyzer();
var composite = new CompositeAnalysisSink(analysisSink: analyzer);
emu.SetTraceSink(composite);
// Write program that loops
emu.Bus.Ram.Write32(0, 0x08000000); // J 0 (infinite loop)
// Act - Execute same instruction multiple times
emu.StepN(5);
// Assert
var stats = analyzer.GetStats();
Assert.Equal(5, stats.InstructionsExecuted);
Assert.Equal(1, stats.UniqueInstructions); // Only one unique PC
}
[Fact]
public void Analyzer_CanBeReset()
{
// Arrange
var analyzer = new SimpleAnalyzer();
var composite = new CompositeAnalysisSink(analysisSink: analyzer);
var emu = new Emulator();
emu.SetTraceSink(composite);
// Collect some data
emu.Bus.Ram.Write32(0, 0x00000000);
emu.Step();
Assert.True(analyzer.GetStats().InstructionsExecuted > 0);
// Act
analyzer.Reset();
// Assert
Assert.Equal(0, analyzer.GetStats().InstructionsExecuted);
}
[Fact]
public void ConsoleTracer_DoesNotCrash()
{
// Arrange
var emu = new Emulator();
var tracer = new ConsoleTracer(verbose: false);
emu.SetTraceSink(tracer);
emu.Bus.Ram.Write32(0, 0x00000000); // NOP
// Act - should not throw
emu.Step();
}
[Fact]
public void Trace_WithPsExe_CollectsData()
{
// Arrange
var emu = new Emulator();
var analyzer = new SimpleAnalyzer();
var composite = new CompositeAnalysisSink(analysisSink: analyzer);
emu.SetTraceSink(composite);
// Create simple PS-EXE
byte[] exeData = CreateSimpleExe();
emu.LoadExe(exeData);
// Act
emu.StepN(50);
// Assert
var stats = analyzer.GetStats();
Assert.True(stats.InstructionsExecuted >= 50);
Assert.True(stats.UniqueInstructions > 0);
}
[Fact]
public void CompositeAnalysisSink_ForwardsToBothSinks()
{
// Arrange
var tracer = new TestTracer();
var analyzer = new SimpleAnalyzer();
var composite = new CompositeAnalysisSink(tracer, analyzer);
// Act
composite.TraceInstruction(0x80000000, 0x00000000);
// Assert
Assert.Equal(1, tracer.InstructionCount);
Assert.Equal(1, analyzer.GetStats().InstructionsExecuted);
}
[Fact]
public void InstructionInfo_ContainsCorrectData()
{
// Arrange
var info = new InstructionInfo
{
PC = 0x80000000,
InstructionWord = 0x00000000,
Type = InstructionType.Arithmetic,
InDelaySlot = false
};
// Assert
Assert.Equal(0x80000000u, info.PC);
Assert.Equal(0x00000000u, info.InstructionWord);
Assert.Equal(InstructionType.Arithmetic, info.Type);
Assert.False(info.InDelaySlot);
}
[Fact]
public void MemoryAccessInfo_ContainsCorrectData()
{
// Arrange
var info = new MemoryAccessInfo
{
Address = 0x1000,
Value = 0x12345678,
Size = 4,
IsWrite = true,
PC = 0x80000000
};
// Assert
Assert.Equal(0x1000u, info.Address);
Assert.Equal(0x12345678u, info.Value);
Assert.Equal(4, info.Size);
Assert.True(info.IsWrite);
Assert.Equal(0x80000000u, info.PC);
}
// Helper methods
private byte[] CreateSimpleExe()
{
// Create minimal PS-EXE header
byte[] exe = new byte[2048 + 128];
// PS-X EXE header
exe[0] = (byte)'P'; exe[1] = (byte)'S'; exe[2] = (byte)'-';
exe[3] = (byte)'X'; exe[4] = (byte)' '; exe[5] = (byte)'E';
exe[6] = (byte)'X'; exe[7] = (byte)'E';
// PC = 0x80000000
BitConverter.GetBytes(0x80000000u).CopyTo(exe, 0x10);
// File size = 128 bytes
BitConverter.GetBytes(128u).CopyTo(exe, 0x1C);
// Load address = 0x80000000
BitConverter.GetBytes(0x80000000u).CopyTo(exe, 0x18);
// SP = 0x801FFF00
BitConverter.GetBytes(0x801FFF00u).CopyTo(exe, 0x30);
// Simple program (32 NOPs)
for (int i = 0; i < 32; i++)
{
BitConverter.GetBytes(0x00000000u).CopyTo(exe, 2048 + i * 4);
}
return exe;
}
private class TestTracer : ITraceSink
{
public int InstructionCount { get; private set; }
public int MemoryReads { get; private set; }
public int MemoryWrites { get; private set; }
public int Exceptions { get; private set; }
public void TraceInstruction(uint pc, uint instruction, string? disassembly = null)
{
InstructionCount++;
}
public void TraceMemoryRead(uint address, uint value, int size)
{
MemoryReads++;
}
public void TraceMemoryWrite(uint address, uint value, int size)
{
MemoryWrites++;
}
public void TraceException(string exceptionType, uint pc)
{
Exceptions++;
}
}
}
@@ -0,0 +1,166 @@
using System.IO;
using Xunit;
using Yaroze.Core.CDROM;
using Yaroze.Core.Interrupts;
namespace Yaroze.Tests.CDROM;
public class CdRomDeviceTests
{
[Fact]
public void CdRomDevice_ContainsCorrectAddressRange()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
// Act & Assert
Assert.True(cdrom.Contains(0x1F801800));
Assert.True(cdrom.Contains(0x1F801801));
Assert.True(cdrom.Contains(0x1F801802));
Assert.True(cdrom.Contains(0x1F801803));
Assert.False(cdrom.Contains(0x1F801804));
Assert.False(cdrom.Contains(0x1F8017FF));
}
[Fact]
public void CdRomDevice_ReadsStatus()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
// Act
byte status = cdrom.Read8(0x1F801800);
// Assert
// Initial status should have index 0, empty FIFOs
Assert.Equal(0, status & 0x03); // Index = 0
Assert.Equal(0, status & 0x20); // Response FIFO empty
}
[Fact]
public void CdRomDevice_WritesIndexRegister()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
// Act
cdrom.Write8(0x1F801800, 0x01); // Set index to 1
byte status = cdrom.Read8(0x1F801800);
// Assert
Assert.Equal(0x01, status & 0x03); // Index should be 1
}
[Fact]
public void CdRomDevice_ExecutesGetStatCommand()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
// Act
cdrom.Write8(0x1F801800, 0x00); // Set index to 0
cdrom.Write8(0x1F801801, 0x01); // Execute GetStat command
// Read response
byte response = cdrom.Read8(0x1F801801);
// Assert
Assert.Equal(0x02, response); // Motor on status
}
[Fact]
public void CdRomDevice_HandlesSetLocCommand()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
// Act
cdrom.Write8(0x1F801800, 0x00); // Set index to 0
// Send SetLoc parameters (MM, SS, FF in BCD)
cdrom.Write8(0x1F801802, 0x00); // 00 minutes
cdrom.Write8(0x1F801802, 0x02); // 02 seconds
cdrom.Write8(0x1F801802, 0x00); // 00 frames
// Execute SetLoc command
cdrom.Write8(0x1F801801, 0x02);
// Read response
byte response = cdrom.Read8(0x1F801801);
// Assert
Assert.Equal(0x02, response); // Motor on status
}
[Fact]
public void CdRomDevice_LoadsDisc()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
var binPath = CreateTempBinFile(2352 * 100);
try
{
// Act
cdrom.LoadDisc(binPath);
// Assert - no exception should be thrown
// Device should be ready to read sectors
Assert.True(true);
}
finally
{
File.Delete(binPath);
}
}
[Fact]
public void CdRomDevice_ResetClearsState()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
// Set some state
cdrom.Write8(0x1F801800, 0x02); // Set index to 2
cdrom.Write8(0x1F801802, 0xFF); // Write parameter
// Act
cdrom.Reset();
// Assert
byte status = cdrom.Read8(0x1F801800);
Assert.Equal(0, status & 0x03); // Index should be 0
Assert.Equal(0, status & 0x08); // Parameter FIFO should be empty
}
[Fact]
public void CdRomDevice_ReadsDmaWord()
{
// Arrange
var interrupts = new InterruptController();
var cdrom = new CdRomDevice(interrupts);
// Act
uint word = cdrom.ReadDmaWord();
// Assert - Should return 0 when no data is loaded
// (actual behavior depends on implementation)
Assert.True(true); // Just verify no exception
}
private string CreateTempBinFile(int size)
{
var tempPath = Path.Combine(Path.GetTempPath(), $"test_{System.Guid.NewGuid()}.bin");
var data = new byte[size];
File.WriteAllBytes(tempPath, data);
return tempPath;
}
}
+235
View File
@@ -0,0 +1,235 @@
using System;
using System.IO;
using Xunit;
using Yaroze.Core.CDROM;
namespace Yaroze.Tests.CDROM;
public class CueSheetTests
{
[Fact]
public void CueSheet_ParsesSimpleCueFile()
{
// Arrange
var cuePath = CreateTempCueFile(@"
FILE ""game.bin"" BINARY
TRACK 01 MODE2/2352
INDEX 01 00:00:00
");
try
{
// Act
var cueSheet = CueSheet.FromFile(cuePath);
// Assert
Assert.Single(cueSheet.Tracks);
Assert.Equal(1, cueSheet.Tracks[0].Number);
Assert.Equal(TrackMode.Mode2_2352, cueSheet.Tracks[0].Mode);
Assert.Equal("game.bin", cueSheet.Tracks[0].FileName);
Assert.Equal("BINARY", cueSheet.Tracks[0].FileType);
Assert.Equal(0, cueSheet.Tracks[0].Index01);
}
finally
{
File.Delete(cuePath);
}
}
[Fact]
public void CueSheet_ParsesMultiTrackCue()
{
// Arrange
var cuePath = CreateTempCueFile(@"
FILE ""game.bin"" BINARY
TRACK 01 MODE2/2352
INDEX 01 00:00:00
TRACK 02 AUDIO
INDEX 01 10:30:45
TRACK 03 AUDIO
INDEX 01 15:20:12
");
try
{
// Act
var cueSheet = CueSheet.FromFile(cuePath);
// Assert
Assert.Equal(3, cueSheet.Tracks.Count);
Assert.Equal(1, cueSheet.Tracks[0].Number);
Assert.Equal(TrackMode.Mode2_2352, cueSheet.Tracks[0].Mode);
Assert.Equal(0, cueSheet.Tracks[0].Index01);
Assert.Equal(2, cueSheet.Tracks[1].Number);
Assert.Equal(TrackMode.Audio, cueSheet.Tracks[1].Mode);
// 10:30:45 = (10*60 + 30)*75 + 45 = 47295 frames
Assert.Equal(47295, cueSheet.Tracks[1].Index01);
Assert.Equal(3, cueSheet.Tracks[2].Number);
Assert.Equal(TrackMode.Audio, cueSheet.Tracks[2].Mode);
// 15:20:12 = (15*60 + 20)*75 + 12 = 69012 frames
Assert.Equal(69012, cueSheet.Tracks[2].Index01);
}
finally
{
File.Delete(cuePath);
}
}
[Fact]
public void CueSheet_ParsesPregapIndex00()
{
// Arrange
var cuePath = CreateTempCueFile(@"
FILE ""game.bin"" BINARY
TRACK 01 MODE2/2352
INDEX 00 00:00:00
INDEX 01 00:02:00
");
try
{
// Act
var cueSheet = CueSheet.FromFile(cuePath);
// Assert
Assert.Single(cueSheet.Tracks);
Assert.Equal(0, cueSheet.Tracks[0].Index00);
// 00:02:00 = 2*60*75 = 9000 frames
Assert.Equal(9000, cueSheet.Tracks[0].Index01);
}
finally
{
File.Delete(cuePath);
}
}
[Fact]
public void CueSheet_ParsesDifferentTrackModes()
{
// Arrange
var cuePath = CreateTempCueFile(@"
FILE ""data.bin"" BINARY
TRACK 01 MODE1/2048
INDEX 01 00:00:00
TRACK 02 MODE2/2336
INDEX 01 05:00:00
");
try
{
// Act
var cueSheet = CueSheet.FromFile(cuePath);
// Assert
Assert.Equal(2, cueSheet.Tracks.Count);
Assert.Equal(TrackMode.Mode1_2048, cueSheet.Tracks[0].Mode);
Assert.Equal(TrackMode.Mode2_2336, cueSheet.Tracks[1].Mode);
}
finally
{
File.Delete(cuePath);
}
}
[Fact]
public void CueSheet_ThrowsOnInvalidMode()
{
// Arrange
var cuePath = CreateTempCueFile(@"
FILE ""game.bin"" BINARY
TRACK 01 INVALID_MODE
INDEX 01 00:00:00
");
try
{
// Act & Assert
Assert.Throws<NotSupportedException>(() => CueSheet.FromFile(cuePath));
}
finally
{
File.Delete(cuePath);
}
}
[Fact]
public void CueSheet_ThrowsOnEmptyCue()
{
// Arrange
var cuePath = CreateTempCueFile("");
try
{
// Act & Assert
Assert.Throws<InvalidDataException>(() => CueSheet.FromFile(cuePath));
}
finally
{
File.Delete(cuePath);
}
}
[Fact]
public void CueSheet_ThrowsOnTrackBeforeFile()
{
// Arrange
var cuePath = CreateTempCueFile(@"
TRACK 01 MODE2/2352
INDEX 01 00:00:00
");
try
{
// Act & Assert
Assert.Throws<InvalidDataException>(() => CueSheet.FromFile(cuePath));
}
finally
{
File.Delete(cuePath);
}
}
[Fact]
public void Track_SectorSize_ReturnsCorrectValues()
{
// Arrange & Act & Assert
var track1 = new Track(1, TrackMode.Audio, "test.bin", "BINARY");
Assert.Equal(2352, track1.SectorSize);
var track2 = new Track(2, TrackMode.Mode1_2048, "test.bin", "BINARY");
Assert.Equal(2048, track2.SectorSize);
var track3 = new Track(3, TrackMode.Mode1_2352, "test.bin", "BINARY");
Assert.Equal(2352, track3.SectorSize);
var track4 = new Track(4, TrackMode.Mode2_2336, "test.bin", "BINARY");
Assert.Equal(2336, track4.SectorSize);
var track5 = new Track(5, TrackMode.Mode2_2352, "test.bin", "BINARY");
Assert.Equal(2352, track5.SectorSize);
}
[Fact]
public void Track_UserDataSize_ReturnsCorrectValues()
{
// Arrange & Act & Assert
var track1 = new Track(1, TrackMode.Mode1_2048, "test.bin", "BINARY");
Assert.Equal(2048, track1.UserDataSize);
var track2 = new Track(2, TrackMode.Mode1_2352, "test.bin", "BINARY");
Assert.Equal(2048, track2.UserDataSize); // Extracts 2048 from raw
var track3 = new Track(3, TrackMode.Mode2_2352, "test.bin", "BINARY");
Assert.Equal(2352, track3.UserDataSize);
}
private string CreateTempCueFile(string content)
{
var tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.cue");
File.WriteAllText(tempPath, content);
return tempPath;
}
}
+234
View File
@@ -0,0 +1,234 @@
using System;
using System.IO;
using Xunit;
using Yaroze.Core.CDROM;
namespace Yaroze.Tests.CDROM;
public class DiscImageTests
{
[Fact]
public void DiscImage_LoadsRawBinFile()
{
// Arrange
var binPath = CreateTempBinFile(2352 * 100); // 100 sectors
try
{
// Act
using var disc = DiscImage.Load(binPath);
// Assert
Assert.Single(disc.Tracks);
Assert.Equal(1, disc.Tracks[0].Number);
Assert.Equal(TrackMode.Mode2_2352, disc.Tracks[0].Mode);
Assert.Equal(100, disc.GetSectorCount());
}
finally
{
File.Delete(binPath);
}
}
[Fact]
public void DiscImage_LoadsIsoFile()
{
// Arrange
var isoPath = CreateTempIsoFile(2048 * 50); // 50 sectors of MODE1/2048
try
{
// Act
using var disc = DiscImage.Load(isoPath);
// Assert
Assert.Single(disc.Tracks);
Assert.Equal(TrackMode.Mode2_2352, disc.Tracks[0].Mode); // Assumes raw format
}
finally
{
File.Delete(isoPath);
}
}
[Fact]
public void DiscImage_LoadsFromCueSheet()
{
// Arrange
var binPath = CreateTempBinFile(2352 * 100);
var binName = Path.GetFileName(binPath);
var cueDir = Path.GetDirectoryName(binPath)!;
var cuePath = Path.Combine(cueDir, Path.GetFileNameWithoutExtension(binPath) + ".cue");
var cueContent = $@"
FILE ""{binName}"" BINARY
TRACK 01 MODE2/2352
INDEX 01 00:00:00
";
File.WriteAllText(cuePath, cueContent);
try
{
// Act
using var disc = DiscImage.Load(cuePath);
// Assert
Assert.Single(disc.Tracks);
Assert.Equal(1, disc.Tracks[0].Number);
Assert.Equal(TrackMode.Mode2_2352, disc.Tracks[0].Mode);
}
finally
{
File.Delete(binPath);
File.Delete(cuePath);
}
}
[Fact]
public void DiscImage_AutoDetectsCueFile()
{
// Arrange
var binPath = CreateTempBinFile(2352 * 100);
var binName = Path.GetFileName(binPath);
var cueDir = Path.GetDirectoryName(binPath)!;
var cuePath = Path.Combine(cueDir, Path.GetFileNameWithoutExtension(binPath) + ".cue");
var cueContent = $@"
FILE ""{binName}"" BINARY
TRACK 01 MODE2/2352
INDEX 01 00:00:00
";
File.WriteAllText(cuePath, cueContent);
try
{
// Act - Load BIN file, should auto-detect CUE
using var disc = DiscImage.Load(binPath);
// Assert - Should have loaded from CUE sheet
Assert.Single(disc.Tracks);
Assert.Equal(TrackMode.Mode2_2352, disc.Tracks[0].Mode);
}
finally
{
File.Delete(binPath);
File.Delete(cuePath);
}
}
[Fact]
public void DiscImage_ReadsSector()
{
// Arrange
var data = new byte[2352 * 10];
for (int i = 0; i < data.Length; i++)
{
data[i] = (byte)(i % 256);
}
var binPath = CreateTempBinFile(data);
try
{
using var disc = DiscImage.Load(binPath);
// Act
var buffer = new byte[2352];
int bytesRead = disc.ReadSector(5, buffer);
// Assert
Assert.Equal(2352, bytesRead);
for (int i = 0; i < 2352; i++)
{
Assert.Equal((byte)((5 * 2352 + i) % 256), buffer[i]);
}
}
finally
{
File.Delete(binPath);
}
}
[Fact]
public void DiscImage_ReadsUserData()
{
// Arrange
var sectorData = new byte[2352];
// Fill with header pattern (24 bytes) + user data
for (int i = 0; i < 24; i++)
sectorData[i] = 0xFF; // Header
for (int i = 24; i < 24 + 2048; i++)
sectorData[i] = (byte)((i - 24) % 256); // User data
var binPath = CreateTempBinFile(sectorData);
try
{
using var disc = DiscImage.Load(binPath);
// Act
var buffer = new byte[2048];
int bytesRead = disc.ReadSectorUserData(0, buffer);
// Assert
Assert.Equal(2048, bytesRead);
for (int i = 0; i < 2048; i++)
{
Assert.Equal((byte)(i % 256), buffer[i]);
}
}
finally
{
File.Delete(binPath);
}
}
[Fact]
public void DiscImage_ThrowsOnNonExistentFile()
{
// Act & Assert
Assert.Throws<FileNotFoundException>(() => DiscImage.Load("nonexistent.bin"));
}
[Fact]
public void DiscImage_ThrowsOnUnsupportedFormat()
{
// Arrange
var txtPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.txt");
File.WriteAllText(txtPath, "test");
try
{
// Act & Assert
Assert.Throws<NotSupportedException>(() => DiscImage.Load(txtPath));
}
finally
{
File.Delete(txtPath);
}
}
private string CreateTempBinFile(int size)
{
var data = new byte[size];
new Random(42).NextBytes(data);
return CreateTempBinFile(data);
}
private string CreateTempBinFile(byte[] data)
{
var tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.bin");
File.WriteAllBytes(tempPath, data);
return tempPath;
}
private string CreateTempIsoFile(int size)
{
var tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.iso");
var data = new byte[size];
new Random(42).NextBytes(data);
File.WriteAllBytes(tempPath, data);
return tempPath;
}
}
+287
View File
@@ -0,0 +1,287 @@
using Yaroze.Core.CPU;
using Yaroze.Core.Memory;
namespace Yaroze.Tests.CPU;
public class ExceptionTests
{
private Cpu CreateCpu()
{
var bus = new Bus();
var cpu = new Cpu(bus);
return cpu;
}
private void WriteInstruction(Cpu cpu, uint address, uint instruction)
{
cpu.Bus.Ram.Write32(address, instruction);
}
[Fact]
public void COP0_InitialState_CorrectValues()
{
var cpu = CreateCpu();
// BEV should be set (bootstrap vectors)
Assert.True((cpu.Cop0.StatusRegister & 0x00400000) != 0);
// CU0 should be set (COP0 usable)
Assert.True((cpu.Cop0.StatusRegister & 0x10000000) != 0);
// Processor ID should be 2 (R3000A)
Assert.Equal(2u, cpu.Cop0.ReadRegister(15));
}
[Fact]
public void MFC0_ReadsCOP0Register()
{
var cpu = CreateCpu();
// MFC0 $1, $12 (read Status Register into $1)
// opcode=0x10, rs=0x00, rt=1, rd=12
uint instruction = 0x40016000;
WriteInstruction(cpu, 0xBFC00000, instruction);
WriteInstruction(cpu, 0xBFC00004, 0x00000000); // NOP (delay slot)
cpu.Registers.PC = 0xBFC00000;
// Execute MFC0
cpu.Step();
// Value not available yet (load delay)
Assert.Equal(0u, cpu.Registers.ReadGPR(1));
// Execute delay slot
cpu.Step();
// Now $1 should contain Status Register value
uint expectedSR = cpu.Cop0.StatusRegister;
Assert.Equal(expectedSR, cpu.Registers.ReadGPR(1));
}
[Fact]
public void MTC0_WritesCOP0Register()
{
var cpu = CreateCpu();
// Set up $1 with a test value
cpu.Registers.WriteGPR(1, 0x12345678);
// MTC0 $1, $3 (write $1 to BPC register)
// opcode=0x10, rs=0x04, rt=1, rd=3
uint instruction = 0x40816000;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// BPC should now contain the value
Assert.Equal(0x12345678u, cpu.Cop0.ReadRegister(3));
}
[Fact]
public void SYSCALL_TriggersException()
{
var cpu = CreateCpu();
// SYSCALL instruction
uint instruction = 0x0000000C;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// PC should be at exception vector
uint expectedVector = 0xBFC00180; // BEV=1, so BIOS vector
Assert.Equal(expectedVector, cpu.Registers.PC);
// EPC should contain address of SYSCALL
Assert.Equal(0xBFC00000u, cpu.Cop0.ExceptionPC);
// CAUSE should have Syscall exception code (0x08 << 2 = 0x20)
uint cause = cpu.Cop0.CauseRegister;
uint excCode = (cause >> 2) & 0x1F;
Assert.Equal(0x08u, excCode);
}
[Fact]
public void BREAK_TriggersException()
{
var cpu = CreateCpu();
// BREAK instruction
uint instruction = 0x0000000D;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// PC should be at exception vector
Assert.Equal(0xBFC00180u, cpu.Registers.PC);
// CAUSE should have Breakpoint exception code (0x09)
uint cause = cpu.Cop0.CauseRegister;
uint excCode = (cause >> 2) & 0x1F;
Assert.Equal(0x09u, excCode);
}
[Fact]
public void RFE_RestoresInterruptEnableStack()
{
var cpu = CreateCpu();
// Manually set up exception state
cpu.Cop0.SetException(ExceptionCode.Syscall, 0x80001000, false);
// Status register should have interrupts disabled
Assert.False(cpu.Cop0.InterruptsEnabled);
// RFE instruction (COP0 with funct=0x10)
// opcode=0x10, rs=0x10, funct=0x10
uint instruction = 0x42000010;
WriteInstruction(cpu, 0xBFC00180, instruction);
cpu.Registers.PC = 0xBFC00180;
cpu.Step();
// RFE should have popped the interrupt stack
// (actual enable state depends on what was pushed)
}
[Fact]
public void Exception_InDelaySlot_SetsBDFlag()
{
var cpu = CreateCpu();
// BEQ $0, $0, +4 (always taken)
uint branchInstr = 0x10000001;
// SYSCALL (in delay slot)
uint syscallInstr = 0x0000000C;
WriteInstruction(cpu, 0xBFC00000, branchInstr);
WriteInstruction(cpu, 0xBFC00004, syscallInstr);
cpu.Registers.PC = 0xBFC00000;
// Execute branch
cpu.Step();
// Execute syscall (in delay slot)
cpu.Step();
// CAUSE BD bit should be set
uint cause = cpu.Cop0.CauseRegister;
Assert.True((cause & 0x80000000) != 0);
// EPC should point to the branch instruction, not the syscall
Assert.Equal(0xBFC00000u, cpu.Cop0.ExceptionPC);
}
[Fact]
public void AddressError_MisalignedLoad_TriggersException()
{
var cpu = CreateCpu();
// LW $1, 1($0) - misaligned address
uint instruction = 0x8C010001;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// Should trigger AddressErrorLoad exception
uint cause = cpu.Cop0.CauseRegister;
uint excCode = (cause >> 2) & 0x1F;
Assert.Equal(0x04u, excCode); // AdEL
// BadVAddr should contain the misaligned address
Assert.Equal(0x00000001u, cpu.Cop0.ReadRegister(8));
}
[Fact]
public void AddressError_MisalignedStore_TriggersException()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 0x12345678);
// SW $1, 1($0) - misaligned address
uint instruction = 0xAC010001;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// Should trigger AddressErrorStore exception
uint cause = cpu.Cop0.CauseRegister;
uint excCode = (cause >> 2) & 0x1F;
Assert.Equal(0x05u, excCode); // AdES
}
[Fact]
public void UnalignedLoad_LWL_LWR_WorkCorrectly()
{
var cpu = CreateCpu();
// Write test data: 0x12345678 at address 0x1000
cpu.Bus.Ram.Write32(0x1000, 0x12345678);
// Load from unaligned address 0x1001 using LWL/LWR
cpu.Registers.WriteGPR(2, 0x00001000);
// LWL $1, 1($2) - load from 0x1001
uint lwlInstr = 0x8C410001;
// LWR $1, 4($2) - load from 0x1004
uint lwrInstr = 0x9C410004;
WriteInstruction(cpu, 0xBFC00000, lwlInstr);
WriteInstruction(cpu, 0xBFC00004, 0x00000000); // NOP
WriteInstruction(cpu, 0xBFC00008, lwrInstr);
WriteInstruction(cpu, 0xBFC0000C, 0x00000000); // NOP
cpu.Registers.PC = 0xBFC00000;
// Execute LWL
cpu.Step();
cpu.Step(); // Delay slot
// Execute LWR
cpu.Step();
cpu.Step(); // Delay slot
// $1 should now contain the word loaded from misaligned address
// The exact value depends on endianness and LWL/LWR implementation
uint result = cpu.Registers.ReadGPR(1);
// With proper LWL/LWR, we should be able to load a word from any alignment
Assert.NotEqual(0u, result);
}
[Fact]
public void InterruptsEnabled_CheckedCorrectly()
{
var cpu = CreateCpu();
// Initially interrupts should be enabled (IEc bit)
// Actually, on reset with BEV=1, interrupts might be disabled
// Let's set them explicitly
uint sr = cpu.Cop0.ReadRegister(12);
sr |= 0x00000001; // Set IEc
cpu.Cop0.WriteRegister(12, sr);
Assert.True(cpu.Cop0.InterruptsEnabled);
// Trigger an exception (disables interrupts)
cpu.Cop0.SetException(ExceptionCode.Syscall, 0x80001000, false);
Assert.False(cpu.Cop0.InterruptsEnabled);
}
}
+222
View File
@@ -0,0 +1,222 @@
using Xunit;
using Yaroze.Core.CPU;
using Yaroze.Core.Memory;
namespace Yaroze.Tests.CPU;
public class GteTests
{
private readonly Cpu _cpu;
public GteTests()
{
var bus = new Bus();
_cpu = new Cpu(bus);
}
[Fact]
public void Gte_Reset_ClearsAllRegisters()
{
// Arrange
_cpu.Gte.WriteDataRegister(5, 0x12345678);
_cpu.Gte.WriteControlRegister(10, 0xABCDEF00);
// Act
_cpu.Gte.Reset();
// Assert
Assert.Equal(0u, _cpu.Gte.ReadDataRegister(5));
Assert.Equal(0u, _cpu.Gte.ReadControlRegister(10));
}
[Fact]
public void Gte_ReadWriteDataRegister_WorksCorrectly()
{
// Arrange
uint testValue = 0xDEADBEEF;
// Act
_cpu.Gte.WriteDataRegister(15, testValue);
uint result = _cpu.Gte.ReadDataRegister(15);
// Assert
Assert.Equal(testValue, result);
}
[Fact]
public void Gte_ReadWriteControlRegister_WorksCorrectly()
{
// Arrange
uint testValue = 0x12345678;
// Act
_cpu.Gte.WriteControlRegister(20, testValue);
uint result = _cpu.Gte.ReadControlRegister(20);
// Assert
Assert.Equal(testValue, result);
}
[Fact]
public void Gte_ReadInvalidRegister_ReturnsZero()
{
// Act & Assert
Assert.Equal(0u, _cpu.Gte.ReadDataRegister(100));
Assert.Equal(0u, _cpu.Gte.ReadControlRegister(100));
}
[Fact]
public void Gte_WriteInvalidRegister_DoesNotCrash()
{
// Act - should not throw
_cpu.Gte.WriteDataRegister(100, 0x12345678);
_cpu.Gte.WriteControlRegister(100, 0xABCDEF00);
}
[Fact]
public void MFC2_MovesFromGteDataRegister()
{
// Arrange - MFC2 $t1, $15 (0x48090000)
// Opcode: 0x12 (COP2), CopOp: 0x00 (MFC2), Rt: 9 ($t1), Rd: 15
_cpu.Gte.WriteDataRegister(15, 0xBAADF00D);
_cpu.Bus.Ram.Write32(0, 0x48090000 | (15 << 11));
// Act
_cpu.Step(); // MFC2
Assert.Equal(0u, _cpu.Registers.ReadGPR(9)); // Load delay
_cpu.Step(); // Delay slot (NOP)
// Assert
Assert.Equal(0xBAADF00Du, _cpu.Registers.ReadGPR(9));
}
[Fact]
public void MTC2_MovesToGteDataRegister()
{
// Arrange - MTC2 $t1, $15 (0x48890000)
// Opcode: 0x12 (COP2), CopOp: 0x04 (MTC2), Rt: 9 ($t1), Rd: 15
_cpu.Registers.WriteGPR(9, 0xCAFEBABE);
_cpu.Bus.Ram.Write32(0, 0x48890000 | (15 << 11));
// Act
_cpu.Step(); // MTC2
// Assert
Assert.Equal(0xCAFEBABEu, _cpu.Gte.ReadDataRegister(15));
}
[Fact]
public void CFC2_MovesFromGteControlRegister()
{
// Arrange - CFC2 $t1, $20 (0x484A0000)
// Opcode: 0x12 (COP2), CopOp: 0x02 (CFC2), Rt: 9 ($t1), Rd: 20
_cpu.Gte.WriteControlRegister(20, 0x11111111);
_cpu.Bus.Ram.Write32(0, 0x48490000 | (20 << 11));
// Act
_cpu.Step(); // CFC2
Assert.Equal(0u, _cpu.Registers.ReadGPR(9)); // Load delay
_cpu.Step(); // Delay slot (NOP)
// Assert
Assert.Equal(0x11111111u, _cpu.Registers.ReadGPR(9));
}
[Fact]
public void CTC2_MovesToGteControlRegister()
{
// Arrange - CTC2 $t1, $20 (0x48C90000)
// Opcode: 0x12 (COP2), CopOp: 0x06 (CTC2), Rt: 9 ($t1), Rd: 20
_cpu.Registers.WriteGPR(9, 0x22222222);
_cpu.Bus.Ram.Write32(0, 0x48C90000 | (20 << 11));
// Act
_cpu.Step(); // CTC2
// Assert
Assert.Equal(0x22222222u, _cpu.Gte.ReadControlRegister(20));
}
[Fact]
public void LWC2_LoadsWordToGteDataRegister()
{
// Arrange - LWC2 $15, 0x100($t0)
// Opcode: 0x32 (LWC2), Base: 8 ($t0), Rt: 15, Offset: 0x100
_cpu.Registers.WriteGPR(8, 0x00000000);
_cpu.Bus.Ram.Write32(0x100, 0x99999999);
_cpu.Bus.Ram.Write32(0, 0xC8080100); // LWC2
// Act
_cpu.Step();
// Assert
Assert.Equal(0x99999999u, _cpu.Gte.ReadDataRegister(15));
}
[Fact]
public void SWC2_StoresWordFromGteDataRegister()
{
// Arrange - SWC2 $15, 0x200($t0)
// Opcode: 0x3A (SWC2), Base: 8 ($t0), Rt: 15, Offset: 0x200
_cpu.Registers.WriteGPR(8, 0x00000000);
_cpu.Gte.WriteDataRegister(15, 0x88888888);
_cpu.Bus.Ram.Write32(0, 0xE8080200); // SWC2
// Act
_cpu.Step();
// Assert
Assert.Equal(0x88888888u, _cpu.Bus.Ram.Read32(0x200));
}
[Fact]
public void GteCommand_Executes_WithoutCrashing()
{
// Arrange - GTE RTPS command (0x4A180001) - Perspective Transformation
// Verifies that GTE commands execute without throwing exceptions
_cpu.Bus.Ram.Write32(0, 0x4A180001);
// Act - should not throw
_cpu.Step();
// Assert: Execution completes successfully
// Note: Full GTE calculations are not implemented, but command processing works
}
[Fact]
public void GteCommand_ClearsFlagRegister()
{
// Arrange
_cpu.Gte.WriteDataRegister(31, 0xFFFFFFFF); // Set FLAG register with errors
_cpu.Bus.Ram.Write32(0, 0x4A180001); // GTE command (RTPS)
// Act
_cpu.Step();
// Assert: FLAG register cleared to indicate successful execution
Assert.Equal(0u, _cpu.Gte.ReadDataRegister(31));
}
[Fact]
public void Gte_GetDataRegisterName_ReturnsCorrectNames()
{
// Test a few key register names
Assert.Equal("VXY0", Gte.GetDataRegisterName(0));
Assert.Equal("VZ0", Gte.GetDataRegisterName(1));
Assert.Equal("IR0", Gte.GetDataRegisterName(8));
Assert.Equal("SXY0", Gte.GetDataRegisterName(12));
Assert.Equal("MAC0", Gte.GetDataRegisterName(24));
Assert.Equal("LZCR", Gte.GetDataRegisterName(31));
}
[Fact]
public void Gte_GetControlRegisterName_ReturnsCorrectNames()
{
// Test a few key register names
Assert.Equal("R11R12", Gte.GetControlRegisterName(0));
Assert.Equal("TRX", Gte.GetControlRegisterName(5));
Assert.Equal("L11L12", Gte.GetControlRegisterName(8));
Assert.Equal("OFX", Gte.GetControlRegisterName(24));
Assert.Equal("FLAG", Gte.GetControlRegisterName(31));
}
}
+382
View File
@@ -0,0 +1,382 @@
using Yaroze.Core.CPU;
using Yaroze.Core.Memory;
namespace Yaroze.Tests.CPU;
public class InstructionTests
{
private Cpu CreateCpu()
{
var bus = new Bus();
var cpu = new Cpu(bus);
return cpu;
}
private void WriteInstruction(Cpu cpu, uint address, uint instruction)
{
cpu.Bus.Ram.Write32(address, instruction);
}
[Fact]
public void ADDU_AddsRegistersCorrectly()
{
var cpu = CreateCpu();
// Set up registers
cpu.Registers.WriteGPR(1, 100);
cpu.Registers.WriteGPR(2, 200);
// ADDU $3, $1, $2
// opcode=0, funct=0x21, rs=1, rt=2, rd=3
uint instruction = 0x00221821; // ADDU $3, $1, $2
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(300u, cpu.Registers.ReadGPR(3));
}
[Fact]
public void ADDIU_AddsImmediateCorrectly()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 100);
// ADDIU $2, $1, 50
// opcode=0x09, rs=1, rt=2, imm=50
uint instruction = 0x24220032; // ADDIU $2, $1, 50
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(150u, cpu.Registers.ReadGPR(2));
}
[Fact]
public void ADDIU_SignExtendsNegativeImmediate()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 100);
// ADDIU $2, $1, -10 (0xFFF6 in 16-bit)
uint instruction = 0x2422FFF6;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(90u, cpu.Registers.ReadGPR(2));
}
[Fact]
public void AND_PerformsBitwiseAnd()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 0b11110000);
cpu.Registers.WriteGPR(2, 0b10101010);
// AND $3, $1, $2
uint instruction = 0x00221824;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(0b10100000u, cpu.Registers.ReadGPR(3));
}
[Fact]
public void OR_PerformsBitwiseOr()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 0b11110000);
cpu.Registers.WriteGPR(2, 0b10101010);
// OR $3, $1, $2
uint instruction = 0x00221825;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(0b11111010u, cpu.Registers.ReadGPR(3));
}
[Fact]
public void SLL_ShiftsLeftCorrectly()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 0x00000001);
// SLL $2, $1, 4
uint instruction = 0x00010900;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(0x00000010u, cpu.Registers.ReadGPR(2));
}
[Fact]
public void SRL_ShiftsRightLogical()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 0x80000000);
// SRL $2, $1, 4
uint instruction = 0x00010902;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(0x08000000u, cpu.Registers.ReadGPR(2));
}
[Fact]
public void LUI_LoadsUpperImmediate()
{
var cpu = CreateCpu();
// LUI $1, 0x1234
uint instruction = 0x3C011234;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(0x12340000u, cpu.Registers.ReadGPR(1));
}
[Fact]
public void LW_LoadsWordFromMemory_WithDelay()
{
var cpu = CreateCpu();
// Write test data to memory
cpu.Bus.Ram.Write32(0x1000, 0xDEADBEEF);
// Set base address
cpu.Registers.WriteGPR(1, 0x00000F00);
// LW $2, 0x100($1) - load from 0x1000
uint instruction = 0x8C220100;
WriteInstruction(cpu, 0xBFC00000, instruction);
WriteInstruction(cpu, 0xBFC00004, 0x00000000); // NOP in delay slot
cpu.Registers.PC = 0xBFC00000;
// Execute LW
cpu.Step();
// Value should NOT be available yet (load delay)
Assert.Equal(0u, cpu.Registers.ReadGPR(2));
// Execute delay slot (NOP)
cpu.Step();
// Now value should be available
Assert.Equal(0xDEADBEEFu, cpu.Registers.ReadGPR(2));
}
[Fact]
public void SW_StoresWordToMemory()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 0x00001000);
cpu.Registers.WriteGPR(2, 0xCAFEBABE);
// SW $2, 0($1)
uint instruction = 0xAC220000;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
Assert.Equal(0xCAFEBABEu, cpu.Bus.Ram.Read32(0x1000));
}
[Fact]
public void BEQ_BranchesWhenEqual()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 100);
cpu.Registers.WriteGPR(2, 100);
// BEQ $1, $2, +8 (skip 2 instructions)
uint instruction = 0x10220002;
WriteInstruction(cpu, 0xBFC00000, instruction);
WriteInstruction(cpu, 0xBFC00004, 0x00000000); // Delay slot
cpu.Registers.PC = 0xBFC00000;
// Execute BEQ
cpu.Step();
// PC should advance to delay slot
Assert.Equal(0xBFC00004u, cpu.Registers.PC);
// Execute delay slot
cpu.Step();
// PC should be at branch target (0xBFC00000 + 4 + 8)
Assert.Equal(0xBFC0000Cu, cpu.Registers.PC);
}
[Fact]
public void BNE_DoesNotBranchWhenEqual()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 100);
cpu.Registers.WriteGPR(2, 100);
// BNE $1, $2, +8
uint instruction = 0x14220002;
WriteInstruction(cpu, 0xBFC00000, instruction);
WriteInstruction(cpu, 0xBFC00004, 0x00000000); // Delay slot
cpu.Registers.PC = 0xBFC00000;
// Execute BNE
cpu.Step();
// Execute delay slot
cpu.Step();
// Should continue to next instruction (no branch)
Assert.Equal(0xBFC00008u, cpu.Registers.PC);
}
[Fact]
public void JAL_JumpsAndLinksCorrectly()
{
var cpu = CreateCpu();
// JAL 0x00100000 (target26 = 0x040000)
uint instruction = 0x0C040000;
WriteInstruction(cpu, 0xBFC00000, instruction);
WriteInstruction(cpu, 0xBFC00004, 0x00000000); // Delay slot
cpu.Registers.PC = 0xBFC00000;
// Execute JAL
cpu.Step();
// $ra should be set to return address (PC + 8)
Assert.Equal(0xBFC00008u, cpu.Registers.ReadGPR(31));
// Execute delay slot
cpu.Step();
// PC should be at jump target
Assert.Equal(0xB0100000u, cpu.Registers.PC);
}
[Fact]
public void MULT_MultipliesSignedCorrectly()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 1000);
cpu.Registers.WriteGPR(2, 2000);
// MULT $1, $2
uint instruction = 0x00220018;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// Result = 1000 * 2000 = 2,000,000 = 0x001E8480
Assert.Equal(0x001E8480u, cpu.Registers.LO);
Assert.Equal(0u, cpu.Registers.HI);
}
[Fact]
public void DIV_DividesSignedCorrectly()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, 100);
cpu.Registers.WriteGPR(2, 7);
// DIV $1, $2
uint instruction = 0x0022001A;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// 100 / 7 = 14 remainder 2
Assert.Equal(14u, cpu.Registers.LO);
Assert.Equal(2u, cpu.Registers.HI);
}
[Fact]
public void SLT_SetsOneLessThan_Signed()
{
var cpu = CreateCpu();
cpu.Registers.WriteGPR(1, unchecked((uint)-10)); // -10 as signed
cpu.Registers.WriteGPR(2, 5);
// SLT $3, $1, $2
uint instruction = 0x0022182A;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
cpu.Step();
// -10 < 5, so $3 should be 1
Assert.Equal(1u, cpu.Registers.ReadGPR(3));
}
[Fact]
public void NOP_DoesNothing()
{
var cpu = CreateCpu();
// Set up a register
cpu.Registers.WriteGPR(5, 0x12345678);
// NOP (all zeros)
uint instruction = 0x00000000;
WriteInstruction(cpu, 0xBFC00000, instruction);
cpu.Registers.PC = 0xBFC00000;
uint initialPC = cpu.Registers.PC;
cpu.Step();
// Register should be unchanged
Assert.Equal(0x12345678u, cpu.Registers.ReadGPR(5));
// PC should advance
Assert.Equal(initialPC + 4, cpu.Registers.PC);
}
}
+182
View File
@@ -0,0 +1,182 @@
using Yaroze.Core.CPU;
namespace Yaroze.Tests.CPU;
public class RegisterTests
{
[Fact]
public void RegisterZero_AlwaysReturnsZero()
{
var registers = new Registers();
// $0 should always read as 0
Assert.Equal(0u, registers.ReadGPR(0));
// Writing to $0 should have no effect
registers.WriteGPR(0, 0x12345678);
Assert.Equal(0u, registers.ReadGPR(0));
}
[Fact]
public void GeneralPurposeRegisters_ReadWriteCorrectly()
{
var registers = new Registers();
// Test all registers except $0
for (uint i = 1; i < 32; i++)
{
uint testValue = 0xDEADBEEF + i;
registers.WriteGPR(i, testValue);
Assert.Equal(testValue, registers.ReadGPR(i));
}
}
[Fact]
public void HI_LO_Registers_ReadWriteCorrectly()
{
var registers = new Registers();
registers.HI = 0x12345678;
registers.LO = 0x9ABCDEF0;
Assert.Equal(0x12345678u, registers.HI);
Assert.Equal(0x9ABCDEF0u, registers.LO);
}
[Fact]
public void PC_InitializedToBiosEntryPoint()
{
var registers = new Registers();
Assert.Equal(0xBFC00000u, registers.PC);
}
[Fact]
public void Reset_RestoresInitialState()
{
var registers = new Registers();
// Modify some registers
registers.WriteGPR(5, 0x12345678);
registers.HI = 0xAAAAAAAA;
registers.LO = 0xBBBBBBBB;
registers.PC = 0x80000000;
// Reset
registers.Reset();
// Check all registers are cleared
for (uint i = 0; i < 32; i++)
{
Assert.Equal(0u, registers.ReadGPR(i));
}
Assert.Equal(0u, registers.HI);
Assert.Equal(0u, registers.LO);
Assert.Equal(0xBFC00000u, registers.PC);
}
[Fact]
public void LoadDelay_ValueNotAvailableInDelaySlot()
{
var registers = new Registers();
// Set initial value
registers.WriteGPR(5, 0x11111111);
// Simulate load delay
registers.SetLoadDelay(5, 0x22222222);
// Value should still be old value before commit
Assert.Equal(0x11111111u, registers.ReadGPR(5));
// Commit the load delay
registers.CommitLoadDelay();
// Now value should be updated
Assert.Equal(0x22222222u, registers.ReadGPR(5));
}
[Fact]
public void LoadDelay_ToRegisterZero_IsIgnored()
{
var registers = new Registers();
registers.SetLoadDelay(0, 0x12345678);
registers.CommitLoadDelay();
// $0 should still be 0
Assert.Equal(0u, registers.ReadGPR(0));
}
[Fact]
public void LoadDelay_CanceledByWriteToSameRegister()
{
var registers = new Registers();
// Set up a load delay
registers.SetLoadDelay(5, 0x11111111);
// Write to the same register (simulating delay slot instruction)
registers.CancelLoadDelay(5);
registers.WriteGPR(5, 0x22222222);
// Commit should not overwrite the direct write
registers.CommitLoadDelay();
Assert.Equal(0x22222222u, registers.ReadGPR(5));
}
[Fact]
public void BranchDelay_BranchTakenAfterDelaySlot()
{
var registers = new Registers();
registers.PC = 0x80000000;
// Set branch to 0x80001000
registers.SetBranch(0x80001000, true);
// PC should not change until commit
Assert.Equal(0x80000000u, registers.PC);
Assert.True(registers.InBranchDelaySlot);
// Commit branch
registers.CommitBranch();
// PC should now be at branch target
Assert.Equal(0x80001000u, registers.PC);
Assert.False(registers.InBranchDelaySlot);
}
[Fact]
public void BranchDelay_BranchNotTaken_PCUnchanged()
{
var registers = new Registers();
registers.PC = 0x80000000;
// Set branch with taken=false
registers.SetBranch(0x80001000, false);
// Commit branch
registers.CommitBranch();
// PC should remain unchanged
Assert.Equal(0x80000000u, registers.PC);
}
[Fact]
public void RegisterNames_CorrectForAllRegisters()
{
Assert.Equal("$zero", Registers.GetRegisterName(0));
Assert.Equal("$at", Registers.GetRegisterName(1));
Assert.Equal("$v0", Registers.GetRegisterName(2));
Assert.Equal("$a0", Registers.GetRegisterName(4));
Assert.Equal("$t0", Registers.GetRegisterName(8));
Assert.Equal("$s0", Registers.GetRegisterName(16));
Assert.Equal("$gp", Registers.GetRegisterName(28));
Assert.Equal("$sp", Registers.GetRegisterName(29));
Assert.Equal("$fp", Registers.GetRegisterName(30));
Assert.Equal("$ra", Registers.GetRegisterName(31));
}
}
+314
View File
@@ -0,0 +1,314 @@
using Xunit;
using Yaroze.Core;
using Yaroze.Core.DMA;
namespace Yaroze.Tests.DMA;
public class DmaTests
{
private readonly Emulator _emu;
private readonly DmaController _dma;
public DmaTests()
{
_emu = new Emulator();
_dma = _emu.Dma;
}
[Fact]
public void Dma_Reset_ClearsChannels()
{
// Arrange
_emu.Bus.Write32(0x1F8010A0, 0x12345678); // Channel 2 MADR
// Act
_dma.Reset();
// Assert
Assert.Equal(0u, _dma.GetChannel(2).MADR);
}
[Fact]
public void Dma_DPCR_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F8010F0, 0x12345678);
uint result = _emu.Bus.Read32(0x1F8010F0);
// Assert
Assert.Equal(0x12345678u, result);
}
[Fact]
public void Dma_DICR_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F8010F4, 0x00FF00FF);
uint result = _emu.Bus.Read32(0x1F8010F4);
// Assert - Check that written value is retained
Assert.Equal(0x00FF00FFu, result & 0x00FFFFFF);
}
[Fact]
public void DmaChannel_MADR_MaskedTo24Bits()
{
// Act
_emu.Bus.Write32(0x1F8010A0, 0xFFFFFFFF); // Channel 2 MADR
// Assert - Should be masked to 24 bits
Assert.Equal(0x00FFFFFFu, _dma.GetChannel(2).MADR);
}
[Fact]
public void DmaChannel_BCR_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F8010A4, 0x12345678); // Channel 2 BCR
uint result = _emu.Bus.Read32(0x1F8010A4);
// Assert
Assert.Equal(0x12345678u, result);
}
[Fact]
public void DmaChannel_CHCR_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F8010A8, 0x01000201); // Channel 2 CHCR
uint result = _emu.Bus.Read32(0x1F8010A8);
// Assert
Assert.Equal(0x01000201u, result);
}
[Fact]
public void DmaChannel_IsActive_ReflectsBusyBit()
{
// Arrange - Set CHCR with start/busy bit
_emu.Bus.Write32(0x1F8010A8, 0x01000000); // Channel 2 CHCR with busy bit
// Act
bool isActive = _dma.GetChannel(2).IsActive;
// Assert
Assert.True(isActive);
}
[Fact]
public void DmaChannel_SyncMode_ExtractsCorrectly()
{
// Arrange - Set sync mode to 2 (linked list) in bits 9-10
_emu.Bus.Write32(0x1F8010A8, 0x00000400); // Sync mode 2
// Act
int syncMode = _dma.GetChannel(2).SyncMode;
// Assert
Assert.Equal(2, syncMode);
}
[Fact]
public void DmaChannel_FromRam_ExtractsCorrectly()
{
// Arrange - Set direction bit
_emu.Bus.Write32(0x1F8010A8, 0x00000001); // From RAM
// Act
bool fromRam = _dma.GetChannel(2).FromRam;
// Assert
Assert.True(fromRam);
}
[Fact]
public void Dma_AllChannelsAccessible()
{
// Test that all 7 channels have distinct addresses
for (int i = 0; i < 7; i++)
{
uint baseAddr = (uint)(0x1F801080 + i * 0x10);
// Write unique value to each channel's MADR
_emu.Bus.Write32(baseAddr, (uint)(0x100 * (i + 1)));
}
// Verify
for (int i = 0; i < 7; i++)
{
Assert.Equal((uint)(0x100 * (i + 1)), _dma.GetChannel(i).MADR);
}
}
[Fact]
public void Dma_BurstTransfer_TransfersData()
{
// Arrange - Set up a burst transfer from RAM to GPU
// Channel 2 (GPU), sync mode 0 (burst), from RAM
// Write test data to RAM
for (int i = 0; i < 10; i++)
{
_emu.Bus.Ram.Write32((uint)(0x1000 + i * 4), (uint)(0xA0 + i));
}
// Configure DMA channel 2
_emu.Bus.Write32(0x1F8010A0, 0x00001000); // MADR = 0x1000
_emu.Bus.Write32(0x1F8010A4, 0x0000000A); // BCR = 10 words
_emu.Bus.Write32(0x1F8010F0, 0x00000800); // DPCR = Enable channel 2 (bit 11)
// Act - Trigger transfer with CHCR (sync mode 0, from RAM, start)
_emu.Bus.Write32(0x1F8010A8, 0x01000001);
// Assert - Channel should no longer be busy
Assert.False(_dma.GetChannel(2).IsActive);
}
[Fact]
public void Dma_BurstTransfer_UpdatesMADR()
{
// Arrange
_emu.Bus.Ram.Write32(0x1000, 0x12345678);
_emu.Bus.Write32(0x1F8010A0, 0x00001000); // MADR = 0x1000
_emu.Bus.Write32(0x1F8010A4, 0x00000005); // BCR = 5 words
_emu.Bus.Write32(0x1F8010F0, 0x00000800); // DPCR = Enable channel 2
// Act
_emu.Bus.Write32(0x1F8010A8, 0x01000001); // Start transfer
// Assert - MADR should have advanced by 5*4 = 20 bytes
Assert.Equal(0x00001014u, _dma.GetChannel(2).MADR);
}
[Fact]
public void Dma_BurstTransfer_BackwardDirection()
{
// Arrange - Transfer with backward step (bit 1 set)
_emu.Bus.Ram.Write32(0x1000, 0x12345678);
_emu.Bus.Write32(0x1F8010A0, 0x00001000); // MADR = 0x1000
_emu.Bus.Write32(0x1F8010A4, 0x00000005); // BCR = 5 words
_emu.Bus.Write32(0x1F8010F0, 0x00000800); // DPCR = Enable channel 2
// Act - Trigger with backward step (bit 1)
_emu.Bus.Write32(0x1F8010A8, 0x01000003); // From RAM + backward
// Assert - MADR should have decremented by 5*4 = 20 bytes
Assert.Equal(0x00000FECu, _dma.GetChannel(2).MADR);
}
[Fact]
public void Dma_SliceTransfer_MultipleBlocks()
{
// Arrange - Sync mode 1 (slice): block size in BCR[15:0], count in BCR[31:16]
_emu.Bus.Ram.Write32(0x1000, 0x12345678);
_emu.Bus.Write32(0x1F8010A0, 0x00001000); // MADR = 0x1000
_emu.Bus.Write32(0x1F8010A4, 0x00030004); // BCR = 3 blocks of 4 words
_emu.Bus.Write32(0x1F8010F0, 0x00000800); // DPCR = Enable channel 2
// Act - Trigger with sync mode 1 (bits 9-10 = 01)
_emu.Bus.Write32(0x1F8010A8, 0x01000201); // Sync mode 1, from RAM
// Assert - MADR should have advanced by 3*4*4 = 48 bytes
Assert.Equal(0x00001030u, _dma.GetChannel(2).MADR);
}
[Fact]
public void Dma_InterruptFlag_SetOnTransfer()
{
// Arrange - Enable interrupt for channel 2 (bit 18)
_emu.Bus.Write32(0x1F8010F4, 0x00040000); // DICR = Enable IRQ for channel 2
_emu.Bus.Write32(0x1F8010F0, 0x00000800); // DPCR = Enable channel 2
_emu.Bus.Write32(0x1F8010A0, 0x00001000); // MADR
_emu.Bus.Write32(0x1F8010A4, 0x00000001); // BCR = 1 word
// Act - Trigger transfer
_emu.Bus.Write32(0x1F8010A8, 0x01000001);
// Assert - Interrupt flag for channel 2 (bit 26) should be set
uint dicr = _emu.Bus.Read32(0x1F8010F4);
Assert.NotEqual(0u, dicr & (1u << 26));
}
[Fact]
public void Dma_MasterFlag_SetWhenEnabled()
{
// Arrange - Enable master IRQ (bit 23) and channel 2 IRQ (bit 18)
_emu.Bus.Write32(0x1F8010F4, 0x00840000); // Master enable + Channel 2 enable
_emu.Bus.Write32(0x1F8010F0, 0x00000800); // DPCR = Enable channel 2
_emu.Bus.Write32(0x1F8010A0, 0x00001000); // MADR
_emu.Bus.Write32(0x1F8010A4, 0x00000001); // BCR = 1 word
// Act - Trigger transfer
_emu.Bus.Write32(0x1F8010A8, 0x01000001);
// Assert - Master flag (bit 31) should be set
uint dicr = _emu.Bus.Read32(0x1F8010F4);
Assert.NotEqual(0u, dicr & (1u << 31));
}
[Fact]
public void Dma_HasPendingInterrupt_ReflectsMasterFlag()
{
// Arrange
_emu.Bus.Write32(0x1F8010F4, 0x00840000); // Enable master + channel 2
_emu.Bus.Write32(0x1F8010F0, 0x00000800); // Enable channel 2
_emu.Bus.Write32(0x1F8010A0, 0x00001000);
_emu.Bus.Write32(0x1F8010A4, 0x00000001);
// Act
_emu.Bus.Write32(0x1F8010A8, 0x01000001); // Trigger
// Assert
Assert.True(_dma.HasPendingInterrupt());
}
[Fact]
public void Dma_ClearInterruptFlag_WritingOne()
{
// Arrange - Set interrupt flag
_emu.Bus.Write32(0x1F8010F4, 0x00840000);
_emu.Bus.Write32(0x1F8010F0, 0x00000800);
_emu.Bus.Write32(0x1F8010A0, 0x00001000);
_emu.Bus.Write32(0x1F8010A4, 0x00000001);
_emu.Bus.Write32(0x1F8010A8, 0x01000001); // Trigger
// Verify flag is set
uint dicr = _emu.Bus.Read32(0x1F8010F4);
Assert.NotEqual(0u, dicr & (1u << 26));
// Act - Clear by writing 1 to the flag bit
_emu.Bus.Write32(0x1F8010F4, (1u << 26));
// Assert - Flag should be cleared
dicr = _emu.Bus.Read32(0x1F8010F4);
Assert.Equal(0u, dicr & (1u << 26));
}
[Fact]
public void Dma_DisabledChannel_NoTransfer()
{
// Arrange - Channel 2 disabled in DPCR (bit 11 = 0)
_emu.Bus.Write32(0x1F8010F0, 0x00000000); // DPCR = All disabled
_emu.Bus.Write32(0x1F8010A0, 0x00001000); // MADR
_emu.Bus.Write32(0x1F8010A4, 0x00000010); // BCR = 16 words
// Act - Try to trigger transfer
_emu.Bus.Write32(0x1F8010A8, 0x01000001);
// Assert - MADR should not have changed (transfer didn't execute)
Assert.Equal(0x00001000u, _dma.GetChannel(2).MADR);
}
[Fact]
public void Dma_Channel6_OTC_IsAccessible()
{
// Channel 6 (OTC - Ordering Table Clear) is special
// Act
_emu.Bus.Write32(0x1F8010E0, 0x00002000); // MADR
_emu.Bus.Write32(0x1F8010E4, 0x00000010); // BCR
// Assert
Assert.Equal(0x00002000u, _dma.GetChannel(6).MADR);
Assert.Equal(0x00000010u, _dma.GetChannel(6).BCR);
}
}
@@ -0,0 +1,370 @@
using Xunit;
using Yaroze.Core.Disassembly;
namespace Yaroze.Tests.Disassembly;
public class DisassemblerTests
{
[Fact]
public void Disassemble_NOP_ReturnsNop()
{
// Arrange
uint instruction = 0x00000000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("nop", result);
}
[Fact]
public void Disassemble_ADDIU_ReturnsCorrectFormat()
{
// Arrange - ADDIU $t0, $zero, 42
uint instruction = 0x2408002A; // ADDIU $8, $0, 42
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("addiu $t0, $zero, 42", result);
}
[Fact]
public void Disassemble_LW_ReturnsCorrectFormat()
{
// Arrange - LW $t1, 0x1000($zero)
uint instruction = 0x8C091000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("lw $t1, 4096($zero)", result);
}
[Fact]
public void Disassemble_SW_ReturnsCorrectFormat()
{
// Arrange - SW $t1, 0x2000($zero)
uint instruction = 0xAC092000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("sw $t1, 8192($zero)", result);
}
[Fact]
public void Disassemble_BEQ_CalculatesBranchTarget()
{
// Arrange - BEQ $t0, $zero, +8 (skip 2 instructions)
uint pc = 0x80000000;
uint instruction = 0x11000002; // BEQ $8, $0, +2
// Act
string result = MipsDisassembler.Disassemble(pc, instruction);
// Assert - Branch target should be PC+4+8 = 0x8000000C
Assert.Contains("0x8000000C", result);
}
[Fact]
public void Disassemble_JAL_CalculatesJumpTarget()
{
// Arrange - JAL 0x80001000
uint pc = 0x80000000;
uint instruction = 0x0C000400; // JAL with target bits
// Act
string result = MipsDisassembler.Disassemble(pc, instruction);
// Assert
Assert.Contains("jal", result);
Assert.Contains("0x80001000", result);
}
[Fact]
public void Disassemble_JR_RA_ReturnsCorrectFormat()
{
// Arrange - JR $ra
uint instruction = 0x03E00008;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("jr $ra", result);
}
[Fact]
public void Disassemble_ADDU_ReturnsCorrectFormat()
{
// Arrange - ADDU $t0, $t1, $t2
uint instruction = 0x012A4021; // ADDU $8, $9, $10
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("addu $t0, $t1, $t2", result);
}
[Fact]
public void Disassemble_SLL_ReturnsCorrectFormat()
{
// Arrange - SLL $t0, $t1, 2
uint instruction = 0x00094080; // SLL $8, $9, 2
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("sll $t0, $t1, 2", result);
}
[Fact]
public void Disassemble_LUI_ReturnsCorrectFormat()
{
// Arrange - LUI $t0, 0x8000
uint instruction = 0x3C088000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("lui $t0, 0x8000", result);
}
[Fact]
public void Disassemble_ORI_ReturnsCorrectFormat()
{
// Arrange - ORI $t0, $t0, 0x1234
uint instruction = 0x35081234;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("ori $t0, $t0, 0x1234", result);
}
[Fact]
public void Disassemble_SYSCALL_ReturnsSyscall()
{
// Arrange
uint instruction = 0x0000000C;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("syscall", result);
}
[Fact]
public void Disassemble_BREAK_ReturnsBreak()
{
// Arrange
uint instruction = 0x0000000D;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("break", result);
}
[Fact]
public void Disassemble_MFC0_ReturnsCorrectFormat()
{
// Arrange - MFC0 $t0, $12 (Status register)
uint instruction = 0x40086000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("mfc0 $t0, $12", result);
}
[Fact]
public void Disassemble_MTC0_ReturnsCorrectFormat()
{
// Arrange - MTC0 $t0, $12
uint instruction = 0x40886000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("mtc0 $t0, $12", result);
}
[Fact]
public void Disassemble_RFE_ReturnsRfe()
{
// Arrange
uint instruction = 0x42000010;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("rfe", result);
}
[Fact]
public void Disassemble_MFC2_ReturnsCorrectFormat()
{
// Arrange - MFC2 $t0, $0
uint instruction = 0x48080000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("mfc2 $t0, $0", result);
}
[Fact]
public void Disassemble_MTC2_ReturnsCorrectFormat()
{
// Arrange - MTC2 $t0, $0
uint instruction = 0x48880000;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Equal("mtc2 $t0, $0", result);
}
[Fact]
public void Disassemble_COP2Command_ReturnsCorrectFormat()
{
// Arrange - GTE command
uint instruction = 0x4A180001; // RTPS
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Contains("cop2", result);
}
[Fact]
public void Disassemble_UnknownInstruction_ReturnsWord()
{
// Arrange - Invalid/unknown instruction
uint instruction = 0xFFFFFFFF;
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.StartsWith(".word", result);
}
[Fact]
public void DisassembleBlock_ReturnsMultipleInstructions()
{
// Arrange
byte[] data = new byte[16];
BitConverter.GetBytes(0x00000000u).CopyTo(data, 0); // NOP
BitConverter.GetBytes(0x2408002Au).CopyTo(data, 4); // ADDIU $t0, $zero, 42
BitConverter.GetBytes(0x8C091000u).CopyTo(data, 8); // LW $t1, 0x1000($zero)
BitConverter.GetBytes(0xAC092000u).CopyTo(data, 12); // SW $t1, 0x2000($zero)
// Act
var result = MipsDisassembler.DisassembleBlock(0x80000000, data, 4);
// Assert
Assert.Equal(4, result.Count);
Assert.Equal(0x80000000u, result[0].Address);
Assert.Equal("nop", result[0].Disassembly);
Assert.Equal(0x80000004u, result[1].Address);
Assert.Contains("addiu", result[1].Disassembly);
}
[Fact]
public void DisassembledInstruction_ToString_FormatsCorrectly()
{
// Arrange
var instr = new DisassembledInstruction
{
Address = 0x80000000,
InstructionWord = 0x00000000,
Disassembly = "nop"
};
// Act
string result = instr.ToString();
// Assert
Assert.Contains("0x80000000", result);
Assert.Contains("00000000", result);
Assert.Contains("nop", result);
}
[Fact]
public void DisassembledInstruction_WithLabel_FormatsWithLabel()
{
// Arrange
var instr = new DisassembledInstruction
{
Address = 0x80000000,
InstructionWord = 0x00000000,
Disassembly = "nop",
Label = "main"
};
// Act
string result = instr.ToString();
// Assert
Assert.Contains("main:", result);
}
[Fact]
public void DisassembledInstruction_WithComment_FormatsWithComment()
{
// Arrange
var instr = new DisassembledInstruction
{
Address = 0x80000000,
InstructionWord = 0x00000000,
Disassembly = "nop",
Comment = "No operation"
};
// Act
string result = instr.ToString();
// Assert
Assert.Contains("; No operation", result);
}
[Theory]
[InlineData(0, "$zero")]
[InlineData(1, "$at")]
[InlineData(2, "$v0")]
[InlineData(8, "$t0")]
[InlineData(16, "$s0")]
[InlineData(28, "$gp")]
[InlineData(29, "$sp")]
[InlineData(31, "$ra")]
public void Disassemble_UsesCorrectRegisterNames(uint regNum, string expectedName)
{
// Arrange - ADDU $reg, $zero, $zero
uint instruction = 0x00000021 | (regNum << 11);
// Act
string result = MipsDisassembler.Disassemble(0, instruction);
// Assert
Assert.Contains(expectedName, result);
}
}
@@ -0,0 +1,382 @@
using Xunit;
using Yaroze.Core.Analysis;
using Yaroze.Core.Disassembly;
namespace Yaroze.Tests.Disassembly;
public class PseudoCDecompilerTests
{
[Fact]
public void DecompileFunction_SimpleReturn_ProducesValidC()
{
// Arrange - Simple function with just a return
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// jr $ra
// nop
WriteInstruction(memory, 0, 0x03E00008); // JR $ra
WriteInstruction(memory, 4, 0x00000000); // NOP
var func = new Function
{
Address = baseAddress,
Name = "test_func"
};
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("void test_func()", result);
Assert.Contains("return;", result);
Assert.Contains("// nop", result);
}
[Fact]
public void DecompileFunction_ArithmeticOperations_ProducesAssignments()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// addiu $v0, $zero, 42
// addu $v1, $v0, $a0
// jr $ra
// nop
WriteInstruction(memory, 0, 0x2402002A); // ADDIU $v0, $zero, 42
WriteInstruction(memory, 4, 0x00441821); // ADDU $v1, $v0, $a0
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "arithmetic" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("v0 = 42;", result);
Assert.Contains("v1 = v0 + a0;", result);
}
[Fact]
public void DecompileFunction_LoadStore_ProducesPointerOperations()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// lw $v0, 0($sp)
// sw $v0, 4($sp)
// jr $ra
// nop
WriteInstruction(memory, 0, 0x8FA20000); // LW $v0, 0($sp)
WriteInstruction(memory, 4, 0xAFA20004); // SW $v0, 4($sp)
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "load_store" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("v0 = *(int*)(sp + 0);", result);
Assert.Contains("*(int*)(sp + 4) = v0;", result);
}
[Fact]
public void DecompileFunction_ConditionalBranch_ProducesGoto()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// beq $t0, $zero, skip
// nop
// addiu $v0, $zero, 1
// skip:
// jr $ra
// nop
WriteInstruction(memory, 0, 0x11000002); // BEQ $t0, $zero, +2
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "branch_test" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
func.Instructions.Add(baseAddress + 16);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("if (t0 == zero) goto", result);
Assert.Contains("8000000C", result); // Branch target
}
[Fact]
public void DecompileFunction_FunctionCall_ProducesCall()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// jal sub_func
// nop
// jr $ra
// nop
WriteInstruction(memory, 0, 0x0C000010); // JAL 0x80000040
WriteInstruction(memory, 4, 0x00000000); // NOP
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "caller" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
var symbolManager = new SymbolManager();
symbolManager.AddSymbol(0x80000040, "sub_func", SymbolType.Function);
var decompiler = new PseudoCDecompiler(memory, baseAddress, symbolManager);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("sub_func();", result);
}
[Fact]
public void DecompileFunction_LogicalOperations_ProducesCorrectOperators()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// and $v0, $t0, $t1
// or $v1, $t2, $t3
// xor $a0, $t4, $t5
// jr $ra
// nop
WriteInstruction(memory, 0, 0x01091024); // AND $v0, $t0, $t1
WriteInstruction(memory, 4, 0x014B1825); // OR $v1, $t2, $t3
WriteInstruction(memory, 8, 0x018D2026); // XOR $a0, $t4, $t5
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "logical" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
func.Instructions.Add(baseAddress + 16);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("v0 = t0 & t1;", result);
Assert.Contains("v1 = t2 | t3;", result);
Assert.Contains("a0 = t4 ^ t5;", result);
}
[Fact]
public void DecompileFunction_ShiftOperations_ProducesShifts()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// sll $v0, $t0, 2
// srl $v1, $t1, 3
// jr $ra
// nop
WriteInstruction(memory, 0, 0x00081080); // SLL $v0, $t0, 2
WriteInstruction(memory, 4, 0x000918C2); // SRL $v1, $t1, 3
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "shifts" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("v0 = t0 << 2;", result);
Assert.Contains("v1 = t1 >> 3;", result);
}
[Fact]
public void DecompileFunction_WithComments_IncludesComments()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
WriteInstruction(memory, 0, 0x2402002A); // ADDIU $v0, $zero, 42
WriteInstruction(memory, 4, 0x03E00008); // JR $ra
WriteInstruction(memory, 8, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "commented" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
var symbolManager = new SymbolManager();
symbolManager.AddComment(baseAddress, "Initialize result");
symbolManager.AddComment(baseAddress + 4, "Return to caller");
var decompiler = new PseudoCDecompiler(memory, baseAddress, symbolManager);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("// Initialize result", result);
Assert.Contains("// Return to caller", result);
}
[Fact]
public void DecompileFunction_Comparisons_ProducesTernary()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// slt $v0, $t0, $t1
// sltu $v1, $t2, $t3
// jr $ra
// nop
WriteInstruction(memory, 0, 0x0109102A); // SLT $v0, $t0, $t1
WriteInstruction(memory, 4, 0x014B182B); // SLTU $v1, $t2, $t3
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "compare" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("v0 = (t0 < t1) ? 1 : 0;", result);
Assert.Contains("v1 = ((uint)t2 < (uint)t3) ? 1 : 0;", result);
}
[Fact]
public void DecompileFunction_LoadUpperImmediate_ProducesShift()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// lui $v0, 0x8000
// jr $ra
// nop
WriteInstruction(memory, 0, 0x3C028000); // LUI $v0, 0x8000
WriteInstruction(memory, 4, 0x03E00008); // JR $ra
WriteInstruction(memory, 8, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "load_upper" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("v0 = 0x8000 << 16;", result);
}
[Fact]
public void DecompileFunction_ByteLoadStore_ProducesCorrectTypes()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// func:
// lb $v0, 0($sp)
// lbu $v1, 1($sp)
// sb $a0, 2($sp)
// jr $ra
// nop
WriteInstruction(memory, 0, 0x83A20000); // LB $v0, 0($sp)
WriteInstruction(memory, 4, 0x93A30001); // LBU $v1, 1($sp)
WriteInstruction(memory, 8, 0xA3A40002); // SB $a0, 2($sp)
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
var func = new Function { Address = baseAddress, Name = "byte_ops" };
func.Instructions.Add(baseAddress);
func.Instructions.Add(baseAddress + 4);
func.Instructions.Add(baseAddress + 8);
func.Instructions.Add(baseAddress + 12);
func.Instructions.Add(baseAddress + 16);
var decompiler = new PseudoCDecompiler(memory, baseAddress);
// Act
string result = decompiler.DecompileFunction(func);
// Assert
Assert.Contains("*(sbyte*)", result);
Assert.Contains("*(byte*)", result);
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
+312
View File
@@ -0,0 +1,312 @@
using Xunit;
using Yaroze.Core;
using Yaroze.Core.GPU;
namespace Yaroze.Tests.GPU;
public class GpuTests
{
private readonly Emulator _emu;
private readonly Gpu _gpu;
public GpuTests()
{
_emu = new Emulator();
_gpu = _emu.Gpu;
}
[Fact]
public void Gpu_Reset_ClearsState()
{
// Arrange
_gpu.WriteVram(100, 100, 0x1234);
_emu.Bus.Write32(0x1F801814, 0x03000001); // GP1: Display disable
// Act
_gpu.Reset();
// Assert
Assert.Equal(0, _gpu.ReadVram(100, 100));
Assert.True(_gpu.State.DisplayEnabled);
}
[Fact]
public void Gpu_VramReadWrite_WorksCorrectly()
{
// Arrange
ushort color = 0x7FFF; // White (15-bit RGB)
// Act
_gpu.WriteVram(512, 256, color);
ushort result = _gpu.ReadVram(512, 256);
// Assert
Assert.Equal(color, result);
}
[Fact]
public void Gpu_VramOutOfBounds_ReturnsZero()
{
// Act & Assert
Assert.Equal(0, _gpu.ReadVram(2000, 2000)); // Out of bounds
Assert.Equal(0, _gpu.ReadVram(-1, -1)); // Negative
}
[Fact]
public void Gpu_VramOutOfBounds_WriteIgnored()
{
// Act - should not crash
_gpu.WriteVram(2000, 2000, 0xFFFF);
_gpu.WriteVram(-1, -1, 0xFFFF);
}
[Fact]
public void GpuStat_InitialState_IsReady()
{
// Act
uint gpustat = _emu.Bus.Read32(0x1F801814);
// Assert
Assert.NotEqual(0u, gpustat & (1u << 26)); // Ready to receive command
Assert.NotEqual(0u, gpustat & (1u << 28)); // Ready to receive DMA block
}
[Fact]
public void Gp1_ResetGpu_ResetsState()
{
// Arrange
_gpu.WriteVram(10, 10, 0xABCD);
// Act
_emu.Bus.Write32(0x1F801814, 0x00000000); // GP1: Reset GPU
// Assert
Assert.Equal(0, _gpu.ReadVram(10, 10));
}
[Fact]
public void Gp1_DisplayEnable_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801814, 0x03000001); // GP1: Display disable
Assert.False(_gpu.State.DisplayEnabled);
_emu.Bus.Write32(0x1F801814, 0x03000000); // GP1: Display enable
Assert.True(_gpu.State.DisplayEnabled);
}
[Fact]
public void Gp1_DmaDirection_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801814, 0x04000002); // GP1: DMA direction = CPU→GP0
// Assert
Assert.Equal(2, _gpu.State.DmaDirection);
}
[Fact]
public void Gp1_DisplayAreaStart_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801814, 0x05000000 | (100) | (50 << 10)); // X=100, Y=50
// Assert
Assert.Equal(100, _gpu.State.DisplayAreaX);
Assert.Equal(50, _gpu.State.DisplayAreaY);
}
[Fact]
public void Gp1_HorizontalRange_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801814, 0x06000000 | (0x260) | (0xC60 << 12));
// Assert
Assert.Equal(0x260, _gpu.State.HorizontalStart);
Assert.Equal(0xC60, _gpu.State.HorizontalEnd);
}
[Fact]
public void Gp1_VerticalRange_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801814, 0x07000000 | (0x10) | (0x100 << 10));
// Assert
Assert.Equal(0x10, _gpu.State.VerticalStart);
Assert.Equal(0x100, _gpu.State.VerticalEnd);
}
[Fact]
public void Gp1_DisplayMode_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801814, 0x08000005); // Some display mode
// Assert
Assert.Equal(5, _gpu.State.VideoMode);
}
[Fact]
public void Gp0_Nop_DoesNothing()
{
// Act - should not crash
_emu.Bus.Write32(0x1F801810, 0x00000000); // GP0: NOP
}
[Fact]
public void Gp0_ClearCache_DoesNotCrash()
{
// Act - should not crash
_emu.Bus.Write32(0x1F801810, 0x01000000); // GP0: Clear cache
}
[Fact]
public void Gp0_FillRectangle_FillsVram()
{
// Arrange - Fill 10x10 rectangle at (50,50) with color 0x1234
_emu.Bus.Write32(0x1F801810, 0x02001234); // GP0: Fill rect, color=0x1234
_emu.Bus.Write32(0x1F801810, (50) | (50 << 16)); // Position
_emu.Bus.Write32(0x1F801810, (10) | (10 << 16)); // Size
// Act & Assert
Assert.Equal((ushort)0x1234, _gpu.ReadVram(50, 50));
Assert.Equal((ushort)0x1234, _gpu.ReadVram(55, 55));
Assert.Equal((ushort)0x1234, _gpu.ReadVram(59, 59));
Assert.Equal(0, _gpu.ReadVram(60, 60)); // Outside rectangle
}
[Fact]
public void Gp0_DrawMode_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801810, 0xE1000ABC); // GP0: Draw mode
// Assert
Assert.Equal(0x000ABCu, _gpu.State.DrawMode);
}
[Fact]
public void Gp0_TextureWindow_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801810, 0xE2000000 | (5) | (10 << 5) | (15 << 10) | (20 << 15));
// Assert
Assert.Equal(5, _gpu.State.TextureWindowMaskX);
Assert.Equal(10, _gpu.State.TextureWindowMaskY);
Assert.Equal(15, _gpu.State.TextureWindowOffsetX);
Assert.Equal(20, _gpu.State.TextureWindowOffsetY);
}
[Fact]
public void Gp0_DrawAreaStart_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801810, 0xE3000000 | (100) | (50 << 10));
// Assert
Assert.Equal(100, _gpu.State.DrawAreaLeft);
Assert.Equal(50, _gpu.State.DrawAreaTop);
}
[Fact]
public void Gp0_DrawAreaEnd_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801810, 0xE4000000 | (640) | (480 << 10));
// Assert
Assert.Equal(640, _gpu.State.DrawAreaRight);
Assert.Equal(480, _gpu.State.DrawAreaBottom);
}
[Fact]
public void Gp0_DrawOffset_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801810, 0xE5000000 | (100) | (50 << 11));
// Assert
Assert.Equal(100, _gpu.State.DrawOffsetX);
Assert.Equal(50, _gpu.State.DrawOffsetY);
}
[Fact]
public void Gp0_DrawOffset_SignExtends()
{
// Act - Use negative offsets (sign-extended 11-bit values)
_emu.Bus.Write32(0x1F801810, 0xE5000000 | (0x7FF) | (0x7FF << 11)); // -1, -1
// Assert - should be sign-extended to negative values
Assert.True(_gpu.State.DrawOffsetX < 0);
Assert.True(_gpu.State.DrawOffsetY < 0);
}
[Fact]
public void Gp0_MaskBit_UpdatesState()
{
// Act
_emu.Bus.Write32(0x1F801810, 0xE6000003); // Both flags set
// Assert
Assert.True(_gpu.State.MaskWhileDrawing);
Assert.True(_gpu.State.CheckMaskBeforeDraw);
}
[Fact]
public void Gp1_GetGpuInfo_ReturnsInfo()
{
// Arrange - Set some state
_emu.Bus.Write32(0x1F801810, 0xE3000000 | (100) | (50 << 10)); // Draw area start
// Act
_emu.Bus.Write32(0x1F801814, 0x13000000); // GP1: Get GPU info (draw area start)
uint info = _emu.Bus.Read32(0x1F801810); // Read GPUREAD
// Assert
Assert.Equal((uint)((100) | (50 << 10)), info);
}
[Fact]
public void Gpu_IntegrationTest_BasicDrawingEnvironmentSetup()
{
// This simulates a typical PS1 game initialization sequence
// Reset GPU
_emu.Bus.Write32(0x1F801814, 0x00000000); // GP1: Reset
// Set display mode (320x240, NTSC)
_emu.Bus.Write32(0x1F801814, 0x08000001);
// Set display area
_emu.Bus.Write32(0x1F801814, 0x05000000); // Display area start (0,0)
// Set horizontal range
_emu.Bus.Write32(0x1F801814, 0x06000000 | (0x260) | (0xC60 << 12));
// Set vertical range
_emu.Bus.Write32(0x1F801814, 0x07000000 | (0x10) | (0x100 << 10));
// Enable display
_emu.Bus.Write32(0x1F801814, 0x03000000);
// Set DMA direction
_emu.Bus.Write32(0x1F801814, 0x04000002); // CPU→GP0
// Set drawing environment
_emu.Bus.Write32(0x1F801810, 0xE1000000); // Draw mode
_emu.Bus.Write32(0x1F801810, 0xE3000000); // Draw area start (0,0)
_emu.Bus.Write32(0x1F801810, 0xE4000000 | (319) | (239 << 10)); // Draw area end
_emu.Bus.Write32(0x1F801810, 0xE5000000); // Draw offset (0,0)
// Assert - GPU should be ready and configured
uint gpustat = _emu.Bus.Read32(0x1F801814);
Assert.NotEqual(0u, gpustat & (1u << 26)); // Ready
Assert.True(_gpu.State.DisplayEnabled);
Assert.Equal(2, _gpu.State.DmaDirection);
Assert.Equal(319, _gpu.State.DrawAreaRight);
Assert.Equal(239, _gpu.State.DrawAreaBottom);
}
}
+1
View File
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,332 @@
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)
}
}
@@ -0,0 +1,175 @@
using Xunit;
using Yaroze.Core;
using Yaroze.Core.Interrupts;
namespace Yaroze.Tests.Interrupts;
public class InterruptTests
{
private readonly Emulator _emu;
public InterruptTests()
{
_emu = new Emulator();
}
[Fact]
public void Interrupts_Reset_ClearsState()
{
// Arrange
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer0);
// Act
_emu.Interrupts.Reset();
// Assert
Assert.Equal(0, _emu.Interrupts.ISTAT);
}
[Fact]
public void Interrupts_ISTAT_ReadWrite()
{
// Act - Write to clear bits
_emu.Bus.Write32(0x1F801070, 0xFFFF);
uint result = _emu.Bus.Read32(0x1F801070);
// Assert - All bits should be cleared
Assert.Equal(0u, result);
}
[Fact]
public void Interrupts_IMASK_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F801074, 0x1234);
uint result = _emu.Bus.Read32(0x1F801074);
// Assert
Assert.Equal(0x1234u, result);
}
[Fact]
public void Interrupts_RaiseInterrupt_SetsFlag()
{
// Act
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer0);
// Assert - Bit 4 should be set
Assert.NotEqual(0, _emu.Interrupts.ISTAT & (1 << 4));
}
[Fact]
public void Interrupts_HasPending_ChecksMask()
{
// Arrange - Raise interrupt but don't enable it in mask
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer0);
_emu.Bus.Write32(0x1F801074, 0x0000); // Mask = 0 (all disabled)
// Assert - No pending interrupts because mask is 0
Assert.False(_emu.Interrupts.HasPendingInterrupt());
}
[Fact]
public void Interrupts_HasPending_WhenMasked()
{
// Arrange - Raise and enable Timer0 interrupt
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer0);
_emu.Bus.Write32(0x1F801074, (1 << 4)); // Enable Timer0 in mask
// Assert
Assert.True(_emu.Interrupts.HasPendingInterrupt());
}
[Fact]
public void Interrupts_ClearFlag_ByWritingZero()
{
// Arrange
_emu.Interrupts.RaiseInterrupt(InterruptType.VBlank);
Assert.NotEqual(0, _emu.Interrupts.ISTAT & 0x01);
// Act - Write 0 to clear bit 0
_emu.Bus.Write32(0x1F801070, 0xFFFE); // All 1s except bit 0
// Assert
Assert.Equal(0, _emu.Interrupts.ISTAT & 0x01);
}
[Fact]
public void Interrupts_MultipleFlags_CanBeSet()
{
// Act
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer0);
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer1);
_emu.Interrupts.RaiseInterrupt(InterruptType.Dma);
// Assert
Assert.NotEqual(0, _emu.Interrupts.ISTAT & (1 << 4)); // Timer0
Assert.NotEqual(0, _emu.Interrupts.ISTAT & (1 << 5)); // Timer1
Assert.NotEqual(0, _emu.Interrupts.ISTAT & (1 << 3)); // DMA
}
[Fact]
public void Interrupts_Timer_RaisesInterrupt()
{
// Arrange - Configure timer to trigger interrupt
_emu.Bus.Write32(0x1F801074, (1 << 4)); // Enable Timer0 in interrupt mask
_emu.Bus.Write32(0x1F801108, 10); // Target = 10
_emu.Bus.Write32(0x1F801104, 0x0050); // IRQ on target + repeat
// Act - Tick timer until interrupt
_emu.Timer0.Tick(20);
// Assert - Timer0 interrupt should be raised
Assert.True(_emu.Interrupts.HasPendingInterrupt());
Assert.NotEqual(0, _emu.Interrupts.ISTAT & (1 << 4));
}
[Fact]
public void Interrupts_UpdateCpu_SetsIrqInCop0()
{
// Arrange
_emu.Interrupts.RaiseInterrupt(InterruptType.VBlank);
_emu.Bus.Write32(0x1F801074, 0x0001); // Enable VBlank
// Act - Step emulator (which updates interrupts)
_emu.Bus.Ram.Write32(0, 0x00000000); // NOP
_emu.Step();
// Assert - COP0 should have interrupt pending
// (We can't directly check this without exposing COP0 internals,
// but we can verify HasPendingInterrupt works)
Assert.True(_emu.Interrupts.HasPendingInterrupt());
}
[Fact]
public void Interrupts_AllTypes_CanBeRaised()
{
// Test all interrupt types
for (int i = 0; i <= 10; i++)
{
_emu.Interrupts.Reset();
_emu.Interrupts.RaiseInterrupt((InterruptType)i);
Assert.NotEqual(0, _emu.Interrupts.ISTAT & (1 << i));
}
}
[Fact]
public void Interrupts_GetPending_ReturnsOnlyMasked()
{
// Arrange - Raise multiple interrupts
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer0);
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer1);
_emu.Interrupts.RaiseInterrupt(InterruptType.Timer2);
// Enable only Timer0 and Timer2
_emu.Bus.Write32(0x1F801074, (1 << 4) | (1 << 6));
// Act
ushort pending = _emu.Interrupts.GetPendingInterrupts();
// Assert - Only Timer0 and Timer2 should be in pending
Assert.NotEqual(0, pending & (1 << 4)); // Timer0
Assert.Equal(0, pending & (1 << 5)); // Timer1 (not in mask)
Assert.NotEqual(0, pending & (1 << 6)); // Timer2
}
}
@@ -0,0 +1,355 @@
using Xunit;
using Yaroze.Core.JIT;
namespace Yaroze.Tests.JIT;
public class BasicBlockScannerTests
{
[Fact]
public void IdentifyBlock_SimpleSequence_CreatesBlock()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $v0, $zero, 1
// addiu $v1, $zero, 2
// addiu $a0, $zero, 3
// jr $ra
// nop
WriteInstruction(memory, 0, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 4, 0x24030002); // ADDIU $v1, $zero, 2
WriteInstruction(memory, 8, 0x24040003); // ADDIU $a0, $zero, 3
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.Equal(baseAddress, block.StartAddress);
Assert.Equal(5, block.Instructions.Count); // 3 arithmetic + JR + NOP (delay slot)
Assert.Equal(BlockExitType.Jump, block.ExitType);
}
[Fact]
public void IdentifyBlock_ConditionalBranch_EndsBlock()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// beq $t0, $zero, target
// nop
WriteInstruction(memory, 0, 0x11000001); // BEQ $t0, $zero, +1
WriteInstruction(memory, 4, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.Equal(BlockExitType.ConditionalBranch, block.ExitType);
Assert.True(block.BranchTarget.HasValue);
Assert.Equal(2, block.Instructions.Count); // BEQ + NOP (delay slot)
}
[Fact]
public void IdentifyBlock_UnconditionalJump_IncludesDelaySlot()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// j target
// addiu $v0, $zero, 1 (delay slot)
WriteInstruction(memory, 0, 0x08000004); // J 0x80000010
WriteInstruction(memory, 4, 0x24020001); // ADDIU $v0, $zero, 1
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.Equal(2, block.Instructions.Count); // J + delay slot
Assert.Equal(BlockExitType.Jump, block.ExitType);
Assert.Contains(baseAddress, block.Instructions);
Assert.Contains(baseAddress + 4, block.Instructions);
}
[Fact]
public void IdentifyBlock_SYSCALL_EndsBlock()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $v0, $zero, 1
// syscall
WriteInstruction(memory, 0, 0x24020001); // ADDIU
WriteInstruction(memory, 4, 0x0000000C); // SYSCALL
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.Equal(2, block.Instructions.Count);
Assert.Equal(BlockExitType.Exception, block.ExitType);
}
[Fact]
public void IdentifyBlock_CachesBlocks()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
WriteInstruction(memory, 0, 0x03E00008); // JR $ra
WriteInstruction(memory, 4, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block1 = scanner.IdentifyBlock(baseAddress);
var block2 = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.Same(block1, block2); // Should return same instance
Assert.Single(scanner.Blocks);
}
[Fact]
public void ScanRange_IdentifiesMultipleBlocks()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// Block 1: beq + delay slot
WriteInstruction(memory, 0, 0x11000002); // BEQ $t0, $zero, +2 (target: 0x8000000C)
WriteInstruction(memory, 4, 0x00000000); // NOP
// Block 2: fallthrough
WriteInstruction(memory, 8, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
scanner.ScanRange(baseAddress, baseAddress + 20);
// Assert
Assert.True(scanner.Blocks.Count >= 2); // At least 2 blocks
}
[Fact]
public void MarkBlockStart_AddsToStarts()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
WriteInstruction(memory, 0, 0x24020001); // ADDIU
WriteInstruction(memory, 4, 0x24030002); // ADDIU
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
scanner.MarkBlockStart(baseAddress + 4);
var block = scanner.IdentifyBlock(baseAddress);
// Assert
// Block should end before the marked start
Assert.Equal(BlockExitType.FallThrough, block.ExitType);
Assert.Equal(1, block.Instructions.Count); // Only first instruction
}
[Fact]
public void GetStats_ReturnsCorrectStatistics()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// Small block
WriteInstruction(memory, 0, 0x03E00008); // JR $ra
WriteInstruction(memory, 4, 0x00000000); // NOP
// Larger block
WriteInstruction(memory, 16, 0x24020001); // ADDIU
WriteInstruction(memory, 20, 0x24030002); // ADDIU
WriteInstruction(memory, 24, 0x24040003); // ADDIU
WriteInstruction(memory, 28, 0x03E00008); // JR $ra
WriteInstruction(memory, 32, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
scanner.IdentifyBlock(baseAddress);
scanner.IdentifyBlock(baseAddress + 16);
// Act
var stats = scanner.GetStats();
// Assert
Assert.Equal(2, stats.BlockCount);
Assert.Equal(7, stats.TotalInstructions); // 2 + 5
Assert.Equal(3.5, stats.AverageBlockSize, 0.1);
Assert.Equal(5, stats.MaxBlockSize);
}
[Fact]
public void IdentifyBlock_SafetyLimit_PreventsTooLargeBlocks()
{
// Arrange - Create a very long sequence without branches
byte[] memory = new byte[8192];
uint baseAddress = 0x80000000;
for (int i = 0; i < 200; i++)
{
WriteInstruction(memory, i * 4, 0x24020001); // ADDIU $v0, $zero, 1
}
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.True(block.Instructions.Count <= 100); // Safety limit
Assert.Equal(BlockExitType.FallThrough, block.ExitType);
}
[Fact]
public void IdentifyBlock_JAL_EndsBlock()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// jal function
// nop
WriteInstruction(memory, 0, 0x0C000010); // JAL 0x80000040
WriteInstruction(memory, 4, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.Equal(2, block.Instructions.Count);
Assert.Equal(BlockExitType.Jump, block.ExitType);
}
[Fact]
public void IdentifyBlock_BREAK_EndsBlock()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// break
WriteInstruction(memory, 0, 0x0000000D); // BREAK
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
var block = scanner.IdentifyBlock(baseAddress);
// Assert
Assert.Single(block.Instructions);
Assert.Equal(BlockExitType.Exception, block.ExitType);
}
[Fact]
public void BasicBlock_ToString_FormatsCorrectly()
{
// Arrange
var block = new BasicBlock
{
StartAddress = 0x80000000,
EndAddress = 0x80000010,
ExitType = BlockExitType.Jump
};
block.Instructions.Add(0x80000000);
block.Instructions.Add(0x80000004);
block.Instructions.Add(0x80000008);
// Act
string result = block.ToString();
// Assert
Assert.Contains("0x80000000", result);
Assert.Contains("0x80000010", result);
Assert.Contains("3 instructions", result);
Assert.Contains("Jump", result);
}
[Fact]
public void BlockScanStats_ToString_FormatsCorrectly()
{
// Arrange
var stats = new BlockScanStats
{
BlockCount = 10,
TotalInstructions = 50,
AverageBlockSize = 5.0,
MaxBlockSize = 12
};
// Act
string result = stats.ToString();
// Assert
Assert.Contains("10", result);
Assert.Contains("50", result);
Assert.Contains("5", result);
Assert.Contains("12", result);
}
[Fact]
public void ScanRange_FollowsBranches()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// Block at 0x80000000: conditional branch
WriteInstruction(memory, 0, 0x11000003); // BEQ $t0, $zero, +3 (target: 0x80000010)
WriteInstruction(memory, 4, 0x00000000); // NOP
// Block at 0x80000008: fallthrough path
WriteInstruction(memory, 8, 0x24020001); // ADDIU
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
// Block at 0x80000010: branch target
WriteInstruction(memory, 16, 0x24030002); // ADDIU
WriteInstruction(memory, 20, 0x03E00008); // JR $ra
WriteInstruction(memory, 24, 0x00000000); // NOP
var scanner = new BasicBlockScanner(memory, baseAddress);
// Act
scanner.ScanRange(baseAddress, baseAddress + 28);
// Assert
Assert.True(scanner.Blocks.ContainsKey(baseAddress));
Assert.True(scanner.Blocks.ContainsKey(baseAddress + 8));
// Branch target might be identified
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
+607
View File
@@ -0,0 +1,607 @@
using Xunit;
using Yaroze.Core.JIT;
namespace Yaroze.Tests.JIT;
public class BranchJumpTests
{
[Fact]
public void BEQ_TakenBranch_UpdatesPC()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// addiu $t1, $zero, 5
// beq $t0, $t1, target # Should take branch (5 == 5)
// nop
// target: addiu $v0, $zero, 42
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x24090005); // ADDIU $t1, $zero, 5
WriteInstruction(memory, 8, 0x11090001); // BEQ $t0, $t1, +1
WriteInstruction(memory, 12, 0x00000000); // NOP (delay slot)
WriteInstruction(memory, 16, 0x2402002A); // ADDIU $v0, $zero, 42
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BEQ_NotTakenBranch_ContinuesNormally()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// addiu $t1, $zero, 10
// beq $t0, $t1, target # Should NOT take branch (5 != 10)
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x2409000A); // ADDIU $t1, $zero, 10
WriteInstruction(memory, 8, 0x11090001); // BEQ $t0, $t1, +1
WriteInstruction(memory, 12, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BNE_TakenBranch_UpdatesPC()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// addiu $t1, $zero, 10
// bne $t0, $t1, target # Should take branch (5 != 10)
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x2409000A); // ADDIU $t1, $zero, 10
WriteInstruction(memory, 8, 0x15090001); // BNE $t0, $t1, +1
WriteInstruction(memory, 12, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BNE_NotTakenBranch_ContinuesNormally()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// addiu $t1, $zero, 5
// bne $t0, $t1, target # Should NOT take branch (5 == 5)
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x24090005); // ADDIU $t1, $zero, 5
WriteInstruction(memory, 8, 0x15090001); // BNE $t0, $t1, +1
WriteInstruction(memory, 12, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BLEZ_TakenBranch_NegativeValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, -5
// blez $t0, target # Should take branch (-5 <= 0)
// nop
WriteInstruction(memory, 0, 0x2408FFFB); // ADDIU $t0, $zero, -5
WriteInstruction(memory, 4, 0x19000001); // BLEZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BLEZ_TakenBranch_ZeroValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 0
// blez $t0, target # Should take branch (0 <= 0)
// nop
WriteInstruction(memory, 0, 0x24080000); // ADDIU $t0, $zero, 0
WriteInstruction(memory, 4, 0x19000001); // BLEZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BLEZ_NotTakenBranch_PositiveValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// blez $t0, target # Should NOT take branch (5 > 0)
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x19000001); // BLEZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BGTZ_TakenBranch_PositiveValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// bgtz $t0, target # Should take branch (5 > 0)
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x1D000001); // BGTZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BGTZ_NotTakenBranch_ZeroValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 0
// bgtz $t0, target # Should NOT take branch (0 <= 0)
// nop
WriteInstruction(memory, 0, 0x24080000); // ADDIU $t0, $zero, 0
WriteInstruction(memory, 4, 0x1D000001); // BGTZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BGTZ_NotTakenBranch_NegativeValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, -5
// bgtz $t0, target # Should NOT take branch (-5 <= 0)
// nop
WriteInstruction(memory, 0, 0x2408FFFB); // ADDIU $t0, $zero, -5
WriteInstruction(memory, 4, 0x1D000001); // BGTZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BLTZ_TakenBranch_NegativeValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, -5
// bltz $t0, target # Should take branch (-5 < 0)
// nop
WriteInstruction(memory, 0, 0x2408FFFB); // ADDIU $t0, $zero, -5
WriteInstruction(memory, 4, 0x05000001); // BLTZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BLTZ_NotTakenBranch_ZeroValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 0
// bltz $t0, target # Should NOT take branch (0 >= 0)
// nop
WriteInstruction(memory, 0, 0x24080000); // ADDIU $t0, $zero, 0
WriteInstruction(memory, 4, 0x05000001); // BLTZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BGEZ_TakenBranch_PositiveValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// bgez $t0, target # Should take branch (5 >= 0)
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x05010001); // BGEZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BGEZ_TakenBranch_ZeroValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 0
// bgez $t0, target # Should take branch (0 >= 0)
// nop
WriteInstruction(memory, 0, 0x24080000); // ADDIU $t0, $zero, 0
WriteInstruction(memory, 4, 0x05010001); // BGEZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BGEZ_NotTakenBranch_NegativeValue()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, -5
// bgez $t0, target # Should NOT take branch (-5 < 0)
// nop
WriteInstruction(memory, 0, 0x2408FFFB); // ADDIU $t0, $zero, -5
WriteInstruction(memory, 4, 0x05010001); // BGEZ $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void J_UnconditionalJump_UpdatesPC()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// j target
// nop
// (instruction encoding: 0x08000010 = j 0x80000040)
WriteInstruction(memory, 0, 0x08000010); // J 0x80000040
WriteInstruction(memory, 4, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void JAL_CallFunction_UpdatesRAandPC()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// jal function
// nop
WriteInstruction(memory, 0, 0x0C000010); // JAL 0x80000040
WriteInstruction(memory, 4, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
// Should set $ra to 0x80000008 (return address)
}
[Fact]
public void JR_IndirectJump_UpdatesPC()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 0x100
// jr $t0
// nop
WriteInstruction(memory, 0, 0x24080100); // ADDIU $t0, $zero, 0x100
WriteInstruction(memory, 4, 0x01000008); // JR $t0
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void JALR_IndirectCall_UpdatesRDandPC()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 0x100
// jalr $ra, $t0
// nop
WriteInstruction(memory, 0, 0x24080100); // ADDIU $t0, $zero, 0x100
WriteInstruction(memory, 4, 0x0100F809); // JALR $ra, $t0
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void DelaySlot_ExecutesBeforeBranch()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// beq $t0, $t0, target # Always taken
// addiu $v0, $zero, 42 # Delay slot - should execute
// target: addiu $v1, $zero, 1
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x11080001); // BEQ $t0, $t0, +1
WriteInstruction(memory, 8, 0x2402002A); // ADDIU $v0, $zero, 42 (delay slot)
WriteInstruction(memory, 12, 0x24030001); // ADDIU $v1, $zero, 1
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
// $v0 should be 42 even though branch is taken
}
[Fact]
public void ComplexControlFlow_MultipleConditionals()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 10
// addiu $t1, $zero, 5
// bne $t0, $t1, skip # Should take (10 != 5)
// addiu $v0, $zero, 1 # Delay slot
// skip: addiu $v1, $zero, 2
WriteInstruction(memory, 0, 0x2408000A); // ADDIU $t0, $zero, 10
WriteInstruction(memory, 4, 0x24090005); // ADDIU $t1, $zero, 5
WriteInstruction(memory, 8, 0x15090001); // BNE $t0, $t1, +1
WriteInstruction(memory, 12, 0x24020001); // ADDIU $v0, $zero, 1 (delay)
WriteInstruction(memory, 16, 0x24030002); // ADDIU $v1, $zero, 2
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
}
[Fact]
public void BLTZAL_TakenBranch_SetsReturnAddress()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, -5
// bltzal $t0, target # Should take branch (-5 < 0) and set $ra
// nop
WriteInstruction(memory, 0, 0x2408FFFB); // ADDIU $t0, $zero, -5
WriteInstruction(memory, 4, 0x05100001); // BLTZAL $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
// $ra should be set to 0x80000008 (return address)
}
[Fact]
public void BLTZAL_NotTakenBranch_StillSetsReturnAddress()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// bltzal $t0, target # Should NOT take branch (5 >= 0) but still set $ra
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x05100001); // BLTZAL $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
// $ra should still be set even though branch not taken
}
[Fact]
public void BGEZAL_TakenBranch_SetsReturnAddress()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 5
// bgezal $t0, target # Should take branch (5 >= 0) and set $ra
// nop
WriteInstruction(memory, 0, 0x24080005); // ADDIU $t0, $zero, 5
WriteInstruction(memory, 4, 0x05110001); // BGEZAL $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
// $ra should be set to 0x80000008 (return address)
}
[Fact]
public void BGEZAL_NotTakenBranch_StillSetsReturnAddress()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, -5
// bgezal $t0, target # Should NOT take branch (-5 < 0) but still set $ra
// nop
WriteInstruction(memory, 0, 0x2408FFFB); // ADDIU $t0, $zero, -5
WriteInstruction(memory, 4, 0x05110001); // BGEZAL $t0, +1
WriteInstruction(memory, 8, 0x00000000); // NOP (delay slot)
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
// $ra should still be set even though branch not taken
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
+469
View File
@@ -0,0 +1,469 @@
using Xunit;
using Yaroze.Core;
using Yaroze.Core.CPU;
using Yaroze.Core.JIT;
using Yaroze.Core.Memory;
namespace Yaroze.Tests.JIT;
public class JitCompilerTests
{
[Fact]
public void GetOrCompile_CreatesCompiledBlock()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// Simple block: addiu $v0, $zero, 42; jr $ra; nop
WriteInstruction(memory, 0, 0x2402002A); // ADDIU $v0, $zero, 42
WriteInstruction(memory, 4, 0x03E00008); // JR $ra
WriteInstruction(memory, 8, 0x00000000); // NOP
// Act
var block = compiler.GetOrCompile(0x80000000);
// Assert
Assert.NotNull(block);
Assert.Equal(0x80000000u, block.StartAddress);
Assert.NotNull(block.Execute);
}
[Fact]
public void CompiledBlock_ADDIU_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// addiu $v0, $zero, 42
WriteInstruction(memory, 0, 0x2402002A);
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(2, 0); // Ensure $v0 starts at 0
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(42u, cpu.Registers.ReadGPR(2)); // $v0 should be 42
}
[Fact]
public void CompiledBlock_ADDU_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// addu $v0, $t0, $t1
WriteInstruction(memory, 0, 0x01091021); // ADDU $v0, $t0, $t1
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 10); // $t0 = 10
cpu.Registers.WriteGPR(9, 32); // $t1 = 32
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(42u, cpu.Registers.ReadGPR(2)); // $v0 = 10 + 32 = 42
}
[Fact]
public void CompiledBlock_SUBU_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// subu $v0, $t0, $t1
WriteInstruction(memory, 0, 0x01091023); // SUBU $v0, $t0, $t1
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 50); // $t0 = 50
cpu.Registers.WriteGPR(9, 8); // $t1 = 8
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(42u, cpu.Registers.ReadGPR(2)); // $v0 = 50 - 8 = 42
}
[Fact]
public void CompiledBlock_AND_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// and $v0, $t0, $t1
WriteInstruction(memory, 0, 0x01091024); // AND $v0, $t0, $t1
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 0xFF); // $t0 = 0xFF
cpu.Registers.WriteGPR(9, 0x2A); // $t1 = 0x2A
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x2Au, cpu.Registers.ReadGPR(2)); // $v0 = 0xFF & 0x2A = 0x2A
}
[Fact]
public void CompiledBlock_OR_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// or $v0, $t0, $t1
WriteInstruction(memory, 0, 0x01091025); // OR $v0, $t0, $t1
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 0x20); // $t0 = 0x20
cpu.Registers.WriteGPR(9, 0x0A); // $t1 = 0x0A
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x2Au, cpu.Registers.ReadGPR(2)); // $v0 = 0x20 | 0x0A = 0x2A
}
[Fact]
public void CompiledBlock_XOR_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// xor $v0, $t0, $t1
WriteInstruction(memory, 0, 0x01091026); // XOR $v0, $t0, $t1
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 0xFF); // $t0 = 0xFF
cpu.Registers.WriteGPR(9, 0xD5); // $t1 = 0xD5
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x2Au, cpu.Registers.ReadGPR(2)); // $v0 = 0xFF ^ 0xD5 = 0x2A
}
[Fact]
public void CompiledBlock_ANDI_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// andi $v0, $t0, 0xFF
WriteInstruction(memory, 0, 0x310200FF); // ANDI $v0, $t0, 0xFF
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 0x12345678); // $t0
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x78u, cpu.Registers.ReadGPR(2)); // $v0 = 0x12345678 & 0xFF = 0x78
}
[Fact]
public void CompiledBlock_ORI_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// ori $v0, $t0, 0xFF
WriteInstruction(memory, 0, 0x350200FF); // ORI $v0, $t0, 0xFF
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 0x12345600); // $t0
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x123456FFu, cpu.Registers.ReadGPR(2)); // $v0 = 0x12345600 | 0xFF
}
[Fact]
public void CompiledBlock_SLL_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// sll $v0, $t0, 2
WriteInstruction(memory, 0, 0x00081080); // SLL $v0, $t0, 2
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 10); // $t0 = 10
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(40u, cpu.Registers.ReadGPR(2)); // $v0 = 10 << 2 = 40
}
[Fact]
public void CompiledBlock_SRL_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// srl $v0, $t0, 2
WriteInstruction(memory, 0, 0x00081082); // SRL $v0, $t0, 2
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 40); // $t0 = 40
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(10u, cpu.Registers.ReadGPR(2)); // $v0 = 40 >> 2 = 10
}
[Fact]
public void CompiledBlock_SRA_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// sra $v0, $t0, 2
WriteInstruction(memory, 0, 0x00081083); // SRA $v0, $t0, 2
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(8, 0xFFFFFFF0); // $t0 = -16 (signed)
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0xFFFFFFFCu, cpu.Registers.ReadGPR(2)); // $v0 = -16 >> 2 = -4 (signed shift)
}
[Fact]
public void CompiledBlock_LUI_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// lui $v0, 0x8000
WriteInstruction(memory, 0, 0x3C028000); // LUI $v0, 0x8000
var block = compiler.GetOrCompile(0x80000000);
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x80000000u, cpu.Registers.ReadGPR(2)); // $v0 = 0x8000 << 16
}
[Fact]
public void CompiledBlock_LW_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// lw $v0, 0($sp)
WriteInstruction(memory, 0, 0x8FA20000); // LW $v0, 0($sp)
// Write test data to RAM at 0x1000
bus.Write32(0x80001000, 0x12345678);
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(29, 0x80001000); // $sp = 0x80001000
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x12345678u, cpu.Registers.ReadGPR(2)); // $v0 loaded from memory
}
[Fact]
public void CompiledBlock_SW_ProducesCorrectResult()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// sw $v0, 0($sp)
WriteInstruction(memory, 0, 0xAFA20000); // SW $v0, 0($sp)
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(2, 0x12345678); // $v0 = 0x12345678
cpu.Registers.WriteGPR(29, 0x80001000); // $sp = 0x80001000
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0x12345678u, bus.Read32(0x80001000)); // Memory should contain value
}
[Fact]
public void CompiledBlock_LB_SignExtends()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// lb $v0, 0($sp)
WriteInstruction(memory, 0, 0x83A20000); // LB $v0, 0($sp)
bus.Write8(0x80001000, 0xFF); // Write -1 as signed byte
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(29, 0x80001000); // $sp
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0xFFFFFFFFu, cpu.Registers.ReadGPR(2)); // Sign extended to 0xFFFFFFFF
}
[Fact]
public void CompiledBlock_LBU_ZeroExtends()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// lbu $v0, 0($sp)
WriteInstruction(memory, 0, 0x93A20000); // LBU $v0, 0($sp)
bus.Write8(0x80001000, 0xFF);
var block = compiler.GetOrCompile(0x80000000);
cpu.Registers.WriteGPR(29, 0x80001000); // $sp
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(0xFFu, cpu.Registers.ReadGPR(2)); // Zero extended to 0x000000FF
}
[Fact]
public void CompiledBlock_MultipleInstructions_ExecutesInOrder()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// addiu $v0, $zero, 10
// addiu $v1, $zero, 32
// addu $a0, $v0, $v1
WriteInstruction(memory, 0, 0x2402000A); // ADDIU $v0, $zero, 10
WriteInstruction(memory, 4, 0x24030020); // ADDIU $v1, $zero, 32
WriteInstruction(memory, 8, 0x00432021); // ADDU $a0, $v0, $v1
var block = compiler.GetOrCompile(0x80000000);
// Act
block.Execute(cpu, bus);
// Assert
Assert.Equal(10u, cpu.Registers.ReadGPR(2)); // $v0 = 10
Assert.Equal(32u, cpu.Registers.ReadGPR(3)); // $v1 = 32
Assert.Equal(42u, cpu.Registers.ReadGPR(4)); // $a0 = 10 + 32 = 42
}
[Fact]
public void GetOrCompile_CachesCompiledBlocks()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
WriteInstruction(memory, 0, 0x2402002A); // ADDIU
// Act
var block1 = compiler.GetOrCompile(0x80000000);
var block2 = compiler.GetOrCompile(0x80000000);
// Assert
Assert.Same(block1, block2); // Should return cached instance
}
[Fact]
public void GetStats_ReturnsCorrectStatistics()
{
// Arrange
var (compiler, cpu, bus, memory) = CreateTestEnvironment();
// Create two blocks
WriteInstruction(memory, 0, 0x2402002A); // Block 1: 1 instruction
WriteInstruction(memory, 16, 0x24030001); // Block 2: 3 instructions
WriteInstruction(memory, 20, 0x24040002);
WriteInstruction(memory, 24, 0x24050003);
compiler.GetOrCompile(0x80000000);
compiler.GetOrCompile(0x80000010);
// Act
var stats = compiler.GetStats();
// Assert
Assert.Equal(2, stats.CompiledBlockCount);
Assert.True(stats.TotalInstructionsCompiled > 0);
Assert.True(stats.AverageBlockSize > 0);
}
[Fact]
public void CompiledBlock_ToString_FormatsCorrectly()
{
// Arrange
var block = new CompiledBlock
{
StartAddress = 0x80000000,
EndAddress = 0x80000010,
InstructionCount = 4,
Execute = (cpu, bus) => { }
};
// Act
string result = block.ToString();
// Assert
Assert.Contains("0x80000000", result);
Assert.Contains("0x80000010", result);
Assert.Contains("4 instructions", result);
}
[Fact]
public void JitStats_ToString_FormatsCorrectly()
{
// Arrange
var stats = new JitStats
{
CompiledBlockCount = 10,
TotalInstructionsCompiled = 50,
AverageBlockSize = 5.0
};
// Act
string result = stats.ToString();
// Assert
Assert.Contains("10 blocks", result);
Assert.Contains("50 instructions", result);
Assert.Contains("5", result);
}
private (JitCompiler compiler, Cpu cpu, Bus bus, byte[] memory) CreateTestEnvironment()
{
byte[] memory = new byte[4096];
var ram = new Ram();
var bus = new Bus();
var cpu = new Cpu(bus);
var compiler = new JitCompiler(cpu, bus, memory, 0x80000000);
return (compiler, cpu, bus, memory);
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
@@ -0,0 +1,443 @@
using Xunit;
using Yaroze.Core.JIT;
namespace Yaroze.Tests.JIT;
public class LockstepVerifierTests
{
[Fact]
public void VerifyBlock_SimpleArithmetic_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $v0, $zero, 42
// addiu $v1, $v0, 8
// jr $ra
// nop
WriteInstruction(memory, 0, 0x24020042); // ADDIU $v0, $zero, 42
WriteInstruction(memory, 4, 0x24430008); // ADDIU $v1, $v0, 8
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
Assert.Empty(result.Differences);
Assert.Equal(4, result.InstructionCount);
}
[Fact]
public void VerifyBlock_LoadStore_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $sp, $zero, 0x1000 (setup stack pointer)
// addiu $v0, $zero, 123
// sw $v0, 0($sp)
// lw $v1, 0($sp)
// jr $ra
// nop
WriteInstruction(memory, 0, 0x24BD1000); // ADDIU $sp, $zero, 0x1000
WriteInstruction(memory, 4, 0x2402007B); // ADDIU $v0, $zero, 123
WriteInstruction(memory, 8, 0xAFA20000); // SW $v0, 0($sp)
WriteInstruction(memory, 12, 0x8FA30000); // LW $v1, 0($sp)
WriteInstruction(memory, 16, 0x03E00008); // JR $ra
WriteInstruction(memory, 20, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
Assert.Empty(result.Differences);
}
[Fact]
public void VerifyBlock_LogicalOperations_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 0xFF
// addiu $t1, $zero, 0x0F
// and $t2, $t0, $t1
// or $t3, $t0, $t1
// jr $ra
// nop
WriteInstruction(memory, 0, 0x240800FF); // ADDIU $t0, $zero, 0xFF
WriteInstruction(memory, 4, 0x2409000F); // ADDIU $t1, $zero, 0x0F
WriteInstruction(memory, 8, 0x01095024); // AND $t2, $t0, $t1
WriteInstruction(memory, 12, 0x01095825); // OR $t3, $t0, $t1
WriteInstruction(memory, 16, 0x03E00008); // JR $ra
WriteInstruction(memory, 20, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
Assert.Empty(result.Differences);
}
[Fact]
public void VerifyBlock_ShiftOperations_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 8
// sll $t1, $t0, 2 # t1 = t0 << 2 = 32
// srl $t2, $t0, 1 # t2 = t0 >> 1 = 4
// jr $ra
// nop
WriteInstruction(memory, 0, 0x24080008); // ADDIU $t0, $zero, 8
WriteInstruction(memory, 4, 0x00084880); // SLL $t1, $t0, 2
WriteInstruction(memory, 8, 0x00085042); // SRL $t2, $t0, 1
WriteInstruction(memory, 12, 0x03E00008); // JR $ra
WriteInstruction(memory, 16, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
Assert.Empty(result.Differences);
}
[Fact]
public void VerifyBlock_ConditionalBranch_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// addiu $t0, $zero, 1
// beq $t0, $zero, target
// nop
WriteInstruction(memory, 0, 0x24080001); // ADDIU $t0, $zero, 1
WriteInstruction(memory, 4, 0x11000001); // BEQ $t0, $zero, +1
WriteInstruction(memory, 8, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert - Should match even though it's just identifying the block
Assert.True(result.IsMatch);
}
[Fact]
public void VerifyRange_MultipleBlocks_VerifiesAll()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// Block 1
WriteInstruction(memory, 0, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 4, 0x03E00008); // JR $ra
WriteInstruction(memory, 8, 0x00000000); // NOP
// Block 2
WriteInstruction(memory, 16, 0x24030002); // ADDIU $v1, $zero, 2
WriteInstruction(memory, 20, 0x03E00008); // JR $ra
WriteInstruction(memory, 24, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var summary = verifier.VerifyRange(baseAddress, baseAddress + 28);
// Assert
Assert.True(summary.TotalBlocks >= 2);
Assert.Equal(summary.PassedBlocks, summary.TotalBlocks);
Assert.Equal(0, summary.FailedBlocks);
Assert.Equal(1.0, summary.SuccessRate);
}
[Fact]
public void GetStats_TracksVerificationProgress()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
WriteInstruction(memory, 0, 0x24020001); // ADDIU $v0, $zero, 1
WriteInstruction(memory, 4, 0x24030002); // ADDIU $v1, $zero, 2
WriteInstruction(memory, 8, 0x03E00008); // JR $ra
WriteInstruction(memory, 12, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
verifier.VerifyBlock(baseAddress);
var stats = verifier.GetStats();
// Assert
Assert.Equal(1, stats.BlocksVerified);
Assert.Equal(4, stats.InstructionsVerified);
Assert.Equal(0, stats.Mismatches);
Assert.Equal(1.0, stats.SuccessRate);
}
[Fact]
public void CpuState_CaptureAndRestore_PreservesState()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
var verifier = new LockstepVerifier(memory, baseAddress);
// We can't directly access the CPU, but we can verify through a simple test
WriteInstruction(memory, 0, 0x24020042); // ADDIU $v0, $zero, 42
WriteInstruction(memory, 4, 0x03E00008); // JR $ra
WriteInstruction(memory, 8, 0x00000000); // NOP
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert - If capture/restore didn't work, states wouldn't match
Assert.True(result.IsMatch);
Assert.NotNull(result.InterpreterState);
Assert.NotNull(result.JitState);
}
[Fact]
public void VerificationResult_ToString_FormatsCorrectly()
{
// Arrange
var result = new VerificationResult
{
StartAddress = 0x80000000,
InstructionCount = 5,
IsMatch = true,
Differences = new string[0]
};
// Act
string text = result.ToString();
// Assert
Assert.Contains("0x80000000", text);
Assert.Contains("PASS", text);
}
[Fact]
public void VerificationResult_WithDifferences_FormatsCorrectly()
{
// Arrange
var result = new VerificationResult
{
StartAddress = 0x80000000,
InstructionCount = 5,
IsMatch = false,
Differences = new[] { "Register $2: 0x00000042 vs 0x00000043", "PC: 0x80000010 vs 0x80000014" }
};
// Act
string text = result.ToString();
// Assert
Assert.Contains("0x80000000", text);
Assert.Contains("FAIL", text);
Assert.Contains("2 differences", text);
}
[Fact]
public void VerificationSummary_ToString_FormatsCorrectly()
{
// Arrange
var summary = new VerificationSummary
{
TotalBlocks = 10,
PassedBlocks = 8,
FailedBlocks = 2,
TotalInstructions = 50
};
// Act
string text = summary.ToString();
// Assert
Assert.Contains("8/10", text);
Assert.Contains("80%", text);
Assert.Contains("50", text);
}
[Fact]
public void VerificationStats_ToString_FormatsCorrectly()
{
// Arrange
var stats = new VerificationStats
{
BlocksVerified = 20,
InstructionsVerified = 150,
Mismatches = 2,
SuccessRate = 0.9
};
// Act
string text = stats.ToString();
// Assert
Assert.Contains("20", text);
Assert.Contains("150", text);
Assert.Contains("2", text);
Assert.Contains("90%", text);
}
[Fact]
public void StateComparison_Match_ReturnsCorrectMessage()
{
// Arrange
var comparison = new StateComparison
{
IsMatch = true,
Differences = new string[0]
};
// Act
string text = comparison.ToString();
// Assert
Assert.Equal("States match", text);
}
[Fact]
public void StateComparison_WithDifferences_ReturnsCorrectMessage()
{
// Arrange
var comparison = new StateComparison
{
IsMatch = false,
Differences = new[] { "Register $2: 0x00000042 vs 0x00000043" }
};
// Act
string text = comparison.ToString();
// Assert
Assert.Contains("States differ", text);
Assert.Contains("Register $2", text);
}
[Fact]
public void VerifyBlock_ComplexSequence_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// A more complex sequence combining multiple operation types
// addiu $t0, $zero, 100
// addiu $t1, $zero, 50
// add $t2, $t0, $t1 # t2 = 150
// sll $t3, $t2, 1 # t3 = 300
// sub $t4, $t3, $t1 # t4 = 250
// jr $ra
// nop
WriteInstruction(memory, 0, 0x24080064); // ADDIU $t0, $zero, 100
WriteInstruction(memory, 4, 0x24090032); // ADDIU $t1, $zero, 50
WriteInstruction(memory, 8, 0x01095020); // ADD $t2, $t0, $t1
WriteInstruction(memory, 12, 0x000A5840); // SLL $t3, $t2, 1
WriteInstruction(memory, 16, 0x01696022); // SUB $t4, $t3, $t1
WriteInstruction(memory, 20, 0x03E00008); // JR $ra
WriteInstruction(memory, 24, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
Assert.Empty(result.Differences);
Assert.Equal(7, result.InstructionCount);
}
[Fact]
public void VerifyBlock_ImmediateOperations_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// Test immediate operations
// addiu $t0, $zero, 0xFFFF
// andi $t1, $t0, 0x00FF
// ori $t2, $t1, 0xF000
// xori $t3, $t2, 0x0F0F
// jr $ra
// nop
WriteInstruction(memory, 0, 0x2408FFFF); // ADDIU $t0, $zero, 0xFFFF
WriteInstruction(memory, 4, 0x310900FF); // ANDI $t1, $t0, 0x00FF
WriteInstruction(memory, 8, 0x352AF000); // ORI $t2, $t1, 0xF000
WriteInstruction(memory, 12, 0x394B0F0F); // XORI $t3, $t2, 0x0F0F
WriteInstruction(memory, 16, 0x03E00008); // JR $ra
WriteInstruction(memory, 20, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
Assert.Empty(result.Differences);
}
[Fact]
public void VerifyBlock_ComparisonOperations_MatchesInterpreter()
{
// Arrange
byte[] memory = new byte[4096];
uint baseAddress = 0x80000000;
// Test comparison operations
// addiu $t0, $zero, 10
// addiu $t1, $zero, 20
// slt $t2, $t0, $t1 # t2 = 1 (10 < 20)
// sltu $t3, $t1, $t0 # t3 = 0 (20 < 10 is false)
// jr $ra
// nop
WriteInstruction(memory, 0, 0x2408000A); // ADDIU $t0, $zero, 10
WriteInstruction(memory, 4, 0x24090014); // ADDIU $t1, $zero, 20
WriteInstruction(memory, 8, 0x0109502A); // SLT $t2, $t0, $t1
WriteInstruction(memory, 12, 0x0128582B); // SLTU $t3, $t1, $t0
WriteInstruction(memory, 16, 0x03E00008); // JR $ra
WriteInstruction(memory, 20, 0x00000000); // NOP
var verifier = new LockstepVerifier(memory, baseAddress);
// Act
var result = verifier.VerifyBlock(baseAddress);
// Assert
Assert.True(result.IsMatch);
Assert.Empty(result.Differences);
}
private void WriteInstruction(byte[] memory, int offset, uint instruction)
{
byte[] bytes = BitConverter.GetBytes(instruction);
Array.Copy(bytes, 0, memory, offset, 4);
}
}
@@ -0,0 +1,251 @@
using Yaroze.Core.CPU;
using Yaroze.Core.Loaders;
using Yaroze.Core.Memory;
namespace Yaroze.Tests.Loaders;
public class PsExeLoaderTests
{
/// <summary>
/// Create a minimal valid PS-EXE for testing.
/// </summary>
private byte[] CreateMinimalExe(uint pc = 0x80010000, uint gp = 0x80020000, uint loadAddr = 0x80010000)
{
byte[] exe = new byte[0x800 + 16]; // Header + 16 bytes of code
// Magic "PS-X EXE"
Array.Copy(System.Text.Encoding.ASCII.GetBytes("PS-X EXE"), 0, exe, 0, 8);
// Initial PC
WriteUInt32LE(exe, 0x010, pc);
// Initial GP
WriteUInt32LE(exe, 0x014, gp);
// Load address
WriteUInt32LE(exe, 0x018, loadAddr);
// File size (16 bytes of code)
WriteUInt32LE(exe, 0x01C, 16);
// Stack base and offset (use defaults)
WriteUInt32LE(exe, 0x030, 0);
WriteUInt32LE(exe, 0x034, 0);
// Add some dummy code (4 NOPs)
WriteUInt32LE(exe, 0x800, 0x00000000); // NOP
WriteUInt32LE(exe, 0x804, 0x00000000); // NOP
WriteUInt32LE(exe, 0x808, 0x00000000); // NOP
WriteUInt32LE(exe, 0x80C, 0x00000000); // NOP
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 Load_ValidExe_LoadsSuccessfully()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = CreateMinimalExe();
var header = PsExeLoader.Load(exeData, bus, cpu);
Assert.Equal("PS-X EXE", header.Magic);
Assert.Equal(0x80010000u, header.InitialPC);
Assert.Equal(0x80020000u, header.InitialGP);
Assert.Equal(0x80010000u, header.LoadAddress);
Assert.Equal(16u, header.FileSize);
}
[Fact]
public void Load_InitializesCpuRegisters()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = CreateMinimalExe(
pc: 0x80010000,
gp: 0x80020000,
loadAddr: 0x80010000
);
PsExeLoader.Load(exeData, bus, cpu);
// Check CPU state
Assert.Equal(0x80010000u, cpu.Registers.PC);
Assert.Equal(0x80020000u, cpu.Registers.ReadGPR(28)); // $gp
Assert.Equal(0x801FFF00u, cpu.Registers.ReadGPR(29)); // $sp (default)
Assert.Equal(0x801FFF00u, cpu.Registers.ReadGPR(30)); // $fp
Assert.Equal(0u, cpu.Registers.ReadGPR(31)); // $ra
}
[Fact]
public void Load_LoadsCodeIntoMemory()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = CreateMinimalExe();
PsExeLoader.Load(exeData, bus, cpu);
// Check that code was loaded (should be NOPs = 0x00000000)
uint instruction1 = bus.Read32(0x80010000);
uint instruction2 = bus.Read32(0x80010004);
Assert.Equal(0x00000000u, instruction1);
Assert.Equal(0x00000000u, instruction2);
}
[Fact]
public void Load_InvalidMagic_ThrowsException()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = CreateMinimalExe();
// Corrupt magic
exeData[0] = (byte)'X';
Assert.Throws<InvalidDataException>(() =>
PsExeLoader.Load(exeData, bus, cpu)
);
}
[Fact]
public void Load_FileTooSmall_ThrowsException()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = new byte[0x100]; // Only 256 bytes, need 0x800
Assert.Throws<InvalidDataException>(() =>
PsExeLoader.Load(exeData, bus, cpu)
);
}
[Fact]
public void Load_PCOutsideLoadedRegion_ThrowsException()
{
var bus = new Bus();
var cpu = new Cpu(bus);
// Create EXE with PC pointing outside the loaded code
byte[] exeData = CreateMinimalExe(
pc: 0x80030000, // PC way beyond loaded region
loadAddr: 0x80010000
);
Assert.Throws<InvalidDataException>(() =>
PsExeLoader.Load(exeData, bus, cpu)
);
}
[Fact]
public void Load_WithCustomStack_UsesCustomStackPointer()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = CreateMinimalExe();
// Set custom stack
WriteUInt32LE(exeData, 0x030, 0x801F0000); // Stack base
WriteUInt32LE(exeData, 0x034, 0x00008000); // Stack offset
PsExeLoader.Load(exeData, bus, cpu);
// Stack pointer should be base + offset
Assert.Equal(0x801F8000u, cpu.Registers.ReadGPR(29));
}
[Fact]
public void Load_WithBssSection_ClearsBss()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = CreateMinimalExe();
// Set BSS section
WriteUInt32LE(exeData, 0x028, 0x80030000); // BSS address
WriteUInt32LE(exeData, 0x02C, 0x100); // BSS size (256 bytes)
// Write non-zero data to BSS region first
for (uint addr = 0x80030000; addr < 0x80030100; addr += 4)
{
bus.Write32(addr, 0xDEADBEEF);
}
// Load EXE (should clear BSS)
PsExeLoader.Load(exeData, bus, cpu);
// Check that BSS was cleared
for (uint addr = 0x80030000; addr < 0x80030100; addr += 4)
{
Assert.Equal(0u, bus.Read32(addr));
}
}
[Fact]
public void GetHeaderSummary_ReturnsFormattedString()
{
var bus = new Bus();
var cpu = new Cpu(bus);
byte[] exeData = CreateMinimalExe();
var header = PsExeLoader.Load(exeData, bus, cpu);
string summary = PsExeLoader.GetHeaderSummary(header);
Assert.Contains("Entry Point", summary);
Assert.Contains("0x80010000", summary);
Assert.Contains("Global Pointer", summary);
}
[Fact]
public void Load_RealWorldSizeExe_HandlesCorrectly()
{
var bus = new Bus();
var cpu = new Cpu(bus);
// Create a more realistic-sized EXE (64KB of code)
int codeSize = 65536;
byte[] exeData = new byte[0x800 + codeSize];
// Magic
Array.Copy(System.Text.Encoding.ASCII.GetBytes("PS-X EXE"), 0, exeData, 0, 8);
// Standard PS1 game entry point
WriteUInt32LE(exeData, 0x010, 0x80010000);
WriteUInt32LE(exeData, 0x014, 0x8001FFFF);
WriteUInt32LE(exeData, 0x018, 0x80010000);
WriteUInt32LE(exeData, 0x01C, (uint)codeSize);
// Fill code with some pattern
for (int i = 0; i < codeSize; i += 4)
{
WriteUInt32LE(exeData, 0x800 + i, (uint)i);
}
var header = PsExeLoader.Load(exeData, bus, cpu);
Assert.Equal((uint)codeSize, header.FileSize);
// Verify code was loaded correctly
Assert.Equal(0u, bus.Read32(0x80010000));
Assert.Equal(4u, bus.Read32(0x80010004));
Assert.Equal(8u, bus.Read32(0x80010008));
}
}
+158
View File
@@ -0,0 +1,158 @@
using Yaroze.Core.Memory;
namespace Yaroze.Tests.Memory;
public class BusTests
{
[Fact]
public void AddressTranslation_KUSEG_MapsCorrectly()
{
var bus = new Bus();
// KUSEG: 0x00000000 - 0x7FFFFFFF should map to physical 0x00000000 - 0x1FFFFFFF
bus.Write32(0x00001000, 0xDEADBEEF);
Assert.Equal(0xDEADBEEFu, bus.Read32(0x00001000));
}
[Fact]
public void AddressTranslation_KSEG0_MapsToSamePhysicalAsKUSEG()
{
var bus = new Bus();
// Write via KUSEG
bus.Write32(0x00001000, 0xCAFEBABE);
// Read via KSEG0 (should map to same physical address)
Assert.Equal(0xCAFEBABEu, bus.Read32(0x80001000));
}
[Fact]
public void AddressTranslation_KSEG1_MapsToSamePhysicalAsKSEG0()
{
var bus = new Bus();
// Write via KSEG0
bus.Write32(0x80001000, 0x12345678);
// Read via KSEG1 (uncached, but same physical address)
Assert.Equal(0x12345678u, bus.Read32(0xA0001000));
}
[Fact]
public void RAM_ReadWriteCorrectly()
{
var bus = new Bus();
// Test various addresses in RAM
bus.Write32(0x00000000, 0x11111111);
bus.Write32(0x00100000, 0x22222222);
bus.Write32(0x001FFFFC, 0x33333333);
Assert.Equal(0x11111111u, bus.Read32(0x00000000));
Assert.Equal(0x22222222u, bus.Read32(0x00100000));
Assert.Equal(0x33333333u, bus.Read32(0x001FFFFC));
}
[Fact]
public void Scratchpad_ReadWriteCorrectly()
{
var bus = new Bus();
// Write to scratchpad
bus.Write32(0x1F800000, 0xAABBCCDD);
bus.Write32(0x1F8003FC, 0x11223344);
Assert.Equal(0xAABBCCDDu, bus.Read32(0x1F800000));
Assert.Equal(0x11223344u, bus.Read32(0x1F8003FC));
}
[Fact]
public void BIOS_ReadOnlyWithoutImage()
{
var bus = new Bus();
// Writing to BIOS should be ignored
bus.Write32(0x1FC00000, 0xDEADBEEF);
// Reading should return 0 (no BIOS loaded)
Assert.Equal(0u, bus.Read32(0x1FC00000));
}
[Fact]
public void UnmappedAddress_ReturnsFFFFFFFF()
{
var bus = new Bus();
// Address not mapped to any device
uint result = bus.Read32(0x1F900000);
Assert.Equal(0xFFFFFFFFu, result);
}
[Fact]
public void Read16_ReadsHalfwordCorrectly()
{
var bus = new Bus();
bus.Write32(0x00001000, 0x12345678);
// Little-endian: bytes at 0x1000 are: 78, 56, 34, 12
Assert.Equal(0x5678, bus.Read16(0x00001000));
Assert.Equal(0x1234, bus.Read16(0x00001002));
}
[Fact]
public void Read8_ReadsByteCorrectly()
{
var bus = new Bus();
bus.Write32(0x00001000, 0x12345678);
// Little-endian: bytes are 78, 56, 34, 12
Assert.Equal(0x78, bus.Read8(0x00001000));
Assert.Equal(0x56, bus.Read8(0x00001001));
Assert.Equal(0x34, bus.Read8(0x00001002));
Assert.Equal(0x12, bus.Read8(0x00001003));
}
[Fact]
public void Write16_WritesHalfwordCorrectly()
{
var bus = new Bus();
bus.Write16(0x00001000, 0xABCD);
bus.Write16(0x00001002, 0x1234);
uint result = bus.Read32(0x00001000);
Assert.Equal(0x1234ABCDu, result);
}
[Fact]
public void Write8_WritesByteCorrectly()
{
var bus = new Bus();
bus.Write8(0x00001000, 0x11);
bus.Write8(0x00001001, 0x22);
bus.Write8(0x00001002, 0x33);
bus.Write8(0x00001003, 0x44);
uint result = bus.Read32(0x00001000);
Assert.Equal(0x44332211u, result);
}
[Fact]
public void IsAligned_ChecksAlignmentCorrectly()
{
Assert.True(Bus.IsAligned(0x1000, 4));
Assert.True(Bus.IsAligned(0x1000, 2));
Assert.True(Bus.IsAligned(0x1000, 1));
Assert.False(Bus.IsAligned(0x1001, 4));
Assert.False(Bus.IsAligned(0x1001, 2));
Assert.True(Bus.IsAligned(0x1001, 1));
Assert.False(Bus.IsAligned(0x1002, 4));
Assert.True(Bus.IsAligned(0x1002, 2));
}
}
+201
View File
@@ -0,0 +1,201 @@
using Xunit;
using Yaroze.Core;
namespace Yaroze.Tests.Timers;
public class TimerTests
{
private readonly Emulator _emu;
public TimerTests()
{
_emu = new Emulator();
}
[Fact]
public void Timer_Reset_ClearsCounter()
{
// Arrange
_emu.Bus.Write32(0x1F801100, 0x1234); // Timer 0 counter
// Act
_emu.Timer0.Reset();
// Assert
Assert.Equal(0, _emu.Timer0.Counter);
}
[Fact]
public void Timer_CounterValue_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F801100, 0x5678); // Timer 0 counter
uint result = _emu.Bus.Read32(0x1F801100);
// Assert
Assert.Equal(0x5678u, result);
}
[Fact]
public void Timer_Mode_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F801104, 0x0123); // Timer 0 mode
uint result = _emu.Bus.Read32(0x1F801104);
// Assert
Assert.Equal(0x0123u, result);
}
[Fact]
public void Timer_Target_ReadWrite()
{
// Act
_emu.Bus.Write32(0x1F801108, 0xABCD); // Timer 0 target
uint result = _emu.Bus.Read32(0x1F801108);
// Assert
Assert.Equal(0xABCDu, result);
}
[Fact]
public void Timer_WritingMode_ResetsCounter()
{
// Arrange
_emu.Bus.Write32(0x1F801100, 0x1234); // Set counter
// Act
_emu.Bus.Write32(0x1F801104, 0x0001); // Write mode
// Assert - Counter should be reset to 0
Assert.Equal(0, _emu.Timer0.Counter);
}
[Fact]
public void Timer_Tick_IncrementsCounter()
{
// Arrange
_emu.Timer0.Reset();
// Act
_emu.Timer0.Tick(10);
// Assert
Assert.True(_emu.Timer0.Counter > 0);
}
[Fact]
public void Timer_ReachTarget_SetsFlag()
{
// Arrange
_emu.Bus.Write32(0x1F801108, 100); // Target = 100
_emu.Bus.Write32(0x1F801104, 0x0000); // Mode = free run
// Act
_emu.Timer0.Tick(150); // Should reach and pass target
// Assert
Assert.True(_emu.Timer0.ReachedTarget);
}
[Fact]
public void Timer_ResetOnTarget_ResetsCounter()
{
// Arrange
_emu.Bus.Write32(0x1F801108, 50); // Target = 50
_emu.Bus.Write32(0x1F801104, 0x0008); // Mode = reset on target (bit 3)
// Act
_emu.Timer0.Tick(100);
// Assert - Counter should have reset at target and continued
Assert.True(_emu.Timer0.Counter < 50);
}
[Fact]
public void Timer_IrqOnTarget_TriggersInterrupt()
{
// Arrange - Enable IRQ on target (bit 4) and repeat mode (bit 6)
_emu.Bus.Write32(0x1F801108, 50); // Target = 50
_emu.Bus.Write32(0x1F801104, 0x0050); // IRQ on target + repeat
// Act
_emu.Timer0.Tick(100);
// Assert
Assert.True(_emu.Timer0.IrqPending);
}
[Fact]
public void Timer_AllTimersAccessible()
{
// Test Timer 0, 1, 2 at their respective addresses
_emu.Bus.Write32(0x1F801100, 111); // Timer 0
_emu.Bus.Write32(0x1F801110, 222); // Timer 1
_emu.Bus.Write32(0x1F801120, 333); // Timer 2
Assert.Equal(111, _emu.Timer0.Counter);
Assert.Equal(222, _emu.Timer1.Counter);
Assert.Equal(333, _emu.Timer2.Counter);
}
[Fact]
public void Timer_Overflow_SetsFlag()
{
// Arrange - Start near overflow
_emu.Bus.Write32(0x1F801100, 0xFFFF - 10);
_emu.Bus.Write32(0x1F801104, 0x0000); // Free run mode
// Act
_emu.Timer0.Tick(20); // Should overflow
// Assert
Assert.True(_emu.Timer0.ReachedOverflow);
}
[Fact]
public void Timer_IrqOnOverflow_TriggersInterrupt()
{
// Arrange - Start near overflow, enable IRQ on overflow (bit 5)
_emu.Bus.Write32(0x1F801100, 0xFFFF - 5);
_emu.Bus.Write32(0x1F801104, 0x0060); // IRQ on overflow + repeat
// Act
_emu.Timer0.Tick(10);
// Assert
Assert.True(_emu.Timer0.IrqPending);
}
[Fact]
public void Timer_ClearIrqFlag_ByWritingOne()
{
// Arrange - Trigger IRQ
_emu.Bus.Write32(0x1F801108, 10);
_emu.Bus.Write32(0x1F801104, 0x0050); // IRQ on target + repeat
_emu.Timer0.Tick(20);
Assert.True(_emu.Timer0.IrqPending);
// Act - Clear by writing 1 to bit 10
_emu.Bus.Write32(0x1F801104, 0x0400);
// Assert
Assert.False(_emu.Timer0.IrqPending);
}
[Fact]
public void Timer_EmulatorStep_TicksTimers()
{
// Arrange
_emu.Reset();
_emu.Bus.Ram.Write32(0, 0x00000000); // NOP
ushort initialCounter = _emu.Timer0.Counter;
// Act - Step CPU (which should tick timers)
_emu.Step();
// Assert - Timer should have advanced
Assert.True(_emu.Timer0.Counter > initialCounter);
}
}
+29
View File
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Yaroze.Core\Yaroze.Core.csproj" />
</ItemGroup>
</Project>