feat(analysis): add sprite scanner

This commit is contained in:
dualfroz committed 2026-08-25 19:30:05 +02:00
1 parent c9c9bbe68b
commit f7c48098ce
2 files changed
+172

No files matched your search

+76
View File
@@ -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) + ")";
}
+96
View File
@@ -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<SpriteCandidate> scan(byte[] program)
{
var traversal = FlowTraversal.explore(program, entry, baseAddress);
var ordered = traversal.decoded.Values.OrderBy(info => info.address).ToList();
var candidates = new List<SpriteCandidate>();
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<SpriteCandidate> scanDistinct(byte[] program)
{
var seen = new HashSet<ushort>();
var distinct = new List<SpriteCandidate>();
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<SpriteCandidate> candidates)
{
var builder = new StringBuilder();
foreach (var candidate in candidates)
{
builder.Append(candidate).Append('\n');
builder.Append(candidate.renderAscii());
}
return builder.ToString();
}
}