Skip to content

Commit

Permalink
Added input to CLI renderer and made the CLI renderer slightly faster
Browse files Browse the repository at this point in the history
  • Loading branch information
qkmaxware committed Jun 28, 2024
1 parent 2141bd6 commit 8876b7f
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 42 deletions.
95 changes: 69 additions & 26 deletions Gameboy.Player.Cli/CliPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class CliPlayer {

private bool CpuTrace;
private bool Benchmark;
private float Scale = 1;

private Gameboy gb;

Expand All @@ -23,46 +24,51 @@ public CliPlayer() {
/// <summary>
/// Main entrypoint for the program
/// </summary>
/// <param name="rom">Path to a rom file</param>
/// <param name="width">Width of the screen on the terminal (default is usually too large for default font)</param>
/// <param name="cpuTrace">Print out a log of executed CPU instructions</param>
/// <param name="benchmark">Record timing metrics for instructions and hardware components</param>
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);
}

/// <summary>
/// Start the emulator interface
/// </summary>
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();

// ---------------------------------------------------------------------------------------
Expand All @@ -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;
}
}
}
// ---------------------------------------------------------------------------------------

Expand Down
142 changes: 130 additions & 12 deletions Gameboy.Player.Cli/CliRenderer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Qkmaxware.Emulators.Gameboy.Hardware;
using System.Runtime.InteropServices;

namespace Qkmaxware.Emulators.Gameboy.Players;

Expand Down Expand Up @@ -38,11 +39,15 @@ public CliRendererCharacterSet(char white, char light, char medium, char dark) {
}

/// <summary>
/// Character set using only standard ASCII characters
/// Character set using only simple ASCII characters
/// </summary>
public static readonly CliRendererCharacterSet Ascii = new CliRendererCharacterSet(' ', '-', 'B', '@');
public static readonly CliRendererCharacterSet Ascii = new CliRendererCharacterSet(' ', '-', 'X', '@');
/// <summary>
/// Character set using Braille ASCII
/// Character set using only standard Density characters
/// </summary>
public static readonly CliRendererCharacterSet Density = new CliRendererCharacterSet(' ', '░', '▒', '▓');
/// <summary>
/// Character set using Braille characters
/// </summary>
public static readonly CliRendererCharacterSet Braille = new CliRendererCharacterSet(' ', '⢁', '⠳', '⣿');
}
Expand All @@ -52,50 +57,163 @@ public CliRendererCharacterSet(char white, char light, char medium, char dark) {
/// </summary>
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;
}
}

/// <summary>
/// Character set used for renderering
/// Character set used for rendering
/// </summary>
public CliRendererCharacterSet CharacterSet {get; set;}

/// <summary>
/// Window x coordinate
/// </summary>
public short WindowX {get; private set;}
/// <summary>
/// Window y coordinate
/// </summary>
public short WindowY {get; private set;}

/// <summary>
/// Create a new renderer with the given characters
/// </summary>
/// <param name="characters">drawing character set</param>
public CliRenderer(CliRendererCharacterSet characters) {
/// <param name="scale">scale of the renderer relative to normal dimensions</param>
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));

/// <summary>
/// Draw the bitmap to the console
/// </summary>
/// <param name="bmp">bitmap to draw</param>
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);
}
}
}

/// <summary>
/// Draw the bitmap to console using classic Console.Write and Console.WriteLine invocations
/// </summary>
/// <param name="bmp">bitmap to draw</param>
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;
}
}
}
}
}
11 changes: 11 additions & 0 deletions Gameboy.Player.Cli/Gameboy.Player.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
</PropertyGroup>
<PropertyGroup Condition="'$(IsWindows)'=='true'">
<DefineConstants>Windows</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(IsLinux)'=='true'">
<DefineConstants>Linux</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" />
</ItemGroup>
Expand Down
17 changes: 14 additions & 3 deletions Gameboy.Player.Godot/TextureRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading

0 comments on commit 8876b7f

Please sign in to comment.