From f7c48098ceb0c419f5ef2dccd421c7cf35d024b5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 19:30:05 +0200 Subject: [PATCH] feat(analysis): add sprite scanner --- src/Chip8.Analysis/SpriteCandidate.cs | 76 +++++++++++++++++++++ src/Chip8.Analysis/SpriteScanner.cs | 96 +++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/Chip8.Analysis/SpriteCandidate.cs create mode 100644 src/Chip8.Analysis/SpriteScanner.cs diff --git a/src/Chip8.Analysis/SpriteCandidate.cs b/src/Chip8.Analysis/SpriteCandidate.cs new file mode 100644 index 0000000..aaeca9e --- /dev/null +++ b/src/Chip8.Analysis/SpriteCandidate.cs @@ -0,0 +1,76 @@ +using System.Globalization; +using System.Text; + +namespace Chip8.Analysis; + +public sealed class SpriteCandidate +{ + public SpriteCandidate(ushort address, int rows, int bytesPerRow, byte[] bytes, ushort loadAddress, ushort drawAddress) + { + this.address = address; + this.rows = rows; + this.bytesPerRow = bytesPerRow; + this.bytes = bytes; + this.loadAddress = loadAddress; + this.drawAddress = drawAddress; + } + + public ushort address { get; } + public int rows { get; } + public int bytesPerRow { get; } + public byte[] bytes { get; } + public ushort loadAddress { get; } + public ushort drawAddress { get; } + + public int widthPixels => bytesPerRow * 8; + + public int setPixelCount + { + get + { + int count = 0; + foreach (byte value in bytes) + { + int bits = value; + while (bits != 0) + { + count += bits & 1; + bits >>= 1; + } + } + return count; + } + } + + public bool isBlank => setPixelCount == 0; + + public bool pixel(int row, int column) + { + int byteIndex = row * bytesPerRow + column / 8; + if (byteIndex >= bytes.Length) + { + return false; + } + int bit = 7 - column % 8; + return (bytes[byteIndex] & (1 << bit)) != 0; + } + + public string renderAscii(char on = '#', char off = '.') + { + var builder = new StringBuilder(); + for (int row = 0; row < rows; row++) + { + for (int column = 0; column < widthPixels; column++) + { + builder.Append(pixel(row, column) ? on : off); + } + builder.Append('\n'); + } + return builder.ToString(); + } + + public override string ToString() => + "sprite@0x" + address.ToString("X4", CultureInfo.InvariantCulture) + + " " + widthPixels + "x" + rows + + " (" + setPixelCount + " px, drawn at 0x" + drawAddress.ToString("X4", CultureInfo.InvariantCulture) + ")"; +} diff --git a/src/Chip8.Analysis/SpriteScanner.cs b/src/Chip8.Analysis/SpriteScanner.cs new file mode 100644 index 0000000..d7be671 --- /dev/null +++ b/src/Chip8.Analysis/SpriteScanner.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Chip8.Analysis; + +public sealed class SpriteScanner +{ + public SpriteScanner(ushort entry = InstructionDecoder.defaultBaseAddress, ushort baseAddress = InstructionDecoder.defaultBaseAddress) + { + this.entry = entry; + this.baseAddress = baseAddress; + } + + public ushort entry { get; } + public ushort baseAddress { get; } + + public IReadOnlyList scan(byte[] program) + { + var traversal = FlowTraversal.explore(program, entry, baseAddress); + var ordered = traversal.decoded.Values.OrderBy(info => info.address).ToList(); + + var candidates = new List(); + ushort? lastIndex = null; + ushort lastIndexAddress = 0; + + foreach (var info in ordered) + { + if (info.loadsIndex && info.indexValue.HasValue) + { + lastIndex = info.indexValue.Value; + lastIndexAddress = info.address; + continue; + } + + if (info.kind == InstructionKind.LoadFont || info.kind == InstructionKind.LoadBigFont || info.kind == InstructionKind.AddIndex) + { + lastIndex = null; + continue; + } + + if (info.kind == InstructionKind.Draw && lastIndex.HasValue) + { + int rows = info.spriteRows!.Value; + int bytesPerRow = info.n == 0 ? 2 : 1; + byte[] bytes = readBytes(program, lastIndex.Value, rows * bytesPerRow); + candidates.Add(new SpriteCandidate(lastIndex.Value, rows, bytesPerRow, bytes, lastIndexAddress, info.address)); + } + } + + return candidates; + } + + public IReadOnlyList scanDistinct(byte[] program) + { + var seen = new HashSet(); + var distinct = new List(); + foreach (var candidate in scan(program)) + { + if (seen.Add(candidate.address)) + { + distinct.Add(candidate); + } + } + return distinct; + } + + public SpriteCandidate scanRegion(byte[] program, ushort address, int rows, int bytesPerRow = 1) + { + byte[] bytes = readBytes(program, address, rows * bytesPerRow); + return new SpriteCandidate(address, rows, bytesPerRow, bytes, 0, 0); + } + + byte[] readBytes(byte[] program, ushort address, int count) + { + int offset = address - baseAddress; + var bytes = new byte[count]; + for (int index = 0; index < count; index++) + { + int source = offset + index; + bytes[index] = source >= 0 && source < program.Length ? program[source] : (byte)0; + } + return bytes; + } + + public string render(IReadOnlyList candidates) + { + var builder = new StringBuilder(); + foreach (var candidate in candidates) + { + builder.Append(candidate).Append('\n'); + builder.Append(candidate.renderAscii()); + } + return builder.ToString(); + } +}