From c5280986f6028d2436cee52b9ab60c11fbd35724 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 14:45:48 +0200 Subject: [PATCH] feat(core): add ascii and ppm frame renderers --- src/Chip8.Core/AsciiScreen.cs | 87 +++++++++++++++++++++++++++++++ src/Chip8.Core/Export/PpmImage.cs | 71 +++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/Chip8.Core/AsciiScreen.cs create mode 100644 src/Chip8.Core/Export/PpmImage.cs diff --git a/src/Chip8.Core/AsciiScreen.cs b/src/Chip8.Core/AsciiScreen.cs new file mode 100644 index 0000000..2ecf47a --- /dev/null +++ b/src/Chip8.Core/AsciiScreen.cs @@ -0,0 +1,87 @@ +using System.Text; + +namespace Chip8.Core; + +public static class AsciiScreen +{ + public static string render(bool[] display, char on = '#', char off = ' ') + { + var builder = new StringBuilder((Machine.displayWidth + 1) * Machine.displayHeight); + + for (int row = 0; row < Machine.displayHeight; row++) + { + for (int col = 0; col < Machine.displayWidth; col++) + { + builder.Append(display[row * Machine.displayWidth + col] ? on : off); + } + builder.Append('\n'); + } + + return builder.ToString(); + } + + public static string renderFramed(bool[] display, char on = '#', char off = ' ') + { + var builder = new StringBuilder(); + string border = new string('-', Machine.displayWidth); + + builder.Append('+').Append(border).Append("+\n"); + for (int row = 0; row < Machine.displayHeight; row++) + { + builder.Append('|'); + for (int col = 0; col < Machine.displayWidth; col++) + { + builder.Append(display[row * Machine.displayWidth + col] ? on : off); + } + builder.Append("|\n"); + } + builder.Append('+').Append(border).Append("+\n"); + + return builder.ToString(); + } + + public static int litPixelCount(bool[] display) + { + int lit = 0; + foreach (bool pixel in display) + { + if (pixel) + { + lit++; + } + } + return lit; + } + + public static string render(Video video, string palette = " #*@") + { + var builder = new StringBuilder((video.width + 1) * video.height); + + for (int row = 0; row < video.height; row++) + { + for (int col = 0; col < video.width; col++) + { + builder.Append(palette[video.colorAt(col, row)]); + } + builder.Append('\n'); + } + + return builder.ToString(); + } + + public static int litPixelCount(Video video) + { + int lit = 0; + for (int row = 0; row < video.height; row++) + { + for (int col = 0; col < video.width; col++) + { + if (video.colorAt(col, row) != 0) + { + lit++; + } + } + } + return lit; + } +} diff --git a/src/Chip8.Core/Export/PpmImage.cs b/src/Chip8.Core/Export/PpmImage.cs new file mode 100644 index 0000000..e094fbd --- /dev/null +++ b/src/Chip8.Core/Export/PpmImage.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Text; + +namespace Chip8.Core.Export; + +public static class PpmImage +{ + public static byte[] fromMonochrome(bool[] pixels, int width, int height, Palette palette, int scale = 1) + { + if (pixels.Length < width * height) + { + throw new ArgumentException("pixel buffer is smaller than " + width + "x" + height, nameof(pixels)); + } + return build(width, height, scale, palette, (x, y) => pixels[y * width + x] ? 1 : 0); + } + + public static byte[] fromVideo(Video video, Palette palette, int scale = 1) => + build(video.width, video.height, scale, palette, video.colorAt); + + public static byte[] fromMachine(Machine machine, Palette palette, int scale = 1) + { + if (machine.platform == Platform.Chip8) + { + return fromMonochrome(machine.display, Machine.displayWidth, Machine.displayHeight, palette, scale); + } + return fromVideo(machine.video, palette, scale); + } + + public static void writeFile(string path, byte[] ppm) => File.WriteAllBytes(path, ppm); + + static byte[] build(int width, int height, int scale, Palette palette, Func colorAt) + { + if (scale < 1) + { + throw new ArgumentOutOfRangeException(nameof(scale), "scale must be at least 1"); + } + + int outWidth = width * scale; + int outHeight = height * scale; + byte[] header = Encoding.ASCII.GetBytes("P6\n" + outWidth + " " + outHeight + "\n255\n"); + + var output = new byte[header.Length + outWidth * outHeight * 3]; + Array.Copy(header, output, header.Length); + + int cursor = header.Length; + for (int y = 0; y < height; y++) + { + int rowStart = cursor; + for (int x = 0; x < width; x++) + { + Rgb color = palette.resolve(colorAt(x, y)); + for (int sx = 0; sx < scale; sx++) + { + output[cursor++] = color.r; + output[cursor++] = color.g; + output[cursor++] = color.b; + } + } + + int rowBytes = outWidth * 3; + for (int sy = 1; sy < scale; sy++) + { + Array.Copy(output, rowStart, output, cursor, rowBytes); + cursor += rowBytes; + } + } + + return output; + } +}