Files
VGMPLAY.WMF/Debuger/Program.cs
T

675 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 { }
}
}
}
}