diff --git a/src/Chip8.Analysis/RomAnalyzer.cs b/src/Chip8.Analysis/RomAnalyzer.cs new file mode 100644 index 0000000..57fa0dc --- /dev/null +++ b/src/Chip8.Analysis/RomAnalyzer.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Linq; +using Chip8.Core; + +namespace Chip8.Analysis; + +public sealed class RomAnalyzer +{ + static readonly HashSet superChipMarkers = new() + { + InstructionKind.ScrollRight, + InstructionKind.ScrollLeft, + InstructionKind.ScrollDown, + InstructionKind.Exit, + InstructionKind.LowRes, + InstructionKind.HiRes, + InstructionKind.LoadBigFont, + InstructionKind.StoreFlags, + InstructionKind.LoadFlags, + }; + + static readonly HashSet xoChipMarkers = new() + { + InstructionKind.LoadLong, + InstructionKind.Plane, + InstructionKind.Audio, + InstructionKind.Pitch, + InstructionKind.SaveRange, + InstructionKind.LoadRange, + InstructionKind.ScrollUp, + }; + + public RomAnalyzer(ushort entry = InstructionDecoder.defaultBaseAddress, ushort baseAddress = InstructionDecoder.defaultBaseAddress) + { + this.entry = entry; + this.baseAddress = baseAddress; + } + + public ushort entry { get; } + public ushort baseAddress { get; } + + public RomProfile analyze(byte[] program) + { + var cfg = ControlFlowGraph.build(program, entry, baseAddress); + var callGraph = CallGraph.build(program, entry, baseAddress); + + var histogram = new Dictionary(); + var superFeatures = new SortedSet(); + var xoFeatures = new SortedSet(); + int drawCount = 0; + int callCount = 0; + int jumpCount = 0; + int skipCount = 0; + + foreach (var block in cfg.blocksInOrder) + { + foreach (var info in block.instructions) + { + histogram[info.kind] = histogram.TryGetValue(info.kind, out int value) ? value + 1 : 1; + + if (info.kind == InstructionKind.Draw) + { + drawCount++; + if (info.n == 0) + { + superFeatures.Add(InstructionKind.Draw); + } + } + else if (info.kind == InstructionKind.Call) + { + callCount++; + } + else if (info.kind == InstructionKind.Jump) + { + jumpCount++; + } + else if (info.category == InstructionCategory.Skip) + { + skipCount++; + } + + if (superChipMarkers.Contains(info.kind)) + { + superFeatures.Add(info.kind); + } + if (xoChipMarkers.Contains(info.kind)) + { + xoFeatures.Add(info.kind); + } + } + } + + bool usesXoChip = xoFeatures.Count > 0; + bool usesSuperChip = superFeatures.Count > 0; + Platform guessed = usesXoChip + ? Platform.XoChip + : usesSuperChip ? Platform.SuperChip : Platform.Chip8; + + return new RomProfile( + program.Length, + entry, + cfg.instructionCount, + cfg.blockCount, + callGraph.subroutineCount, + drawCount, + callCount, + jumpCount, + skipCount, + usesSuperChip, + usesXoChip, + guessed, + histogram, + superFeatures.ToList(), + xoFeatures.ToList()); + } +} diff --git a/src/Chip8.Analysis/RomProfile.cs b/src/Chip8.Analysis/RomProfile.cs new file mode 100644 index 0000000..939c63a --- /dev/null +++ b/src/Chip8.Analysis/RomProfile.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Chip8.Core; + +namespace Chip8.Analysis; + +public sealed class RomProfile +{ + public RomProfile( + int size, + ushort entry, + int instructionCount, + int blockCount, + int subroutineCount, + int drawCount, + int callCount, + int jumpCount, + int skipCount, + bool usesSuperChip, + bool usesXoChip, + Platform guessedPlatform, + IReadOnlyDictionary kindHistogram, + IReadOnlyList superChipFeatures, + IReadOnlyList xoChipFeatures) + { + this.size = size; + this.entry = entry; + this.instructionCount = instructionCount; + this.blockCount = blockCount; + this.subroutineCount = subroutineCount; + this.drawCount = drawCount; + this.callCount = callCount; + this.jumpCount = jumpCount; + this.skipCount = skipCount; + this.usesSuperChip = usesSuperChip; + this.usesXoChip = usesXoChip; + this.guessedPlatform = guessedPlatform; + this.kindHistogram = kindHistogram; + this.superChipFeatures = superChipFeatures; + this.xoChipFeatures = xoChipFeatures; + } + + public int size { get; } + public ushort entry { get; } + public int instructionCount { get; } + public int blockCount { get; } + public int subroutineCount { get; } + public int drawCount { get; } + public int callCount { get; } + public int jumpCount { get; } + public int skipCount { get; } + public bool usesSuperChip { get; } + public bool usesXoChip { get; } + public Platform guessedPlatform { get; } + public IReadOnlyDictionary kindHistogram { get; } + public IReadOnlyList superChipFeatures { get; } + public IReadOnlyList xoChipFeatures { get; } + + public int kindCount(InstructionKind kind) => + kindHistogram.TryGetValue(kind, out int value) ? value : 0; + + public string render() + { + var builder = new StringBuilder(); + builder.Append("rom profile\n"); + builder.Append(" size: ").Append(size).Append(" bytes\n"); + builder.Append(" entry: 0x").Append(entry.ToString("X4", CultureInfo.InvariantCulture)).Append('\n'); + builder.Append(" instructions: ").Append(instructionCount).Append('\n'); + builder.Append(" blocks: ").Append(blockCount).Append('\n'); + builder.Append(" subroutines: ").Append(subroutineCount).Append('\n'); + builder.Append(" draws: ").Append(drawCount).Append('\n'); + builder.Append(" calls: ").Append(callCount).Append('\n'); + builder.Append(" jumps: ").Append(jumpCount).Append('\n'); + builder.Append(" skips: ").Append(skipCount).Append('\n'); + builder.Append(" platform: ").Append(guessedPlatform).Append('\n'); + builder.Append(" histogram:\n"); + foreach (var pair in kindHistogram.OrderByDescending(entry => entry.Value).ThenBy(entry => entry.Key.ToString())) + { + builder.Append(" ").Append(pair.Key).Append(": ").Append(pair.Value).Append('\n'); + } + return builder.ToString(); + } +}