From 3547f0400be45e2fb5ec79ea139e20b454f37c1d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 27 Aug 2026 09:56:12 +0200 Subject: [PATCH] feat(cli): add headless CLI runner with PNG/ASCII output --- src/GameBoy.Cli/Program.cs | 114 +++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/GameBoy.Cli/Program.cs diff --git a/src/GameBoy.Cli/Program.cs b/src/GameBoy.Cli/Program.cs new file mode 100644 index 0000000..be9b0b0 --- /dev/null +++ b/src/GameBoy.Cli/Program.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using GameBoy; +using GameBoy.Util; + +namespace GameBoy.Cli +{ + internal static class Program + { + private const int MaxSerialFrames = 4000; + + private static int Main(string[] args) + { + if (args.Length == 0) + { + Console.Error.WriteLine("usage: GameBoy.Cli [--frames N] [--serial] [--png path] [--ascii] [--ascii-out path]"); + return 1; + } + + string romPath = args[0]; + int frames = 60; + bool waitSerial = false; + bool ascii = false; + string? pngPath = null; + string? asciiPath = null; + + for (int i = 1; i < args.Length; i++) + { + switch (args[i]) + { + case "--frames": + frames = int.Parse(args[++i]); + break; + case "--serial": + waitSerial = true; + break; + case "--png": + pngPath = args[++i]; + break; + case "--ascii": + ascii = true; + break; + case "--ascii-out": + asciiPath = args[++i]; + break; + } + } + + if (!File.Exists(romPath)) + { + Console.Error.WriteLine($"rom not found: {romPath}"); + return 1; + } + + var emulator = new Emulator(File.ReadAllBytes(romPath)); + Console.WriteLine($"loaded {emulator.Cartridge.Title} ({emulator.Cartridge.MapperName}, type 0x{emulator.Cartridge.CartridgeType:X2})"); + + if (waitSerial) + RunUntilSerial(emulator); + else + for (int f = 0; f < frames; f++) + emulator.RunFrame(); + + Console.WriteLine($"ran {emulator.TotalCycles} cycles"); + + if (waitSerial || emulator.SerialOutput.Length > 0) + Console.WriteLine($"serial: {emulator.SerialOutput.Trim()}"); + + if (pngPath != null) + { + PngImage.WriteGrayscale(pngPath, Emulator.ScreenWidth, Emulator.ScreenHeight, emulator.Framebuffer); + Console.WriteLine($"framebuffer written to {pngPath}"); + } + + if (ascii || asciiPath != null) + { + string art = AsciiRender(emulator.Framebuffer); + if (asciiPath != null) + File.WriteAllText(asciiPath, art); + if (ascii) + Console.WriteLine(art); + } + + return 0; + } + + private static void RunUntilSerial(Emulator emulator) + { + for (int f = 0; f < MaxSerialFrames; f++) + { + emulator.RunFrame(); + string output = emulator.SerialOutput; + if (output.Contains("Passed") || output.Contains("Failed")) + return; + } + } + + private static string AsciiRender(byte[] framebuffer) + { + char[] ramp = { ' ', '.', '+', '#' }; + var builder = new System.Text.StringBuilder(); + for (int y = 0; y < Emulator.ScreenHeight; y += 2) + { + for (int x = 0; x < Emulator.ScreenWidth; x += 2) + { + byte shade = framebuffer[y * Emulator.ScreenWidth + x]; + builder.Append(ramp[shade & 0x03]); + } + builder.Append('\n'); + } + return builder.ToString(); + } + } +}