From 2b4d9c0c7de6255fd325418e2bc637271706e191 Mon Sep 17 00:00:00 2001 From: YoshiRulz Date: Tue, 11 Oct 2022 09:59:04 +1000 Subject: [PATCH] Add debug utility to byteswap N64 roms --- src/BizHawk.Client.EmuHawk/MainForm.Debug.cs | 1 + .../debug/N64RomByteswapToolForm.cs | 96 +++++++++++++++++++ src/BizHawk.Common/EndiannessUtils.cs | 8 ++ .../N64RomByteswapper.cs | 16 ++++ .../EndiannessUtils/EndiannessUtilsTests.cs | 11 +++ 5 files changed, 132 insertions(+) create mode 100644 src/BizHawk.Client.EmuHawk/debug/N64RomByteswapToolForm.cs diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Debug.cs b/src/BizHawk.Client.EmuHawk/MainForm.Debug.cs index 7d3395e764..1823ab3c1c 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Debug.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Debug.cs @@ -56,6 +56,7 @@ namespace BizHawk.Client.EmuHawk { DropDownItems = { + new DebugVSystemChildItem(N64RomByteswapToolForm.TOOL_NAME, OpenTool) { RequiresLoadedRom = false }, new DebugVSystemChildItem(N64VideoSettingsFuzzToolForm.TOOL_NAME, OpenTool), }, }, diff --git a/src/BizHawk.Client.EmuHawk/debug/N64RomByteswapToolForm.cs b/src/BizHawk.Client.EmuHawk/debug/N64RomByteswapToolForm.cs new file mode 100644 index 0000000000..9630adb2db --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/debug/N64RomByteswapToolForm.cs @@ -0,0 +1,96 @@ +#nullable enable + +using System; +using System.IO; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.WinForms.Controls; + +namespace BizHawk.Client.EmuHawk.ForDebugging +{ + internal sealed class N64RomByteswapToolForm : ToolFormBase + { + public const string TOOL_NAME = "Manual N64 ROM Byteswapping Tool"; + + protected override string WindowTitleStatic + => TOOL_NAME; + + public N64RomByteswapToolForm() + { + SzTextBoxEx txtBaseFile = new() { Size = new(224, 23) }; + void ChooseBaseFile() + { + using OpenFileDialog ofd = new() { InitialDirectory = Config!.PathEntries.RomAbsolutePath() }; + this.ShowDialogAsChild(ofd); + txtBaseFile.Text = ofd.FileName; + } + ComboBox comboFormats = new() + { + Items = { "rare little-endian (.n64)", "byte-swapped (.v64)", "native (.z64)" }, + SelectedIndex = 2, + Size = new(160, 23), + }; + SzTextBoxEx txtTargetFile = new() { Size = new(224, 23) }; + void ChooseTargetFile() + { + using SaveFileDialog sfd = new() + { + DefaultExt = comboFormats.SelectedIndex switch + { + 0 => "n64", + 1 => "v64", + 2 => "z64", + _ => string.Empty + }, + InitialDirectory = Config!.PathEntries.RomAbsolutePath(), + }; + this.ShowDialogAsChild(sfd); + txtTargetFile.Text = sfd.FileName; + } + void DoConvert() + { + try + { + var rom = File.ReadAllBytes(txtBaseFile.Text); + switch (comboFormats.SelectedIndex) // can't have Action> (System.Buffers.SpanAction isn't suitable) or I'd be able to have a tiny switch expr >:( --yoshi + { + case 0: + N64RomByteswapper.ToN64LittleEndian(rom); + break; + case 1: + N64RomByteswapper.ToV64ByteSwapped(rom); + break; + case 2: + default: + N64RomByteswapper.ToZ64Native(rom); + break; + } + File.WriteAllBytes(txtTargetFile.Text, rom); + this.ModalMessageBox($"wrote {txtTargetFile.Text}\n{SHA1Checksum.ComputePrefixedHex(rom)}"); + } + catch (Exception e) + { + this.ModalMessageBox($"caught {e.GetType().Name}:\n{e}"); + } + } + ClientSize = new(320, 200); + SuspendLayout(); + Controls.Add(new SingleColumnFLP + { + Controls = + { + new LabelEx { Text = "convert" }, + new SingleRowFLP { Controls = { txtBaseFile, GenControl.Button("(browse)", width: 75, ChooseBaseFile) } }, + new LabelEx { Text = "from (assumes .z64 on fail)" }, + new SingleRowFLP { Controls = { new LabelEx { Text = "to" }, comboFormats } }, + new SingleRowFLP { Controls = { txtTargetFile, GenControl.Button("(browse)", width: 75, ChooseTargetFile) } }, + GenControl.Button("--> convert", width: 75, DoConvert), + } + }); + ResumeLayout(); + } + } +} diff --git a/src/BizHawk.Common/EndiannessUtils.cs b/src/BizHawk.Common/EndiannessUtils.cs index fd64c38d00..cb00446bb0 100644 --- a/src/BizHawk.Common/EndiannessUtils.cs +++ b/src/BizHawk.Common/EndiannessUtils.cs @@ -45,5 +45,13 @@ namespace BizHawk.Common for (var i = 0; i < ints.Length; i++) ints[i] = BinaryPrimitives.ReverseEndianness(ints[i]); #endif } + + /// swaps pairs of 16-bit words in-place: 0xAABBIIJJPPQQYYZZ <=> 0xIIJJAABBYYZZPPQQ + public static void MutatingShortSwap32(Span a) + { + // no need to optimise this further, it's only used in the manual byteswap tool and only between the two less common formats + MutatingByteSwap32(a); // calls can be in either order, though this order ensures the length-mod-4 assert is hit immediately + MutatingByteSwap16(a); + } } } diff --git a/src/BizHawk.Emulation.Common/N64RomByteswapper.cs b/src/BizHawk.Emulation.Common/N64RomByteswapper.cs index a802b1a0f0..dec57ae0e7 100644 --- a/src/BizHawk.Emulation.Common/N64RomByteswapper.cs +++ b/src/BizHawk.Emulation.Common/N64RomByteswapper.cs @@ -11,6 +11,22 @@ namespace BizHawk.Emulation.Common /// not actually magic, just always the same in commercial carts? https://n64brew.dev/wiki/ROM_Header works all the same private static readonly byte[] MAGIC_BYTES = { 0x80, 0x37, 0x12, 0x40 }; + /// ensures is in the rare little-endian (.n64) format, mutating it in-place if necessary + public static void ToN64LittleEndian(Span rom) + { + if (rom[0] == MAGIC_BYTES[0]) EndiannessUtils.MutatingByteSwap32(rom); // native (.z64) + else if (rom[0] == MAGIC_BYTES[1]) EndiannessUtils.MutatingShortSwap32(rom); // byte-swapped (.v64) + // else already rare little-endian .n64 + } + + /// ensures is in the byte-swapped (.v64) format, mutating it in-place if necessary + public static void ToV64ByteSwapped(Span rom) + { + if (rom[0] == MAGIC_BYTES[0]) EndiannessUtils.MutatingByteSwap16(rom); // native (.z64) + else if (rom[0] == MAGIC_BYTES[3]) EndiannessUtils.MutatingShortSwap32(rom); // rare little-endian .n64 + // else already byte-swapped (.v64) + } + /// ensures is in the native (.z64) format, mutating it in-place if necessary public static void ToZ64Native(Span rom) { diff --git a/src/BizHawk.Tests/Common/EndiannessUtils/EndiannessUtilsTests.cs b/src/BizHawk.Tests/Common/EndiannessUtils/EndiannessUtilsTests.cs index 0d96a33391..f1a35a841e 100644 --- a/src/BizHawk.Tests/Common/EndiannessUtils/EndiannessUtilsTests.cs +++ b/src/BizHawk.Tests/Common/EndiannessUtils/EndiannessUtilsTests.cs @@ -32,5 +32,16 @@ namespace BizHawk.Tests.Common EndiannessUtils.MutatingByteSwap32(a); Assert.IsTrue(a.SequenceEqual(b)); } + + [TestMethod] + public void TestShortSwap32() + { + var b = new byte[] { 0x45, 0x67, 0x01, 0x23, 0xCD, 0xEF, 0x89, 0xAB }.AsSpan(); + var a = b.ToArray().AsSpan(); + EndiannessUtils.MutatingShortSwap32(a); + Assert.IsTrue(a.SequenceEqual(expected)); + EndiannessUtils.MutatingShortSwap32(a); + Assert.IsTrue(a.SequenceEqual(b)); + } } }