mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
338 lines
11 KiB
C
338 lines
11 KiB
C
/*
|
|
ARMSX — Skip BIOS (fast boot). See fastboot.h for the design and the failure policy.
|
|
*/
|
|
|
|
#include "fastboot.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "bus_init.h"
|
|
#include "exe.h"
|
|
#include "log.h"
|
|
#include "dev/cdrom/cdrom.h"
|
|
#include "dev/cdrom/disc.h"
|
|
#include "dev/ram.h"
|
|
|
|
/* A raw sector is 2352 bytes; the 2048 bytes of user data start at offset 24 (12 sync,
|
|
4 header, 8 subheader). Confirmed against disc.c's PVD probe, which finds the ISO system
|
|
identifier — user-data offset 8 — at sector byte 0x20. 24 + 8 = 32 = 0x20. */
|
|
#define FB_SECTOR_BYTES 2352
|
|
#define FB_USER_DATA_OFFSET 24
|
|
#define FB_USER_DATA_BYTES 2048
|
|
|
|
/* psx_disc_read() takes an LBA that includes the 150-sector lead-in: disc.c reads the PVD, ISO
|
|
sector 16, as MSF_TO_LBA(0, 2, 16) = 2*75 + 16 = 166. So ISO sector N is LBA N + 150. */
|
|
#define FB_LEAD_IN 150
|
|
#define FB_ISO_PVD_SECTOR 16
|
|
|
|
/* ISO9660 directory record layout, from the start of the record. */
|
|
#define FB_DR_LENGTH 0
|
|
#define FB_DR_EXTENT_LE 2
|
|
#define FB_DR_SIZE_LE 10
|
|
#define FB_DR_NAME_LEN 32
|
|
#define FB_DR_NAME 33
|
|
/* Root directory record, from the start of the PVD's user data. */
|
|
#define FB_PVD_ROOT_DR 156
|
|
|
|
/* Default OFF, matching the settings.toml template and Ps1Settings.fastBoot. All three must
|
|
agree: an existing settings.toml written before the key existed has no [cpu] fast_boot line,
|
|
so config.c never calls the setter and whatever sits here is what that install gets.
|
|
Skipping the BIOS is a shortcut, not a neutral speed-up — it boots the executable directly
|
|
at the shell entry, so anything the BIOS would have set up first is simply not there. That is
|
|
the wrong thing to hand someone by default; the authentic boot is. */
|
|
static int g_fastboot_enabled = 0;
|
|
static int g_fastboot_armed = 0;
|
|
|
|
void psx_fastboot_set_enabled(int enabled) {
|
|
g_fastboot_enabled = enabled ? 1 : 0;
|
|
/* Turning it on mid-session should still take effect on the NEXT boot rather than silently
|
|
waiting for a reset that may never come. */
|
|
g_fastboot_armed = g_fastboot_enabled;
|
|
}
|
|
|
|
int psx_fastboot_enabled(void) {
|
|
return g_fastboot_enabled;
|
|
}
|
|
|
|
void psx_fastboot_rearm(void) {
|
|
g_fastboot_armed = g_fastboot_enabled;
|
|
}
|
|
|
|
static char tolower_ascii(char c) {
|
|
return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
|
}
|
|
|
|
static uint32_t fb_read_le32(const uint8_t* p) {
|
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
|
}
|
|
|
|
/* Reads one ISO sector's 2048 user bytes. Returns 1 on success. */
|
|
static int fb_read_iso_sector(psx_disc_t* disc, uint32_t iso_lba, uint8_t* out) {
|
|
uint8_t sector[FB_SECTOR_BYTES];
|
|
|
|
if (!disc)
|
|
return 0;
|
|
|
|
if (!psx_disc_read(disc, iso_lba + FB_LEAD_IN, sector))
|
|
return 0;
|
|
|
|
memcpy(out, sector + FB_USER_DATA_OFFSET, FB_USER_DATA_BYTES);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
Compares an ISO9660 directory-record name against a plain filename, case-insensitively and
|
|
ignoring the ";1" version suffix ISO9660 appends. Both matter: discs store "SYSTEM.CNF;1",
|
|
and the BOOT= line's spelling of the executable does not reliably match the directory's case.
|
|
*/
|
|
static int fb_name_matches(const uint8_t* record_name, int record_len, const char* wanted) {
|
|
int i = 0;
|
|
|
|
for (; i < record_len; ++i) {
|
|
char c = (char)record_name[i];
|
|
|
|
if (c == ';')
|
|
break;
|
|
|
|
if (!wanted[i])
|
|
return 0;
|
|
|
|
if (tolower_ascii(c) != tolower_ascii(wanted[i]))
|
|
return 0;
|
|
}
|
|
|
|
return wanted[i] == '\0';
|
|
}
|
|
|
|
/* Walks the root directory looking for `name`. On success fills extent/size and returns 1. */
|
|
static int fb_find_in_root(psx_disc_t* disc, const char* name,
|
|
uint32_t* out_extent, uint32_t* out_size) {
|
|
uint8_t buffer[FB_USER_DATA_BYTES];
|
|
uint32_t root_extent, root_size, sector, sectors;
|
|
|
|
if (!fb_read_iso_sector(disc, FB_ISO_PVD_SECTOR, buffer))
|
|
return 0;
|
|
|
|
/* "CD001" at user-data offset 1 marks a valid volume descriptor. Without this check a
|
|
non-ISO disc walks arbitrary bytes as if they were directory records. */
|
|
if (memcmp(buffer + 1, "CD001", 5) != 0)
|
|
return 0;
|
|
|
|
root_extent = fb_read_le32(buffer + FB_PVD_ROOT_DR + FB_DR_EXTENT_LE);
|
|
root_size = fb_read_le32(buffer + FB_PVD_ROOT_DR + FB_DR_SIZE_LE);
|
|
|
|
if (!root_size || root_size > (16u * 1024u * 1024u))
|
|
return 0;
|
|
|
|
sectors = (root_size + FB_USER_DATA_BYTES - 1) / FB_USER_DATA_BYTES;
|
|
|
|
for (sector = 0; sector < sectors; ++sector) {
|
|
uint32_t offset = 0;
|
|
|
|
if (!fb_read_iso_sector(disc, root_extent + sector, buffer))
|
|
return 0;
|
|
|
|
while (offset < FB_USER_DATA_BYTES) {
|
|
const uint8_t* record = buffer + offset;
|
|
const uint8_t length = record[FB_DR_LENGTH];
|
|
uint8_t name_len;
|
|
|
|
/* A zero length means "no more records in this sector" — the rest is padding, and
|
|
the next record starts at the next sector boundary. */
|
|
if (length == 0)
|
|
break;
|
|
|
|
if (offset + length > FB_USER_DATA_BYTES)
|
|
break;
|
|
|
|
name_len = record[FB_DR_NAME_LEN];
|
|
|
|
if (name_len && (uint32_t)(FB_DR_NAME + name_len) <= length) {
|
|
if (fb_name_matches(record + FB_DR_NAME, name_len, name)) {
|
|
*out_extent = fb_read_le32(record + FB_DR_EXTENT_LE);
|
|
*out_size = fb_read_le32(record + FB_DR_SIZE_LE);
|
|
|
|
return (*out_size != 0);
|
|
}
|
|
}
|
|
|
|
offset += length;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Pulls the executable name out of SYSTEM.CNF's BOOT line, e.g.
|
|
|
|
BOOT = cdrom:\SLUS_005.94;1
|
|
|
|
Returns 1 and fills `out` with "SLUS_005.94". Tolerates the many spellings real discs use:
|
|
"BOOT=" with no spaces, lower-case "cdrom:", forward or back slashes, a leading slash or
|
|
none, and a missing ";1".
|
|
*/
|
|
static int fb_parse_boot_line(const char* text, size_t length, char* out, size_t out_size) {
|
|
size_t i;
|
|
|
|
for (i = 0; i + 4 < length; ++i) {
|
|
const char* p;
|
|
size_t written = 0;
|
|
|
|
if (!(text[i] == 'B' || text[i] == 'b'))
|
|
continue;
|
|
if (strncmp(text + i, "BOOT", 4) != 0 && strncmp(text + i, "boot", 4) != 0)
|
|
continue;
|
|
|
|
p = text + i + 4;
|
|
|
|
while ((size_t)(p - text) < length && (*p == ' ' || *p == '\t'))
|
|
++p;
|
|
if ((size_t)(p - text) >= length || *p != '=')
|
|
continue;
|
|
++p;
|
|
while ((size_t)(p - text) < length && (*p == ' ' || *p == '\t'))
|
|
++p;
|
|
|
|
/* Skip the "cdrom:" scheme and any leading slashes. */
|
|
if ((size_t)(p - text) + 6 <= length &&
|
|
(strncmp(p, "cdrom:", 6) == 0 || strncmp(p, "CDROM:", 6) == 0)) {
|
|
p += 6;
|
|
}
|
|
while ((size_t)(p - text) < length && (*p == '\\' || *p == '/'))
|
|
++p;
|
|
|
|
while ((size_t)(p - text) < length && written + 1 < out_size) {
|
|
const char c = *p;
|
|
|
|
if (c == ';' || c == '\r' || c == '\n' || c == ' ' || c == '\t' || c == '\0')
|
|
break;
|
|
|
|
out[written++] = c;
|
|
++p;
|
|
}
|
|
|
|
out[written] = '\0';
|
|
|
|
return written > 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int psx_fastboot_try(psx_cpu_t* cpu) {
|
|
uint8_t sector[FB_USER_DATA_BYTES];
|
|
char boot_name[64];
|
|
char cnf[FB_USER_DATA_BYTES + 1];
|
|
psx_disc_t* disc;
|
|
psx_exe_hdr_t hdr;
|
|
uint32_t extent = 0, size = 0, ram_offset, remaining, iso_sector;
|
|
uint8_t* dst;
|
|
|
|
if (!g_fastboot_armed || !cpu || !cpu->bus || !cpu->bus->cdrom || !cpu->bus->ram)
|
|
return 0;
|
|
|
|
/* Fires once. Disarm FIRST so that any failure below cannot re-enter on the next
|
|
instruction and retry the whole disc walk every cycle. */
|
|
g_fastboot_armed = 0;
|
|
|
|
disc = cpu->bus->cdrom->disc;
|
|
|
|
if (!disc)
|
|
return 0;
|
|
|
|
/* SYSTEM.CNF names the executable. A disc without one is not a standard game disc
|
|
(an audio CD, or a homebrew that boots differently) — let the BIOS handle it. */
|
|
if (!fb_find_in_root(disc, "SYSTEM.CNF", &extent, &size)) {
|
|
log_info("fastboot: no SYSTEM.CNF; booting the BIOS normally");
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (!fb_read_iso_sector(disc, extent, sector))
|
|
return 0;
|
|
|
|
memcpy(cnf, sector, FB_USER_DATA_BYTES);
|
|
cnf[FB_USER_DATA_BYTES] = '\0';
|
|
|
|
if (!fb_parse_boot_line(cnf, (size < FB_USER_DATA_BYTES) ? size : FB_USER_DATA_BYTES,
|
|
boot_name, sizeof(boot_name))) {
|
|
log_info("fastboot: SYSTEM.CNF has no usable BOOT line; booting the BIOS normally");
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (!fb_find_in_root(disc, boot_name, &extent, &size)) {
|
|
log_info("fastboot: '%s' not in the root directory; booting the BIOS normally", boot_name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Header first: it says where the image belongs and how much of it there is. */
|
|
if (!fb_read_iso_sector(disc, extent, sector))
|
|
return 0;
|
|
|
|
memcpy(&hdr, sector, sizeof(hdr));
|
|
|
|
if (memcmp(hdr.id, "PS-X EXE", 8) != 0) {
|
|
log_info("fastboot: '%s' is not a PS-X EXE; booting the BIOS normally", boot_name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
ram_offset = hdr.ramdest & 0x7fffffff;
|
|
|
|
/* Refuse anything that would land outside RAM rather than trusting the disc's header —
|
|
a truncated or corrupt image must not be able to write past the buffer. */
|
|
if (!hdr.filesz || ram_offset >= PSX_RAM_SIZE || hdr.filesz > (PSX_RAM_SIZE - ram_offset)) {
|
|
log_error("fastboot: '%s' header out of range (dest=%08x size=%u); booting normally",
|
|
boot_name, hdr.ramdest, hdr.filesz);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* The image starts after the 0x800-byte header, i.e. one sector in. */
|
|
dst = cpu->bus->ram->buf + ram_offset;
|
|
remaining = hdr.filesz;
|
|
iso_sector = extent + 1;
|
|
|
|
while (remaining) {
|
|
const uint32_t chunk = (remaining < FB_USER_DATA_BYTES) ? remaining : FB_USER_DATA_BYTES;
|
|
|
|
if (!fb_read_iso_sector(disc, iso_sector, sector)) {
|
|
log_error("fastboot: read failed %u bytes in; the BIOS has already been skipped, "
|
|
"so this boot cannot recover", hdr.filesz - remaining);
|
|
|
|
return 0;
|
|
}
|
|
|
|
memcpy(dst, sector, chunk);
|
|
|
|
dst += chunk;
|
|
remaining -= chunk;
|
|
++iso_sector;
|
|
}
|
|
|
|
/* The recompiler's decode cache may hold entries for these addresses from the BIOS's own
|
|
use of them. Without this the game runs stale handlers. */
|
|
psx_cpu_invalidate_range(cpu, ram_offset, hdr.filesz);
|
|
|
|
cpu->pc = hdr.ipc;
|
|
cpu->next_pc = cpu->pc + 4;
|
|
cpu->r[28] = hdr.igp;
|
|
|
|
if (hdr.ispb) {
|
|
cpu->r[29] = hdr.ispb + hdr.ispoff;
|
|
cpu->r[30] = cpu->r[29];
|
|
}
|
|
|
|
/* Deliberately log_info, not log_fatal. psx_exe_load() uses log_fatal for its register dump,
|
|
which bypasses log_level and spams the diagnostic file. */
|
|
log_info("fastboot: booted '%s' directly (PC=%08x SP=%08x GP=%08x, %u bytes at %08x)",
|
|
boot_name, cpu->pc, cpu->r[29], cpu->r[28], hdr.filesz, hdr.ramdest);
|
|
|
|
return 1;
|
|
}
|