Introducing ROM (+ISX) loader
This commit is contained in:
parent
73c8245eff
commit
3dc19b5c9e
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -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,8 +173,21 @@ 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
|
||||
romFile = file;
|
||||
this.rom = rom;
|
||||
romFile = file;
|
||||
loc.put(this, "ctrl.filename", file.getName());
|
||||
updateTitle();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue