From 8876b7f8af5a04d23a3ff60f2faddc58e4077f61 Mon Sep 17 00:00:00 2001 From: Colin Halseth Date: Thu, 27 Jun 2024 21:30:56 -0700 Subject: [PATCH] Added input to CLI renderer and made the CLI renderer slightly faster --- Gameboy.Player.Cli/CliPlayer.cs | 95 +++++++++---- Gameboy.Player.Cli/CliRenderer.cs | 142 +++++++++++++++++-- Gameboy.Player.Cli/Gameboy.Player.Cli.csproj | 11 ++ Gameboy.Player.Godot/TextureRenderer.cs | 17 ++- Gameboy/hardware/input/Input.cs | 21 +++ Readme.md | 2 +- 6 files changed, 246 insertions(+), 42 deletions(-) diff --git a/Gameboy.Player.Cli/CliPlayer.cs b/Gameboy.Player.Cli/CliPlayer.cs index 3fc7aee..1e9217f 100644 --- a/Gameboy.Player.Cli/CliPlayer.cs +++ b/Gameboy.Player.Cli/CliPlayer.cs @@ -9,6 +9,7 @@ public class CliPlayer { private bool CpuTrace; private bool Benchmark; + private float Scale = 1; private Gameboy gb; @@ -23,46 +24,51 @@ public CliPlayer() { /// /// Main entrypoint for the program /// + /// Path to a rom file + /// Width of the screen on the terminal (default is usually too large for default font) /// Print out a log of executed CPU instructions /// Record timing metrics for instructions and hardware components - static void Main(bool cpuTrace = false, bool benchmark = false) { + static void Main(string? rom = null, int width = 160, bool cpuTrace = false, bool benchmark = false) { CliPlayer player = new CliPlayer() { CpuTrace = cpuTrace, - Benchmark = benchmark + Benchmark = benchmark, + Scale = (float)width / (float)Gpu.LCD_WIDTH }; - player.Start(); + player.Start(rom); } /// /// Start the emulator interface /// - public void Start() { + public void Start(string? rom) { // --------------------------------------------------------------------------------------- // Browse for ROM // --------------------------------------------------------------------------------------- - FileBrowser browser = new FileBrowser(); FileInfo? gameFile = null; - while (gameFile is null) { - Console.Clear(); - browser.ToConsole(); + if (string.IsNullOrEmpty(rom)) { + FileBrowser browser = new FileBrowser(); + while (gameFile is null) { + Console.Clear(); + browser.ToConsole(); - var key = Console.ReadKey(); - switch (key.Key) { - case ConsoleKey.Enter: - gameFile = browser.Accept(); - break; - case ConsoleKey.UpArrow: - browser.PrevEntry(); - break; - case ConsoleKey.DownArrow: - browser.NextEntry(); - break; + var key = Console.ReadKey(); + switch (key.Key) { + case ConsoleKey.Enter: + gameFile = browser.Accept(); + break; + case ConsoleKey.UpArrow: + browser.PrevEntry(); + break; + case ConsoleKey.DownArrow: + browser.NextEntry(); + break; + } } + Console.Clear(); + } else { + gameFile = new FileInfo(rom); } - Console.Clear(); // --------------------------------------------------------------------------------------- - - CliRenderer renderer = new CliRenderer(CliRendererCharacterSet.Ascii); Gameboy gb = new Gameboy(); // --------------------------------------------------------------------------------------- @@ -85,22 +91,59 @@ public void Start() { // --------------------------------------------------------------------------------------- Cartridge cart = new Cartridge(File.ReadAllBytes(gameFile.FullName)); gb.LoadCartridge(cart); - var position = Console.GetCursorPosition(); bool running = true; Console.CancelKeyPress += new ConsoleCancelEventHandler((object? sender, ConsoleCancelEventArgs e) => { e.Cancel = true; Console.WriteLine(); - Console.WriteLine("SIGINT Recieved, shutting down emulator"); running = false; }); + Console.Title = cart.Info.title; + var renderer = new CliRenderer(CliRendererCharacterSet.Ascii, Scale); while (running) { - // TODO somehow handle user input without cancelling or pausing? + // Run CPU until flush gb.DispatchUntilBufferFlush(); + // Draw screen var metric = gb.PerformanceAnalyzer?.BeginMeasure(renderer); - Console.SetCursorPosition(position.Left, position.Top); renderer.ToConsole(gb.GPU.Canvas); metric?.Record(); + // Handle user input without cancelling or pausing? + gb.Input.ClearKeys(); + if (Console.KeyAvailable) { + var key = Console.ReadKey(true); // Read key without printing to console + switch (key.Key) { + //case ConsoleKey.UpArrow: + case ConsoleKey.W: + gb.Input.SetKeyState(KeyCodes.Up, true); + break; + //case ConsoleKey.DownArrow: + case ConsoleKey.S: + gb.Input.SetKeyState(KeyCodes.Down, true); + break; + //case ConsoleKey.LeftArrow: + case ConsoleKey.A: + gb.Input.SetKeyState(KeyCodes.Left, true); + break; + //case ConsoleKey.RightArrow: + case ConsoleKey.D: + gb.Input.SetKeyState(KeyCodes.Right, true); + break; + + case ConsoleKey.Enter: + gb.Input.SetKeyState(KeyCodes.Start, true); + break; + case ConsoleKey.Tab: + gb.Input.SetKeyState(KeyCodes.Select, true); + break; + + case ConsoleKey.Spacebar: + gb.Input.SetKeyState(KeyCodes.A, true); + break; + case ConsoleKey.Escape: + gb.Input.SetKeyState(KeyCodes.B, true); + break; + } + } } // --------------------------------------------------------------------------------------- diff --git a/Gameboy.Player.Cli/CliRenderer.cs b/Gameboy.Player.Cli/CliRenderer.cs index 37eb8ff..c50f8c0 100644 --- a/Gameboy.Player.Cli/CliRenderer.cs +++ b/Gameboy.Player.Cli/CliRenderer.cs @@ -1,4 +1,5 @@ using Qkmaxware.Emulators.Gameboy.Hardware; +using System.Runtime.InteropServices; namespace Qkmaxware.Emulators.Gameboy.Players; @@ -38,11 +39,15 @@ public CliRendererCharacterSet(char white, char light, char medium, char dark) { } /// - /// Character set using only standard ASCII characters + /// Character set using only simple ASCII characters /// - public static readonly CliRendererCharacterSet Ascii = new CliRendererCharacterSet(' ', '-', 'B', '@'); + public static readonly CliRendererCharacterSet Ascii = new CliRendererCharacterSet(' ', '-', 'X', '@'); /// - /// Character set using Braille ASCII + /// Character set using only standard Density characters + /// + public static readonly CliRendererCharacterSet Density = new CliRendererCharacterSet(' ', '░', '▒', '▓'); + /// + /// Character set using Braille characters /// public static readonly CliRendererCharacterSet Braille = new CliRendererCharacterSet(' ', '⢁', '⠳', '⣿'); } @@ -52,50 +57,163 @@ public CliRendererCharacterSet(char white, char light, char medium, char dark) { /// public class CliRenderer { + [StructLayout(LayoutKind.Sequential)] + struct Coord { + public short x, y; + public Coord(short x, short y) { + this.x = x; this.y = y; + } + }; + [StructLayout(LayoutKind.Explicit)] + struct CharInfo { + [FieldOffset(0)] public ushort Char; + [FieldOffset(2)] public short Attributes; + } + [StructLayout(LayoutKind.Sequential)] + struct Rectangle { + public short left, top, right, bottom; + public Rectangle(short left, short top, short right, short bottom) { + this.left = left; this.top = top; this.right = right; this.bottom = bottom; + } + } + /// - /// Character set used for renderering + /// Character set used for rendering /// public CliRendererCharacterSet CharacterSet {get; set;} + /// + /// Window x coordinate + /// + public short WindowX {get; private set;} + /// + /// Window y coordinate + /// + public short WindowY {get; private set;} + /// /// Create a new renderer with the given characters /// /// drawing character set - public CliRenderer(CliRendererCharacterSet characters) { + /// scale of the renderer relative to normal dimensions + public CliRenderer(CliRendererCharacterSet characters, float scale = 1) { this.CharacterSet = characters; + Console.Clear(); + Console.SetCursorPosition(0, 0); + Console.CursorVisible = false; + var position = Console.GetCursorPosition(); + this.WindowX = (short)position.Left; + this.WindowY = (short)position.Top; + this.scale = scale; + this.charsWidth = (short)(Gpu.LCD_WIDTH * scale); + this.charsHeight = (short)(Gpu.LCD_HEIGHT * scale); + chars = new CharInfo[charsHeight * charsWidth]; + changed = new bool[chars.Length]; + for (var i = 0; i < chars.Length; i++) { + chars[i].Char = CharacterSet.DarkCharacter; + chars[i].Attributes = colorset(Console.ForegroundColor, Console.BackgroundColor); + changed[i] = true; + } + prepareBuffer(); } + private static short colorset(ConsoleColor foreground, ConsoleColor background) + => (short)(foreground + ((short)background << 4)); + /// /// Draw the bitmap to the console /// /// bitmap to draw public void ToConsole(Bitmap bmp) { - for (var row = 0; row < Gpu.LCD_HEIGHT; row++) { - for (var col = 0; col < Gpu.LCD_WIDTH; col++) { + ToConsoleAsConsoleWrite(bmp); + } + + private float scale; + private short charsWidth; + private short charsHeight; + private CharInfo[] chars; + private bool[] changed; + private void clear() { + for (int i = 0; i < chars.Length; i++) { + chars[i].Char = CharacterSet.DarkCharacter; + } + } + private char getChar(short x, short y) { + var addr = charsWidth * y + x; + if (addr < 0 || addr >= chars.Length) + return '\0'; + return (char)chars[addr].Char; + } + private void setChar(short x, short y, char new_value) { + var xOnWindow = (short)MathF.Round(x * scale); + var yOnWindow = (short)MathF.Round(y * scale); + var addr = charsWidth * yOnWindow + xOnWindow; + if (addr < 0 || addr >= chars.Length) + return; + + var current_value = chars[addr].Char; + chars[addr].Char = new_value; // TODO value blend if multiple bmp pixels go same scaled pixel + if (new_value != current_value) { + changed[addr] = true; + } + } + private void fillChars(Bitmap bmp) { + // Fill arrays + for (short row = 0; row < Gpu.LCD_HEIGHT; row++) { + for (short col = 0; col < Gpu.LCD_WIDTH; col++) { switch (bmp[col, row]) { case ColourPallet.BackgroundDark: case ColourPallet.Object0Dark: case ColourPallet.Object1Dark: - Console.Write(CharacterSet.DarkCharacter); + setChar(col, row, CharacterSet.DarkCharacter); break; case ColourPallet.BackgroundMedium: case ColourPallet.Object0Medium: case ColourPallet.Object1Medium: - Console.Write(CharacterSet.MediumCharacter); + setChar(col, row, CharacterSet.MediumCharacter); break; case ColourPallet.BackgroundLight: case ColourPallet.Object0Light: case ColourPallet.Object1Light: - Console.Write(CharacterSet.LightCharacter); + setChar(col, row, CharacterSet.LightCharacter); break; case ColourPallet.BackgroundWhite: case ColourPallet.Object0White: case ColourPallet.Object1White: - Console.Write(CharacterSet.WhiteCharacter); + setChar(col, row, CharacterSet.WhiteCharacter); break; } } - Console.WriteLine(); + } + } + + private void prepareBuffer() { + for (short row = 0; row < charsHeight; row++) { + if (row != 0) + Console.WriteLine(); + for (short col = 0; col < charsWidth; col++) { + Console.Write(CharacterSet.DarkCharacter); + } + } + } + + /// + /// Draw the bitmap to console using classic Console.Write and Console.WriteLine invocations + /// + /// bitmap to draw + private void ToConsoleAsConsoleWrite(Bitmap bmp) { + fillChars(bmp); + + // Draw array + for (short row = 0; row < charsHeight; row++) { + for (short col = 0; col < charsWidth; col++) { + var addr = charsWidth * row + col; + if (changed[addr]) { + Console.SetCursorPosition(this.WindowX + col, this.WindowY + row); + Console.Write(getChar(col, row)); + changed[addr] = false; + } + } } } } \ No newline at end of file diff --git a/Gameboy.Player.Cli/Gameboy.Player.Cli.csproj b/Gameboy.Player.Cli/Gameboy.Player.Cli.csproj index 9c43e1d..7c6a195 100644 --- a/Gameboy.Player.Cli/Gameboy.Player.Cli.csproj +++ b/Gameboy.Player.Cli/Gameboy.Player.Cli.csproj @@ -7,6 +7,17 @@ enable + + true + true + + + Windows + + + Linux + + diff --git a/Gameboy.Player.Godot/TextureRenderer.cs b/Gameboy.Player.Godot/TextureRenderer.cs index f4eedd9..0f47cc3 100644 --- a/Gameboy.Player.Godot/TextureRenderer.cs +++ b/Gameboy.Player.Godot/TextureRenderer.cs @@ -92,16 +92,27 @@ public void LoadCartFromPath(string filepath) { try { LastCartPath = filepath; var cart = new Cartridge(File.ReadAllBytes(filepath)); - Stop(); - this.gb?.LoadCartridge(cart); + insertCart(cart); } catch (Exception e) { GD.PushError(e); } } public void LoadCart(Cartridge cart) { - Stop(); this.LastCartPath = null; + insertCart(cart); + } + + + public void insertCart(Cartridge cart) { + /* + 1. Check the manufacture code for 01 or 33h (if 33h, also check the new manufacture code for 01). + 2. Add together the characters of the game title. + 3. Look up the value of the sum in a big list of known sums. if it's found in one of the 65 first entries, the position in the list will be used as pointer in the list of palette setups. If the sum is found later in the list, then the 4th character of the game title is looked up in the corresponding column of a table of known 4th game-title-characters. The position in the table + 65 will be used as pointer in the list of palette setups. + 4.The given palette setup is decoded and the color scheme is set. + var hash = cart.Info.title.Select(character => (int)character).Sum(); + */ + Stop(); this.gb?.LoadCartridge(cart); } diff --git a/Gameboy/hardware/input/Input.cs b/Gameboy/hardware/input/Input.cs index 9f9bcc2..222e14b 100644 --- a/Gameboy/hardware/input/Input.cs +++ b/Gameboy/hardware/input/Input.cs @@ -60,6 +60,27 @@ public bool IsKeyDown(KeyCodes key){ return false; } + public void ClearKeys() { + KeyUp(KeyCodes.Up); + KeyUp(KeyCodes.Down); + KeyUp(KeyCodes.Left); + KeyUp(KeyCodes.Right); + + KeyUp(KeyCodes.Select); + KeyUp(KeyCodes.Start); + + KeyUp(KeyCodes.A); + KeyUp(KeyCodes.B); + } + + public void SetKeyState(KeyCodes keycode, bool isDown) { + if (isDown) { + KeyDown(keycode); + } else { + KeyUp(keycode); + } + } + public void KeyDown(KeyCodes keycode){ if(keycode == KeyCodes.Up){ rows[1] &= 0xB; diff --git a/Readme.md b/Readme.md index f4d1634..f7997a8 100644 --- a/Readme.md +++ b/Readme.md @@ -21,7 +21,7 @@ Automated unit tests for the emulated hardware. The main renderer for the emulated hardware. A Blazor app which runs the emulated console and displays the graphics as well as handles user input. Runs entirely in the browser thanks to WebAssembly. ### Gameboy.Player.Cli -An example renderer that prints the images to the terminal's console. Doesn't support any input. +An example renderer that prints the images to the terminal's console. Does technically have input, but I wouldn't recommend playing anything in this (rendering is too slow and input is inconsistent). ### Gameboy.Player.Godot A simple Godot 4 project running the emulated hardware. \ No newline at end of file