Introducing ROM (+ISX) loader

This commit is contained in:
Guy Perfect 2020-08-02 12:30:05 -05:00
parent 73c8245eff
commit 3dc19b5c9e
4 changed files with 1514 additions and 4 deletions

View File

@ -35,6 +35,8 @@ core {
# File dialog
dialog {
ext_isx ISX modules (*.isx)
ext_vb Virtual Boy ROMs (*.vb)
load Load
load_rom Load ROM
load_rom_error Unable to load the selected ROM file.

251
src/desktop/app/ROM.java Normal file
View File

@ -0,0 +1,251 @@
package app;
// Java imports
import java.nio.charset.*;
import java.util.*;
// Cartridge ROM module
public class ROM {
// Instance fields
private byte[] data; // Binary data
private int format; // File format of loaded file
///////////////////////////////////////////////////////////////////////////
// Constants //
///////////////////////////////////////////////////////////////////////////
// Formats
public static final int RAW = 0;
public static final int ISX = 1;
///////////////////////////////////////////////////////////////////////////
// Classes //
///////////////////////////////////////////////////////////////////////////
// ISX code segments
private static class Code {
int address; // CPU address
int offset; // Position within file
int size; // Number of bytes
Code(int o, int a, int s) { address = a; offset = o; size = s; }
}
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
ROM(byte[] data) {
// Attempt to decode as ISX
try { isxDecode(data); return; } catch (Exception e) { }
// Attempt to decode as raw
try { rawDecode(data); return; } catch (Exception e) { }
// Unable to identify the file contents
throw new RuntimeException();
}
///////////////////////////////////////////////////////////////////////////
// Public Methods //
///////////////////////////////////////////////////////////////////////////
// Retrieve the game code from the ROM header
public String getGameCode() {
var bytes = new byte[4];
System.arraycopy(data, data.length - 517, bytes, 0, 4);
return new String(bytes, StandardCharsets.ISO_8859_1);
}
// Retrieve the file format of the loaded ROM file
public int getFormat() {
return format;
}
// Retrieve the maker code from the ROM header
public String getMaker() {
var bytes = new byte[2];
System.arraycopy(data, data.length - 519, bytes, 0, 2);
return new String(bytes, StandardCharsets.ISO_8859_1);
}
// Retrieve the number of bytes in the ROM data
public int getSize() {
return data.length;
}
// Retrieve the game title from the ROM header
public String getTitle() {
return ShiftJIS.decode(data, data.length - 544, 20).trim();
}
// Retrieve the version number from the ROM header
public int getVersion() {
return data[data.length - 513] & 0xFF;
}
// Produce a byte array containing the ROM contents
public byte[] toByteArray() {
return data;
}
///////////////////////////////////////////////////////////////////////////
// Private Methods //
///////////////////////////////////////////////////////////////////////////
// Decode the file data as ISX
private void isxDecode(byte[] data) {
var codes = isxParse(data); // ISX code segments
int head = -1; // Latest address in bottom half of ROM address space
int tail = -1; // Earliest address in upper half of ROM address space
// Process all code segments
for (var code : codes) {
int end = code.address + code.size - 1;
int start = code.address;
boolean isHead = end >= 0; // Lower half of CPU address range
boolean isTail = start < 0; // Upper half of CPU address range
// The segment spans the middle of the ROM address space
if (isHead && isTail) {
head = 0x7FFFFF;
tail = 0x800000;
break;
}
// Track the segment's position within the ROM address space
end &= 0x00FFFFFF;
start &= 0x00FFFFFF;
if (isHead && (head == -1 || head < end )) head = end;
if (isTail && (tail == -1 || tail > start)) tail = start;
}
// Determine the required ROM size
int size = 1024;
int minSize = (head == -1 ? 0 : head + 1) +
(tail == -1 ? 0 : 0x01000000 - tail);
if (minSize == 0 || tail == -1 && (minSize - 1 & minSize) != 0)
throw new RuntimeException();
for (; size < minSize; size <<= 1);
// Transfer the code segments into the ROM buffer
this.data = new byte[size];
for (var code : codes)
System.arraycopy(
data , code.offset,
this.data, code.address & size - 1,
code.size
);
// The ROM is valid
format = ISX;
}
// Parse the code records from the ISX data
private Code[] isxParse(byte[] data) {
int count;
int offset = 0;
var ret = new ArrayList<Code>();
// Check for an extended ISX header
if (readInt(data, 0, 3) == 0x585349) // ASCII "ISX"
offset = 32;
// Process records
while (offset != data.length)
switch (data[offset++] & 0xFF) {
// SNES code
case 0x01:
if ((data[offset++] & 0xFF) >= 0x80) // Bank
offset++; // BankHigh
offset += 2; // Address
offset += 2 + readInt(data, offset, 2); // Data
break;
// SNES range
case 0x03:
// Count, { Bank, StartAddress, EndAddress, Type }
offset += 2 + readInt(data, offset, 2) * 6;
break;
// SNES symbol
case 0x04:
count = readInt(data, offset, 2); offset += 2;
for (; count > 0; count--) // Name, Flags, Bank, Address
offset += 5 + readInt(data, offset, 1);
break;
// Virtual Boy code
case 0x11:
int address = readInt(data, offset, 4); offset += 4;
int size = readInt(data, offset, 4); offset += 4;
if ((address & 0x07000000) != 0x07000000 || size < 0 ||
(address & 0x00FFFFFF) + size > 0x01000000)
throw new RuntimeException();
ret.add(new Code(offset, address, size));
offset += size;
break;
// Virtual Boy range
case 0x13:
// Count, { StartAddress, EndAddress, Type }
offset += 2 + readInt(data, offset, 2) * 9;
break;
// Virtual Boy symbol
case 0x14:
count = readInt(data, offset, 2); offset += 2;
for (; count > 0; count--) // Name, Flags, Address
offset += 7 + readInt(data, offset, 1);
break;
// System
case 0x20: case 0x21: case 0x22:
offset += 4 + readInt(data, offset, 4); // Debug, undocumented
break;
// Invalid record type
default:
throw new RuntimeException();
}
return ret.toArray(new Code[ret.size()]);
}
// Decode the file as raw
private void rawDecode(byte[] data) {
// Validate length
if (
data.length < 1024 || // Exception handlers and ROM header
data.length > 0x00FFFFFF || // 24-bit bus width
(data.length & data.length - 1) != 0 // Must be a power of 2
) throw new RuntimeException();
// The ROM is valid
this.data = data;
format = RAW;
}
// Read an integer from a byte array
private static int readInt(byte[] data, int offset, int size) {
int ret = 0;
for (size--; size >= 0; size--)
ret = ret << 8 | data[offset + size] & 0xFF;
return ret;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@ class Window extends JFrame {
private App app; // Containing application
private int number; // Window number within application
private boolean only; // This is the only application window
private ROM rom; // Currently loaded ROM
private File romFile; // Currently loaded ROM file
// UI components
@ -146,9 +147,9 @@ class Window extends JFrame {
// Prompt the user to select a file
var dlgFile = new JFileChooser(pwd);
dlgFile.addChoosableFileFilter(new FileNameExtensionFilter(
"Virtual Boy ROMs (*.vb)", "vb"));
loc.get("dialog.ext_vb"), "vb"));
dlgFile.addChoosableFileFilter(new FileNameExtensionFilter(
"ISX modules (*.isx)", "isx"));
loc.get("dialog.ext_isx"), "isx"));
dlgFile.setAcceptAllFileFilterUsed(true);
dlgFile.setDialogTitle(loc.get("dialog.load_rom"));
int option = dlgFile.showDialog(this, loc.get("dialog.load"));
@ -161,7 +162,7 @@ class Window extends JFrame {
// Update the current directory
pwd = file.getParentFile();
// Attempt to load the ROM file
// Read the file
var data = Util.fileRead(file);
if (data == null) {
JOptionPane.showMessageDialog(this,
@ -172,7 +173,20 @@ class Window extends JFrame {
return;
}
// Process the ROM
ROM rom = null;
try { rom = new ROM(data); }
catch (Exception e) {
JOptionPane.showMessageDialog(this,
loc.get("dialog.load_rom_notvb"),
loc.get("dialog.load_rom"),
JOptionPane.ERROR_MESSAGE
);
return;
}
// Update the emulation state
this.rom = rom;
romFile = file;
loc.put(this, "ctrl.filename", file.getName());
updateTitle();