675 lines
29 KiB
C#
675 lines
29 KiB
C#
// InflateDebugger — Z80 emulator for TSConfig inflate + WC plugin debugging
|
||
//
|
||
// MODE 1 — inflate (default):
|
||
// Loads VGZ file into emulated TSConfig memory pages (as tape loader would),
|
||
// loads inflate.bin into Win3, sets up registers as inflate_call.s would,
|
||
// and runs the Z80 CPU with debug port monitoring and watchdog.
|
||
//
|
||
// Usage: dotnet run [--vgz <path>] [--ref <path>] [--inflate <path>]
|
||
// [--dump-on-hang] [--max-cycles <N>] [--watchdog <N>]
|
||
//
|
||
// MODE 2 — plugin:
|
||
// Loads a WMF plugin binary, emulates WC kernel API (P8000 dispatch),
|
||
// emulates disk I/O from test_data directory, runs the full plugin lifecycle
|
||
// with CPU trace, page protection, and WC call logging.
|
||
//
|
||
// Usage: dotnet run --mode plugin --wmf <path> [--file <vgz>]
|
||
// [--max-cycles <N>] [--verbose]
|
||
|
||
using System;
|
||
using System.IO;
|
||
using System.IO.Compression;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using ZXMAK2.Engine.Cpu;
|
||
using ZXMAK2.Engine.Cpu.Processor;
|
||
|
||
namespace InflateDebugger
|
||
{
|
||
class Program
|
||
{
|
||
// ── TSConfig page assignments (matching real WC plugin) ──
|
||
const byte SRC_START_PAGE = 0xA1; // TAP pages for VGZ source
|
||
const byte DST_START_PAGE = 0x20; // megabuffer for decompressed output
|
||
const byte INFLATE_PAGE = 0x6C; // inflate code page (base 0x67 + 5)
|
||
const byte PAGE_DECODE = 0xEA; // decode working buffer page
|
||
const byte WC_BASE_PAGE = 0x67; // WC plugin base page
|
||
|
||
// ── Ports ──
|
||
const ushort DBG_STATUS = 0xFFAF;
|
||
const ushort DBG_SRC = 0xFEAF;
|
||
const ushort DBG_DST = 0xFDAF;
|
||
|
||
// ── Defaults ──
|
||
const long DEFAULT_MAX_CYCLES = 500_000_000; // 500M cycles max
|
||
const long DEFAULT_WATCHDOG = 200_000; // 200K cycles without dbg port = hang
|
||
|
||
// ── State ──
|
||
static TsMemory mem = new();
|
||
static CpuUnit cpu = new();
|
||
|
||
static long lastDbgCycle = 0;
|
||
static long watchdogLimit = DEFAULT_WATCHDOG;
|
||
static long maxCycles = DEFAULT_MAX_CYCLES;
|
||
static bool dumpOnHang = true;
|
||
static bool finished = false;
|
||
static byte lastDbgStatus = 0;
|
||
static byte lastDbgSrc = 0;
|
||
static byte lastDbgDst = 0;
|
||
static int srcPagesLoaded = 0;
|
||
static string? refPath = null;
|
||
static string dumpDir = "";
|
||
|
||
// Track which pages were written as destination
|
||
static HashSet<int> dstPagesWritten = new();
|
||
|
||
static int Main(string[] args)
|
||
{
|
||
string basePath = FindProjectRoot();
|
||
|
||
// ── Detect mode ──
|
||
string mode = "inflate"; // default
|
||
for (int i = 0; i < args.Length; i++)
|
||
{
|
||
if (args[i] == "--mode" && i + 1 < args.Length)
|
||
{
|
||
mode = args[++i].ToLowerInvariant();
|
||
break;
|
||
}
|
||
}
|
||
|
||
return mode switch
|
||
{
|
||
"plugin" => RunPluginMode(args, basePath),
|
||
_ => RunInflateMode(args, basePath)
|
||
};
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// Plugin mode — runs full WMF plugin with WC API emulation
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
static int RunPluginMode(string[] args, string basePath)
|
||
{
|
||
string? wmfPath = null;
|
||
string? testFile = null;
|
||
string? diskRoot = Path.Combine(basePath, "Debuger", "test_data");
|
||
long maxCyclesP = 2_000_000_000;
|
||
bool verbose = true;
|
||
bool verbosePages = false;
|
||
|
||
for (int i = 0; i < args.Length; i++)
|
||
{
|
||
switch (args[i])
|
||
{
|
||
case "--wmf" when i + 1 < args.Length:
|
||
wmfPath = args[++i]; break;
|
||
case "--file" when i + 1 < args.Length:
|
||
testFile = args[++i]; break;
|
||
case "--disk-root" when i + 1 < args.Length:
|
||
diskRoot = args[++i]; break;
|
||
case "--max-cycles" when i + 1 < args.Length:
|
||
maxCyclesP = long.Parse(args[++i]); break;
|
||
case "--verbose":
|
||
verbose = true; break;
|
||
case "--quiet":
|
||
verbose = false; break;
|
||
case "--verbose-pages":
|
||
verbosePages = true; break;
|
||
}
|
||
}
|
||
|
||
// Default WMF: look in DiskRef/WC/VGMPLAY.WMF or variants/
|
||
if (wmfPath == null)
|
||
{
|
||
wmfPath = Path.Combine(basePath, @"DiskRef\WC\VGMPLAY.WMF");
|
||
if (!File.Exists(wmfPath))
|
||
wmfPath = Path.Combine(basePath, @"variants\VGMPLAY_v1_b8.WMF");
|
||
}
|
||
|
||
// Default test file: first .vgz in test_data
|
||
if (testFile == null && Directory.Exists(diskRoot!))
|
||
{
|
||
var firstVgz = Directory.GetFiles(diskRoot!, "*.vgz")
|
||
.OrderBy(f => f).FirstOrDefault();
|
||
if (firstVgz != null)
|
||
testFile = Path.GetFileName(firstVgz);
|
||
}
|
||
|
||
var runner = new PluginRunner
|
||
{
|
||
MaxCycles = maxCyclesP,
|
||
VerboseWcCalls = verbose,
|
||
VerbosePageSwitch = verbosePages,
|
||
DumpOnExit = true,
|
||
DumpDir = Path.Combine(basePath, @"Debuger\dumps"),
|
||
DiskRootPath = diskRoot!,
|
||
TestFileName = testFile
|
||
};
|
||
|
||
return runner.Run(wmfPath!);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// Inflate mode — original inflate test harness
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
static int RunInflateMode(string[] args, string basePath)
|
||
{
|
||
string vgzPath = Path.Combine(basePath,
|
||
@"DiskRef\Music\OPL2\Arcade\_\Act-Fancer_Cybernetick_Hyper_Weapon_(Arcade)\02 Sci-Vax (BGM 1).vgz");
|
||
string inflatePath = Path.Combine(basePath, @"src_sdcc\build\inflate.bin");
|
||
refPath = Path.Combine(basePath, @"Debuger\ref.vgm");
|
||
dumpDir = Path.Combine(basePath, @"Debuger\dumps");
|
||
|
||
// Parse args
|
||
for (int i = 0; i < args.Length; i++)
|
||
{
|
||
switch (args[i])
|
||
{
|
||
case "--vgz" when i + 1 < args.Length: vgzPath = args[++i]; break;
|
||
case "--ref" when i + 1 < args.Length: refPath = args[++i]; break;
|
||
case "--inflate" when i + 1 < args.Length: inflatePath = args[++i]; break;
|
||
case "--max-cycles" when i + 1 < args.Length: maxCycles = long.Parse(args[++i]); break;
|
||
case "--watchdog" when i + 1 < args.Length: watchdogLimit = long.Parse(args[++i]); break;
|
||
case "--dump-on-hang": dumpOnHang = true; break;
|
||
case "--no-dump": dumpOnHang = false; break;
|
||
}
|
||
}
|
||
|
||
Console.WriteLine("═══════════════════════════════════════════════════");
|
||
Console.WriteLine(" TSConfig Inflate Debugger");
|
||
Console.WriteLine("═══════════════════════════════════════════════════");
|
||
|
||
// ─── 1. Load VGZ file ───
|
||
if (!File.Exists(vgzPath))
|
||
{
|
||
Console.Error.WriteLine($"VGZ file not found: {vgzPath}");
|
||
return 1;
|
||
}
|
||
byte[] vgzData = File.ReadAllBytes(vgzPath);
|
||
Console.WriteLine($"VGZ file: {vgzPath}");
|
||
Console.WriteLine($"VGZ size: {vgzData.Length} bytes");
|
||
|
||
// ─── 2. Load inflate.bin ───
|
||
if (!File.Exists(inflatePath))
|
||
{
|
||
Console.Error.WriteLine($"inflate.bin not found: {inflatePath}");
|
||
return 1;
|
||
}
|
||
byte[] inflateData = File.ReadAllBytes(inflatePath);
|
||
Console.WriteLine($"inflate.bin: {inflateData.Length} bytes");
|
||
|
||
// ─── 3. Set up TSConfig memory ───
|
||
SetupMemory(vgzData, inflateData);
|
||
|
||
// ─── 4. Set up CPU ───
|
||
SetupCpu();
|
||
|
||
// ─── 5. Run ───
|
||
Console.WriteLine();
|
||
Console.WriteLine("─── Execution Start ─────────────────────────────");
|
||
Console.WriteLine();
|
||
|
||
int exitCode = RunCpu();
|
||
|
||
// ─── 6. Post-run analysis ───
|
||
Console.WriteLine();
|
||
Console.WriteLine("─── Post-Run Analysis ───────────────────────────");
|
||
AnalyzeResults();
|
||
|
||
return exitCode;
|
||
}
|
||
|
||
static string FindProjectRoot()
|
||
{
|
||
// Walk up from current dir or exe dir looking for Debuger folder
|
||
string? dir = AppDomain.CurrentDomain.BaseDirectory;
|
||
for (int i = 0; i < 10 && dir != null; i++)
|
||
{
|
||
if (Directory.Exists(Path.Combine(dir, "Debuger")) &&
|
||
Directory.Exists(Path.Combine(dir, "src_sdcc")))
|
||
return dir;
|
||
dir = Path.GetDirectoryName(dir);
|
||
}
|
||
// Try current directory
|
||
dir = Environment.CurrentDirectory;
|
||
for (int i = 0; i < 10 && dir != null; i++)
|
||
{
|
||
if (Directory.Exists(Path.Combine(dir, "Debuger")) &&
|
||
Directory.Exists(Path.Combine(dir, "src_sdcc")))
|
||
return dir;
|
||
dir = Path.GetDirectoryName(dir);
|
||
}
|
||
return @"D:\zx_projects\OplPlug";
|
||
}
|
||
|
||
static void SetupMemory(byte[] vgzData, byte[] inflateData)
|
||
{
|
||
Console.WriteLine();
|
||
Console.WriteLine("─── Memory Setup ────────────────────────────────");
|
||
|
||
// Load VGZ into TAP pages (0xA1+) — as WC tape loader would
|
||
srcPagesLoaded = mem.LoadToPages(vgzData, SRC_START_PAGE);
|
||
Console.WriteLine($"VGZ loaded to pages 0x{SRC_START_PAGE:X2}–0x{SRC_START_PAGE + srcPagesLoaded - 1:X2} ({srcPagesLoaded} pages)");
|
||
|
||
// Load inflate.bin into inflate page (0x6C)
|
||
// inflate.bin is padded to fill code + RAM tables within one 16KB page
|
||
if (inflateData.Length > TsMemory.PageSize)
|
||
{
|
||
Console.WriteLine($"WARNING: inflate.bin ({inflateData.Length}) > 16KB, truncating");
|
||
}
|
||
Array.Copy(inflateData, 0, mem.GetPage(INFLATE_PAGE), 0,
|
||
Math.Min(inflateData.Length, TsMemory.PageSize));
|
||
Console.WriteLine($"inflate.bin loaded to page 0x{INFLATE_PAGE:X2}");
|
||
|
||
// Set up initial window mapping:
|
||
// Win0 = source page 0xA1 (set by inflate entry code)
|
||
// Win1 = don't care yet (inflate sets it)
|
||
// Win2 = WC base page 0x67 (current plugin page — inflate will remap to PAGE_DECODE)
|
||
// Win3 = inflate page 0x6C (set by inflate_call.s before JP 0xC000)
|
||
mem.SetWindow(0, SRC_START_PAGE); // Will be set by inflate entry
|
||
mem.SetWindow(1, 0x00); // Don't care — inflate sets Win1
|
||
mem.SetWindow(2, WC_BASE_PAGE); // Current Win2 = plugin base
|
||
mem.SetWindow(3, INFLATE_PAGE); // inflate code
|
||
|
||
// Set up WC system variables at 0x6000–0x6003
|
||
// These are in the page mapped to Win1 at offset 0x2000
|
||
// Actually in the real system, Win1 maps some WC page.
|
||
// We need 0x6000–0x6003 readable. Let's put a known page in Win1
|
||
// that has these values. In WC, Win1 = page at sys var area.
|
||
// For simplicity: map Win1 to a scratch page, write sys vars there
|
||
byte sysVarPage = 0xFE; // use a spare page for sys vars
|
||
mem.SetWindow(1, sysVarPage);
|
||
// 0x6000 = offset 0x2000 in Win1
|
||
// Write the page values as WC would have them:
|
||
mem.GetPage(sysVarPage)[0x2000] = SRC_START_PAGE; // 0x6000 = Win0 page
|
||
mem.GetPage(sysVarPage)[0x2001] = sysVarPage; // 0x6001 = Win1 page
|
||
mem.GetPage(sysVarPage)[0x2002] = WC_BASE_PAGE; // 0x6002 = Win2 page (plugin base)
|
||
mem.GetPage(sysVarPage)[0x2003] = INFLATE_PAGE; // 0x6003 = Win3 page
|
||
|
||
Console.WriteLine($"Windows: W0=0x{SRC_START_PAGE:X2} W1=0x{sysVarPage:X2} W2=0x{WC_BASE_PAGE:X2} W3=0x{INFLATE_PAGE:X2}");
|
||
Console.WriteLine($"Sys vars @0x6000: [{SRC_START_PAGE:X2} {sysVarPage:X2} {WC_BASE_PAGE:X2} {INFLATE_PAGE:X2}]");
|
||
|
||
// Display first bytes of inflate at 0xC000
|
||
Console.Write("inflate @0xC000: ");
|
||
for (int i = 0; i < 16; i++)
|
||
Console.Write($"{mem.Read((ushort)(0xC000 + i)):X2} ");
|
||
Console.WriteLine();
|
||
}
|
||
|
||
static void SetupCpu()
|
||
{
|
||
Console.WriteLine();
|
||
Console.WriteLine("─── CPU Setup ───────────────────────────────────");
|
||
|
||
// Wire CPU bus to TSConfig memory
|
||
cpu.RDMEM = (addr) => mem.Read(addr);
|
||
cpu.RDMEM_M1 = (addr) => mem.Read(addr);
|
||
cpu.WRMEM = (addr, val) => mem.Write(addr, val);
|
||
cpu.RDPORT = (port) => 0xFF; // unused reads return 0xFF
|
||
cpu.WRPORT = HandlePortWrite;
|
||
cpu.RDNOMREQ = (addr) => { };
|
||
cpu.WRNOMREQ = (addr) => { };
|
||
cpu.RESET = () => { };
|
||
cpu.NMIACK_M1 = () => { };
|
||
cpu.INTACK_M1 = () => { };
|
||
cpu.SCANSIG = () => { };
|
||
|
||
// No interrupts during inflate
|
||
cpu.IFF1 = false;
|
||
cpu.IFF2 = false;
|
||
cpu.INT = false;
|
||
cpu.NMI = false;
|
||
cpu.RST = false;
|
||
cpu.IM = 1;
|
||
|
||
// Set up registers as inflate_call.s would before JP 0xC000:
|
||
// A = src start page (0xA1)
|
||
// E = dst start page (0x20)
|
||
// D = saved Win 2 page (0x67 — the plugin base page)
|
||
// SP = 0xFFF0
|
||
// Return address pushed on stack
|
||
cpu.regs.A = SRC_START_PAGE; // 0xA1
|
||
cpu.regs.E = DST_START_PAGE; // 0x20
|
||
cpu.regs.D = WC_BASE_PAGE; // 0x67 — saved pg2
|
||
cpu.regs.SP = 0xFFF0;
|
||
cpu.regs.PC = 0xC000;
|
||
|
||
// Push a sentinel return address (0x0000) so RET goes to addr 0,
|
||
// which we detect as "inflate returned"
|
||
cpu.regs.SP -= 2;
|
||
mem.Write(cpu.regs.SP, 0x00); // low byte
|
||
mem.Write((ushort)(cpu.regs.SP + 1), 0x00); // high byte
|
||
|
||
cpu.Tact = 0;
|
||
cpu.FX = CpuModeIndex.None;
|
||
cpu.XFX = CpuModeEx.None;
|
||
cpu.HALTED = false;
|
||
|
||
Console.WriteLine($"PC=0x{cpu.regs.PC:X4} SP=0x{cpu.regs.SP:X4}");
|
||
Console.WriteLine($"A=0x{cpu.regs.A:X2} D=0x{cpu.regs.D:X2} E=0x{cpu.regs.E:X2}");
|
||
Console.WriteLine($"Return sentinel at [0x{cpu.regs.SP:X4}] = 0x0000");
|
||
}
|
||
|
||
static void HandlePortWrite(ushort port, byte value)
|
||
{
|
||
// TSConfig page ports
|
||
if (mem.HandlePort(port, value))
|
||
{
|
||
// Log page changes (optional verbose)
|
||
return;
|
||
}
|
||
|
||
// Debug ports
|
||
switch (port)
|
||
{
|
||
case DBG_STATUS:
|
||
lastDbgStatus = value;
|
||
lastDbgCycle = cpu.Tact;
|
||
PrintDbgStatus(value);
|
||
break;
|
||
|
||
case DBG_SRC:
|
||
lastDbgSrc = value;
|
||
lastDbgCycle = cpu.Tact;
|
||
break;
|
||
|
||
case DBG_DST:
|
||
lastDbgDst = value;
|
||
lastDbgCycle = cpu.Tact;
|
||
break;
|
||
}
|
||
}
|
||
|
||
static readonly Dictionary<byte, string> StatusNames = new()
|
||
{
|
||
[0x0E] = "inflate entry",
|
||
[0x0F] = "stores done",
|
||
[0x10] = "Win0+Win2 remapped",
|
||
[0x11] = "Win2→PAGE_DECODE",
|
||
[0x12] = "gzip header OK",
|
||
[0x14] = "blocktype0 (stored)",
|
||
[0x15] = "blocktype1 (static)",
|
||
[0x16] = "blocktype2 (dynamic)",
|
||
[0x17] = "Huffman tables built → decode loop",
|
||
[0x20] = "page flush (bt0)",
|
||
[0x21] = "page flush (decode)",
|
||
[0x30] = "EXIT OK",
|
||
[0x31] = "FAIL",
|
||
[0xA6] = "bt2: HLIT/HDIST/HCLEN read",
|
||
[0xA7] = "bt2: makeHuffmanTable(19) done",
|
||
[0xA8] = "bt2: decodeProtoHuffman done",
|
||
[0xA9] = "bt2: code_length_orders read → mht",
|
||
[0xB1] = "mht: count loop done",
|
||
[0xB2] = "mht: start codes done",
|
||
[0xB3] = "mht: code assignment done",
|
||
[0xBF] = "decodeProtoHuffman entry",
|
||
[0xC0] = "dph: loop iteration",
|
||
[0xC1] = "dph: leaf found",
|
||
[0xC2] = "dph: special code",
|
||
};
|
||
|
||
static void PrintDbgStatus(byte code)
|
||
{
|
||
string name = StatusNames.TryGetValue(code, out var n) ? n : "???";
|
||
string extra = "";
|
||
// For codes that also emit SRC/DST, show them
|
||
if (code == 0xC1 || code == 0xC2)
|
||
extra = $" FEAF=0x{lastDbgSrc:X2}";
|
||
else if (code == 0x10 || code == 0x20 || code == 0x21 || code == 0x30)
|
||
extra = $" src_pg=0x{lastDbgSrc:X2} dst_pg=0x{lastDbgDst:X2}";
|
||
|
||
Console.ForegroundColor = code switch
|
||
{
|
||
0x30 => ConsoleColor.Green,
|
||
0x31 => ConsoleColor.Red,
|
||
>= 0xB0 and <= 0xBF => ConsoleColor.DarkGray, // mht internals
|
||
>= 0xC0 and <= 0xCF => ConsoleColor.DarkCyan, // dph internals
|
||
_ => ConsoleColor.Yellow
|
||
};
|
||
Console.WriteLine($" [{cpu.Tact,10}] DBG 0x{code:X2}: {name}{extra}");
|
||
Console.ResetColor();
|
||
}
|
||
|
||
static int RunCpu()
|
||
{
|
||
long cycleCount = 0;
|
||
|
||
while (!finished)
|
||
{
|
||
// ── Watchdog: execution outside Win3 ──
|
||
ushort pc = cpu.regs.PC;
|
||
if (pc < 0xC000)
|
||
{
|
||
if (pc == 0x0000)
|
||
{
|
||
// Sentinel return address — inflate returned!
|
||
Console.ForegroundColor = ConsoleColor.Green;
|
||
Console.WriteLine($"\n ✓ Inflate returned at cycle {cpu.Tact}, A=0x{cpu.regs.A:X2}");
|
||
Console.ResetColor();
|
||
if (cpu.regs.A == 0)
|
||
Console.WriteLine(" Result: SUCCESS (A=0, no carry)");
|
||
else
|
||
Console.WriteLine($" Result: FAIL (A=0x{cpu.regs.A:X2})");
|
||
|
||
finished = true;
|
||
DumpState("return");
|
||
return cpu.regs.A == 0 ? 0 : 2;
|
||
}
|
||
else
|
||
{
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
Console.WriteLine($"\n ✗ RUNAWAY: PC=0x{pc:X4} at cycle {cpu.Tact}");
|
||
Console.WriteLine($" Last DBG: 0x{lastDbgStatus:X2} at cycle {lastDbgCycle}");
|
||
Console.ResetColor();
|
||
DumpState("runaway");
|
||
return 3;
|
||
}
|
||
}
|
||
|
||
// ── Watchdog: cycles without debug port activity ──
|
||
if (cpu.Tact - lastDbgCycle > watchdogLimit && lastDbgCycle > 0)
|
||
{
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
Console.WriteLine($"\n ✗ WATCHDOG: {cpu.Tact - lastDbgCycle} cycles since last debug output");
|
||
Console.WriteLine($" Last DBG: 0x{lastDbgStatus:X2} at cycle {lastDbgCycle}");
|
||
Console.WriteLine($" PC=0x{cpu.regs.PC:X4} SP=0x{cpu.regs.SP:X4}");
|
||
Console.ResetColor();
|
||
PrintCpuState();
|
||
if (dumpOnHang)
|
||
DumpState("watchdog");
|
||
return 4;
|
||
}
|
||
|
||
// ── Watchdog: max total cycles ──
|
||
if (cpu.Tact > maxCycles)
|
||
{
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
Console.WriteLine($"\n ✗ MAX CYCLES ({maxCycles}) exceeded");
|
||
Console.ResetColor();
|
||
DumpState("maxcycles");
|
||
return 5;
|
||
}
|
||
|
||
// ── HALT detection ──
|
||
if (cpu.HALTED)
|
||
{
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
Console.WriteLine($"\n ✗ HALT at PC=0x{cpu.regs.PC:X4}, cycle {cpu.Tact}");
|
||
Console.ResetColor();
|
||
DumpState("halt");
|
||
return 6;
|
||
}
|
||
|
||
// ── Execute one instruction ──
|
||
cpu.ExecCycle();
|
||
cycleCount++;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
static void PrintCpuState()
|
||
{
|
||
var r = cpu.regs;
|
||
Console.WriteLine($" AF={r.AF:X4} BC={r.BC:X4} DE={r.DE:X4} HL={r.HL:X4}");
|
||
Console.WriteLine($" AF'={r._AF:X4} BC'={r._BC:X4} DE'={r._DE:X4} HL'={r._HL:X4}");
|
||
Console.WriteLine($" IX={r.IX:X4} IY={r.IY:X4} SP={r.SP:X4} PC={r.PC:X4}");
|
||
Console.WriteLine($" Win0=pg{mem.GetWindow(0):X2} Win1=pg{mem.GetWindow(1):X2} Win2=pg{mem.GetWindow(2):X2} Win3=pg{mem.GetWindow(3):X2}");
|
||
|
||
// Disassemble a few instructions around PC
|
||
Console.Write(" Code @PC: ");
|
||
for (int i = 0; i < 8; i++)
|
||
Console.Write($"{mem.Read((ushort)(r.PC + i)):X2} ");
|
||
Console.WriteLine();
|
||
|
||
// Stack dump
|
||
Console.Write(" Stack: ");
|
||
for (int i = 0; i < 8; i++)
|
||
Console.Write($"{mem.Read((ushort)(r.SP + i)):X2} ");
|
||
Console.WriteLine();
|
||
}
|
||
|
||
static void DumpState(string reason)
|
||
{
|
||
if (!dumpOnHang) return;
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(dumpDir);
|
||
|
||
// Dump inflate page (Win3)
|
||
string inflFile = Path.Combine(dumpDir, $"{reason}_inflate_page_{INFLATE_PAGE:X2}.bin");
|
||
File.WriteAllBytes(inflFile, mem.GetPage(INFLATE_PAGE));
|
||
Console.WriteLine($" Dumped: {inflFile}");
|
||
|
||
// Dump decode buffer page
|
||
string decFile = Path.Combine(dumpDir, $"{reason}_decode_page_{PAGE_DECODE:X2}.bin");
|
||
File.WriteAllBytes(decFile, mem.GetPage(PAGE_DECODE));
|
||
Console.WriteLine($" Dumped: {decFile}");
|
||
|
||
// Dump destination pages
|
||
for (int p = DST_START_PAGE; p < DST_START_PAGE + 16; p++)
|
||
{
|
||
byte[] pg = mem.GetPage(p);
|
||
bool allZero = true;
|
||
for (int i = 0; i < pg.Length && allZero; i++)
|
||
if (pg[i] != 0) allZero = false;
|
||
if (!allZero)
|
||
{
|
||
string dstFile = Path.Combine(dumpDir, $"{reason}_dst_page_{p:X2}.bin");
|
||
File.WriteAllBytes(dstFile, pg);
|
||
}
|
||
}
|
||
Console.WriteLine($" Dumped destination pages (0x{DST_START_PAGE:X2}+)");
|
||
|
||
// Dump Win3 area (the whole mapped page, including runtime modifications)
|
||
string win3File = Path.Combine(dumpDir, $"{reason}_win3_mapped.bin");
|
||
File.WriteAllBytes(win3File, mem.GetPage(mem.GetWindow(3)));
|
||
Console.WriteLine($" Dumped: {win3File} (Win3=page 0x{mem.GetWindow(3):X2})");
|
||
|
||
// CPU state dump
|
||
string stateFile = Path.Combine(dumpDir, $"{reason}_cpu_state.txt");
|
||
using (var sw = new StreamWriter(stateFile))
|
||
{
|
||
var r = cpu.regs;
|
||
sw.WriteLine($"Reason: {reason}");
|
||
sw.WriteLine($"Tact: {cpu.Tact}");
|
||
sw.WriteLine($"AF={r.AF:X4} BC={r.BC:X4} DE={r.DE:X4} HL={r.HL:X4}");
|
||
sw.WriteLine($"AF'={r._AF:X4} BC'={r._BC:X4} DE'={r._DE:X4} HL'={r._HL:X4}");
|
||
sw.WriteLine($"IX={r.IX:X4} IY={r.IY:X4} SP={r.SP:X4} PC={r.PC:X4}");
|
||
sw.WriteLine($"Win0=0x{mem.GetWindow(0):X2} Win1=0x{mem.GetWindow(1):X2} Win2=0x{mem.GetWindow(2):X2} Win3=0x{mem.GetWindow(3):X2}");
|
||
sw.WriteLine($"Last DBG status=0x{lastDbgStatus:X2} src=0x{lastDbgSrc:X2} dst=0x{lastDbgDst:X2}");
|
||
sw.WriteLine($"Last DBG at cycle {lastDbgCycle}");
|
||
sw.WriteLine();
|
||
|
||
// Disassemble around PC
|
||
sw.Write("Code @PC: ");
|
||
for (int i = 0; i < 32; i++)
|
||
sw.Write($"{mem.Read((ushort)(r.PC + i)):X2} ");
|
||
sw.WriteLine();
|
||
|
||
sw.Write("Stack: ");
|
||
for (int i = 0; i < 16; i++)
|
||
sw.Write($"{mem.Read((ushort)(r.SP + i)):X2} ");
|
||
sw.WriteLine();
|
||
}
|
||
Console.WriteLine($" Dumped: {stateFile}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.Error.WriteLine($" Dump error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
static void AnalyzeResults()
|
||
{
|
||
if (refPath == null || !File.Exists(refPath))
|
||
{
|
||
Console.WriteLine("No ref.vgm found, skipping comparison.");
|
||
return;
|
||
}
|
||
|
||
byte[] refData = File.ReadAllBytes(refPath);
|
||
Console.WriteLine($"ref.vgm: {refData.Length} bytes");
|
||
|
||
// Collect decompressed output from destination pages
|
||
int refPages = (refData.Length + TsMemory.PageSize - 1) / TsMemory.PageSize;
|
||
byte[] output = mem.ReadPages(DST_START_PAGE, refPages);
|
||
|
||
// Compare
|
||
int firstDiff = -1;
|
||
int matchBytes = 0;
|
||
int compareLen = Math.Min(refData.Length, output.Length);
|
||
|
||
for (int i = 0; i < compareLen; i++)
|
||
{
|
||
if (output[i] == refData[i])
|
||
matchBytes++;
|
||
else if (firstDiff < 0)
|
||
firstDiff = i;
|
||
}
|
||
|
||
if (firstDiff < 0 && matchBytes == refData.Length)
|
||
{
|
||
Console.ForegroundColor = ConsoleColor.Green;
|
||
Console.WriteLine($" ✓ PERFECT MATCH: {matchBytes} bytes identical to ref.vgm");
|
||
Console.ResetColor();
|
||
}
|
||
else
|
||
{
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
Console.WriteLine($" ✗ MISMATCH at offset 0x{firstDiff:X}");
|
||
Console.WriteLine($" Matched: {matchBytes}/{refData.Length} bytes ({100.0 * matchBytes / refData.Length:F1}%)");
|
||
Console.ResetColor();
|
||
|
||
if (firstDiff >= 0)
|
||
{
|
||
Console.Write(" ref: ");
|
||
for (int i = Math.Max(0, firstDiff - 4); i < Math.Min(refData.Length, firstDiff + 12); i++)
|
||
Console.Write(i == firstDiff ? $"[{refData[i]:X2}]" : $" {refData[i]:X2} ");
|
||
Console.WriteLine();
|
||
|
||
Console.Write(" got: ");
|
||
for (int i = Math.Max(0, firstDiff - 4); i < Math.Min(output.Length, firstDiff + 12); i++)
|
||
Console.Write(i == firstDiff ? $"[{output[i]:X2}]" : $" {output[i]:X2} ");
|
||
Console.WriteLine();
|
||
}
|
||
|
||
// Save output for analysis
|
||
try
|
||
{
|
||
Directory.CreateDirectory(dumpDir);
|
||
string outFile = Path.Combine(dumpDir, "output.vgm");
|
||
byte[] trimmed = new byte[refData.Length];
|
||
Array.Copy(output, trimmed, Math.Min(output.Length, trimmed.Length));
|
||
File.WriteAllBytes(outFile, trimmed);
|
||
Console.WriteLine($" Saved decompressed output: {outFile}");
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
}
|
||
}
|