a16d6f0432
Hardware dump: "frame 1 (110 B) etype=6c86 at 12: 72 33 33 00 00 00 16 00".
33 33 00 00 00 16 is an IPv6 MLDv2 multicast MAC from the host, but it sits at
offset ONE, behind a leading 0x72 -- and 0x72 is 114, the low byte of the
W5100's MACRAW 2-byte length prefix. Hence 6c86 at offset 12: the last byte
of the source MAC and the first of 86DD, both one byte late.
An earlier dump in the SAME build read FF FF FF FF FF FF 00 2B, perfectly
aligned. Two frames, one build, different alignment -- that is a lapped read
of a partially overwritten page, not a systematic off-by-one. The alignment
fix is intact.
Two self-inflicted costs removed from the poll loop, whose speed is exactly
what decides whether the writer laps us:
- The 32-byte hex dump ran on EVERY frame, adding an EXI transaction per
iteration. The diagnostic was making the fault it was diagnosing worse.
It now runs only when a descriptor is actually rejected.
- RRP was re-read from the device every poll, costing another transaction
for a value we had just written ourselves. It is now tracked locally and
kept in step at the three places that move it.
That takes the per-frame cost from six EXI transactions to four. It does not
make the race winnable on a busy link -- the ring is 15 pages and has no
overflow protection -- but it stops me handing back a loop that is slower than
the one that produced the measurement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
681 lines
22 KiB
C
681 lines
22 KiB
C
#include "netboot.h"
|
|
#include "bba.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <malloc.h>
|
|
#include <unistd.h>
|
|
#include <ogc/lwp_watchdog.h>
|
|
#include <ogc/cache.h>
|
|
#include <fat.h>
|
|
#include <sdcard/gcsd.h>
|
|
|
|
/* ── frame layout ────────────────────────────────────────────────────────
|
|
* 0 dst MAC (6)
|
|
* 6 src MAC (6)
|
|
* 12 ethertype (2) = NB_ETHERTYPE
|
|
* 14 magic (4) = "GCUP"
|
|
* 18 opcode (1)
|
|
* 19 reserved (1)
|
|
* 20 seq (2, big endian)
|
|
* 22 nblocks (2, big endian)
|
|
* 24 len (2, big endian)
|
|
* 26 data (len)
|
|
*/
|
|
#define NB_HDR 26u
|
|
#define NB_MIN 60u /* ethernet minimum, padded */
|
|
|
|
static u8 s_our_mac[6];
|
|
static u8 s_peer_mac[6];
|
|
/* Last descriptor ring_next() looked at, kept for the error path: when a
|
|
* length is rejected, the four raw bytes are the whole diagnosis. */
|
|
static u8 s_last_desc[4];
|
|
static u8 s_last_dump[32];
|
|
static u32 s_last_page;
|
|
|
|
static u16 rd16(const u8 *p) { return ((u16)p[0] << 8) | p[1]; }
|
|
static void wr16(u8 *p, u16 v) { p[0] = (u8)(v >> 8); p[1] = (u8)v; }
|
|
|
|
/* ── RX: pull one frame out of the SPRAM ring ────────────────────────────
|
|
* The ring is pages 1-15 with a 4-byte descriptor at each page start whose
|
|
* length field INCLUDES the descriptor. RRP is ours to advance; the
|
|
* gateware does not act on it. */
|
|
/* Pages the writer is ahead of us, over the 15-page ring. */
|
|
static u32 ring_lag(u32 rpage, u32 wpage)
|
|
{
|
|
return (wpage >= rpage) ? (wpage - rpage) : (wpage + 15u - rpage);
|
|
}
|
|
|
|
/* Our own view of RRP. Re-reading it from the device every poll cost an EXI
|
|
* transaction for a value we had just written ourselves -- pure overhead in
|
|
* the one loop whose speed decides whether the writer laps us. */
|
|
static u32 s_rrp;
|
|
|
|
static int ring_next(u8 *buf, u32 cap)
|
|
{
|
|
u8 wp[2], hdr[4];
|
|
u32 rpage, wpage, dlen, flen, addr;
|
|
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
rpage = s_rrp;
|
|
wpage = ((u32)wp[1] << 8) | wp[0];
|
|
if (rpage == wpage) return 0; /* empty */
|
|
if (rpage < 1 || rpage > 15) return -2; /* pointer corrupt */
|
|
|
|
/* THE RING HAS NO OVERFLOW PROTECTION. Nothing in the gateware consumes
|
|
* RRP, so RXFrameAssembler keeps writing and laps us: the real BBA has
|
|
* RBFI for this and we do not. Reading from a page the writer has
|
|
* already reused yields a "descriptor" that is really frame data -- which
|
|
* is where 00 00 00 01 came from, not a dropped write.
|
|
*
|
|
* Our poll loop costs ~5 EXI transactions per frame (~200 us) while a
|
|
* 100 Mbit link delivers a 342-byte frame every ~30 us, and the ring is
|
|
* only 15 pages, so under continuous traffic we lose that race. Bail out
|
|
* once the writer is more than two thirds of the ring ahead: the data we
|
|
* would read is not trustworthy, and resyncing is strictly better than
|
|
* parsing someone else's bytes. */
|
|
if (ring_lag(rpage, wpage) > 10) return -4;
|
|
|
|
addr = rpage << 8;
|
|
if (bba_stream((u16)addr, hdr, 4) < 0) return -1;
|
|
memcpy(s_last_desc, hdr, 4);
|
|
s_last_page = rpage;
|
|
dlen = ((u32)hdr[2] << 8) | hdr[3];
|
|
if (dlen <= 4 || dlen > 1518 + 4) {
|
|
/* Only now is a hex dump worth an EXI transaction. Doing it for
|
|
* EVERY frame (as an earlier version did) added a round trip to the
|
|
* poll loop that is already losing the race against the writer --
|
|
* i.e. the diagnostic made the fault it was diagnosing worse. */
|
|
bba_stream((u16)addr, s_last_dump, sizeof(s_last_dump));
|
|
return -3;
|
|
}
|
|
flen = dlen - 4;
|
|
if (flen > cap) flen = cap;
|
|
|
|
/* Descriptor + frame are contiguous from the page start. Read both and
|
|
* drop the descriptor rather than streaming from a mid-page address, so
|
|
* one DMA read covers it. */
|
|
{
|
|
static u8 tmp[1536 + 4];
|
|
u32 want = dlen;
|
|
if (want > sizeof(tmp)) want = sizeof(tmp);
|
|
if (bba_stream((u16)addr, tmp, want) < 0) return -1;
|
|
memcpy(buf, tmp + 4, flen);
|
|
}
|
|
|
|
/* Advance RRP by the pages this frame occupied, wrapping 1..15. */
|
|
{
|
|
u32 pages = (dlen + 255) >> 8;
|
|
u32 next = ((rpage - 1 + pages) % 15) + 1;
|
|
if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1;
|
|
s_rrp = next;
|
|
}
|
|
return (int)flen;
|
|
}
|
|
|
|
static int send_ack(u16 seq)
|
|
{
|
|
u8 f[NB_MIN];
|
|
memset(f, 0, sizeof(f));
|
|
memcpy(f + 0, s_peer_mac, 6);
|
|
memcpy(f + 6, s_our_mac, 6);
|
|
wr16(f + 12, NB_ETHERTYPE);
|
|
f[14] = 'G'; f[15] = 'C'; f[16] = 'U'; f[17] = 'P';
|
|
f[18] = NB_OP_ACK;
|
|
wr16(f + 20, seq);
|
|
wr16(f + 22, 0);
|
|
wr16(f + 24, 0);
|
|
return bba_tx_bytes(f, sizeof(f));
|
|
}
|
|
|
|
/* Destination path carried in the DONE frame, so the host decides where the
|
|
* build lands rather than it being baked in here. */
|
|
static char s_dest[64] = "/gc_bba_test.dol";
|
|
|
|
const char *netboot_dest_path(void) { return s_dest; }
|
|
|
|
int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg))
|
|
{
|
|
static u8 frame[1536];
|
|
u8 *out = (u8 *)dst;
|
|
u32 got = 0, nblocks = 0, bad = 0, seen = 0, lapped = 0;
|
|
u16 expect = 0;
|
|
char line[80];
|
|
u64 t0;
|
|
int have_peer = 0;
|
|
|
|
/* Bring the device up deterministically rather than hoping someone else
|
|
* already did. Pressing Z straight after launch is the normal case, and
|
|
* then NOTHING has issued the NCRA reset that triggers the W5100's own
|
|
* init -- MACRAW is not running, no frames are stored, and this would sit
|
|
* in "timeout: no frames" for a reason that has nothing to do with the
|
|
* transfer. Swiss may have initialised it, or an earlier test may have,
|
|
* or neither; a reset here makes all three cases identical.
|
|
*
|
|
* The reset also clears the SPRAM ring pointers, which is what we want:
|
|
* a clean ring with RRP == RWP == page 1, so the first frame we see is
|
|
* genuinely the host's first block. */
|
|
if (status) status("resetting the adapter...");
|
|
if (bba_w8(REG_NCRA, NCRA_RESET) < 0) return -1;
|
|
{
|
|
int i, v = -1;
|
|
for (i = 0; i < 50; i++) {
|
|
v = bba_r8(REG_NCRA);
|
|
if (v >= 0 && !(v & NCRA_RESET)) break;
|
|
usleep(5000);
|
|
}
|
|
if (i == 50) {
|
|
if (status) status("NCRA reset never self-cleared");
|
|
return -1;
|
|
}
|
|
/* The W5100 re-inits behind that: RSTn pulse plus a 60.3 ms settle,
|
|
* done twice. Polling it sooner reads all-zeros and looks dead. */
|
|
usleep(400000);
|
|
}
|
|
/* Start receive. Without the SR bit the assembler never writes the
|
|
* ring, so frames arrive at the W5100 and go nowhere. */
|
|
if (bba_w8(REG_NCRA, NCRA_SR) < 0) return -1;
|
|
|
|
/* Establish a KNOWN ring state instead of trusting what we inherited.
|
|
* Swiss initialises the BBA before launching us and writes RRP itself,
|
|
* and an NCRA reset does not restore the register file's copy of either
|
|
* pointer -- their init values only apply at FPGA configuration. So
|
|
* RRP can legitimately be anything, including 0, which is outside the
|
|
* ring (pages 1-15) and used to abort the whole transfer.
|
|
*
|
|
* Setting RRP = RWP declares the ring empty and consistent, whatever it
|
|
* was, so the first frame we read is genuinely the host's first block. */
|
|
{
|
|
u8 wp[2];
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
if (wp[0] < 1 || wp[0] > 15) {
|
|
/* RWP is the gateware's own pointer, so this should not
|
|
* happen -- say so loudly rather than papering over it. */
|
|
if (status) { snprintf(line, sizeof(line),
|
|
"RWP=%u outside ring pages 1-15", wp[0]);
|
|
status(line); }
|
|
return -5;
|
|
}
|
|
if (bba_w8(REG_RRP_LO, wp[0]) < 0) return -1;
|
|
s_rrp = wp[0];
|
|
if (status) { snprintf(line, sizeof(line),
|
|
"ring synced at page %u", wp[0]); status(line); }
|
|
}
|
|
|
|
/* PAR0-5 survives the reset (it is the gateware's power-on MAC), and it
|
|
* is read AFTER the reset so the source address we put in ACKs is the
|
|
* one the adapter is actually filtering on. */
|
|
if (bba_read(REG_PAR0, s_our_mac, 6) < 0) return -1;
|
|
|
|
if (status) {
|
|
status("waiting for host (send-dol.py)...");
|
|
status("NOTE: quiet the link first, or other traffic laps the ring:");
|
|
status(" sudo ./scripts/gc-testlink.sh up <iface>");
|
|
}
|
|
t0 = gettime();
|
|
|
|
for (;;) {
|
|
int n = ring_next(frame, sizeof(frame));
|
|
if (n == -4) {
|
|
/* Lapped. Skip to the writer and carry on; with stop-and-wait
|
|
* the host simply retransmits the block we just threw away. */
|
|
lapped++;
|
|
if (lapped <= 3 && status)
|
|
status("ring lapped (too much other traffic) - resyncing");
|
|
{
|
|
u8 wp[2];
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
if (wp[0] >= 1 && wp[0] <= 15) {
|
|
bba_w8(REG_RRP_LO, wp[0]);
|
|
s_rrp = wp[0];
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (n == -3) {
|
|
/* A descriptor we cannot make sense of must NOT kill the
|
|
* transfer. The ring collects EVERYTHING on the wire --
|
|
* mDNS, IPv6 RAs, ARP from the host -- so pressing Z on a
|
|
* live network hits stray frames long before the sender
|
|
* starts. Log the first few, resync, and carry on. */
|
|
bad++;
|
|
if (bad <= 3 && status) {
|
|
int i;
|
|
snprintf(line, sizeof(line),
|
|
"skip bad descriptor at page %u: %02X %02X %02X %02X",
|
|
(unsigned)s_last_page, s_last_desc[0],
|
|
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
|
|
status(line);
|
|
for (i = 0; i < 32; i += 16) {
|
|
snprintf(line, sizeof(line),
|
|
" +%02X %02X %02X %02X %02X %02X %02X %02X %02X "
|
|
"%02X %02X %02X %02X %02X %02X %02X %02X", i,
|
|
s_last_dump[i+0], s_last_dump[i+1], s_last_dump[i+2],
|
|
s_last_dump[i+3], s_last_dump[i+4], s_last_dump[i+5],
|
|
s_last_dump[i+6], s_last_dump[i+7], s_last_dump[i+8],
|
|
s_last_dump[i+9], s_last_dump[i+10], s_last_dump[i+11],
|
|
s_last_dump[i+12], s_last_dump[i+13], s_last_dump[i+14],
|
|
s_last_dump[i+15]);
|
|
status(line);
|
|
}
|
|
}
|
|
if (bad > 200) {
|
|
if (status) status("too many bad descriptors - giving up");
|
|
return -2;
|
|
}
|
|
/* Resync to the gateware's own pointer and keep waiting. */
|
|
{
|
|
u8 wp[2];
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
if (wp[0] >= 1 && wp[0] <= 15) {
|
|
bba_w8(REG_RRP_LO, wp[0]);
|
|
s_rrp = wp[0];
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (n < 0) {
|
|
/* Report the pointers, not just the code: -1 is an EXI access
|
|
* failure, -2 an RRP outside the ring, -3 a descriptor whose
|
|
* length is implausible. Which one it is, and what the
|
|
* pointers read, is the whole diagnosis. */
|
|
u8 rp[2] = {0xFF, 0xFF}, wp[2] = {0xFF, 0xFF};
|
|
bba_read(REG_RRP_LO, rp, 2);
|
|
bba_read(REG_RWP_LO, wp, 2);
|
|
if (status) {
|
|
snprintf(line, sizeof(line),
|
|
"ring error %d (RRP=%u RWP=%u) %s", n, rp[0], wp[0],
|
|
n == -1 ? "EXI access failed" :
|
|
n == -2 ? "RRP outside pages 1-15" :
|
|
"descriptor length implausible");
|
|
status(line);
|
|
snprintf(line, sizeof(line),
|
|
" page %u descriptor = %02X %02X %02X %02X",
|
|
(unsigned)s_last_page, s_last_desc[0],
|
|
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
|
|
status(line);
|
|
}
|
|
return -2;
|
|
}
|
|
if (n == 0) {
|
|
/* 30 s with nothing at all, or 5 s mid-transfer, is a bail-out
|
|
* rather than a hang -- a wedged update must not require a
|
|
* console power cycle. */
|
|
u32 idle = (u32)ticks_to_millisecs(diff_ticks(t0, gettime()));
|
|
if (idle > (got ? 5000u : 30000u)) {
|
|
if (status) {
|
|
snprintf(line, sizeof(line),
|
|
"%s (%u seen, %u bad desc, %u lapped)",
|
|
got ? "timeout mid-transfer" : "timeout: no match",
|
|
(unsigned)seen, (unsigned)bad, (unsigned)lapped);
|
|
status(line);
|
|
}
|
|
return -3;
|
|
}
|
|
continue;
|
|
}
|
|
seen++;
|
|
if ((u32)n < NB_HDR) continue;
|
|
if (rd16(frame + 12) != NB_ETHERTYPE) {
|
|
/* Dump the first non-matching frame. If the sender is running
|
|
* and nothing matches, the bytes show whether the body is
|
|
* offset -- the ethertype would appear at 11 or 13 instead of
|
|
* 12. */
|
|
if (seen == 1 && status) {
|
|
snprintf(line, sizeof(line),
|
|
"frame 1 (%d B) etype=%04X at 12; "
|
|
"%02X %02X %02X %02X %02X %02X %02X %02X",
|
|
n, rd16(frame + 12), frame[0], frame[1], frame[2],
|
|
frame[3], frame[4], frame[5], frame[6], frame[7]);
|
|
status(line);
|
|
}
|
|
continue;
|
|
}
|
|
if (frame[14] != 'G' || frame[15] != 'C' ||
|
|
frame[16] != 'U' || frame[17] != 'P') continue;
|
|
|
|
if (!have_peer) { memcpy(s_peer_mac, frame + 6, 6); have_peer = 1; }
|
|
t0 = gettime();
|
|
|
|
if (frame[18] == NB_OP_DONE) {
|
|
/* DONE may carry the destination path as its payload. */
|
|
u16 plen = rd16(frame + 24);
|
|
if (plen && plen < sizeof(s_dest) && NB_HDR + plen <= (u32)n) {
|
|
memcpy(s_dest, frame + NB_HDR, plen);
|
|
s_dest[plen] = 0;
|
|
}
|
|
if (status) { snprintf(line, sizeof(line),
|
|
"received %u bytes in %u blocks",
|
|
(unsigned)got, (unsigned)nblocks);
|
|
status(line); }
|
|
return (int)got;
|
|
}
|
|
if (frame[18] != NB_OP_DATA) continue;
|
|
|
|
{
|
|
u16 seq = rd16(frame + 20);
|
|
u16 len = rd16(frame + 24);
|
|
nblocks = rd16(frame + 22);
|
|
if (len > NB_BLOCK_MAX || NB_HDR + len > (u32)n) continue;
|
|
|
|
if (seq == expect) {
|
|
if (got + len > maxlen) {
|
|
if (status) status("image larger than buffer");
|
|
return -4;
|
|
}
|
|
memcpy(out + got, frame + NB_HDR, len);
|
|
got += len;
|
|
expect++;
|
|
if ((expect & 0x1F) == 0 && status) {
|
|
snprintf(line, sizeof(line), "block %u/%u (%u KB)",
|
|
(unsigned)expect, (unsigned)nblocks,
|
|
(unsigned)(got >> 10));
|
|
status(line);
|
|
}
|
|
}
|
|
/* ACK even a duplicate: it means our previous ACK was lost, and
|
|
* re-ACKing is what lets the host move on. */
|
|
send_ack(seq);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── DOL loader ──────────────────────────────────────────────────────────
|
|
* 7 text + 11 data sections, each with a file offset, load address and size,
|
|
* then BSS and the entry point. All big endian. */
|
|
typedef struct {
|
|
u32 text_off[7];
|
|
u32 data_off[11];
|
|
u32 text_addr[7];
|
|
u32 data_addr[11];
|
|
u32 text_len[7];
|
|
u32 data_len[11];
|
|
u32 bss_addr;
|
|
u32 bss_len;
|
|
u32 entry;
|
|
u32 pad[7];
|
|
} dolhdr_t;
|
|
|
|
/* Reserved region at the top of MEM1:
|
|
*
|
|
* [ staged image .............. ][ stub ][ descriptor ][ new stack ]
|
|
* ^ s_stage ^ s_stub
|
|
*
|
|
* Nothing here may overlap any DOL section, and netboot_launch() checks that
|
|
* before it jumps rather than trusting it. */
|
|
#define NB_STAGE_MAX (6u << 20) /* generous: Swiss is ~2-3 MB */
|
|
#define NB_TAIL (64u << 10) /* stub + descriptor + stack */
|
|
|
|
extern void nb_stub(void *desc);
|
|
extern void nb_stub_end(void);
|
|
|
|
static u8 *s_stage;
|
|
static u8 *s_tail;
|
|
|
|
void *netboot_reserve(u32 *out_max)
|
|
{
|
|
u32 hi = (u32)SYS_GetArena1Hi();
|
|
u32 base = (hi - (NB_STAGE_MAX + NB_TAIL)) & ~31u;
|
|
|
|
if (base <= (u32)SYS_GetArena1Lo()) return NULL;
|
|
s_stage = (u8 *)base;
|
|
s_tail = (u8 *)(base + NB_STAGE_MAX);
|
|
/* Tell libogc this is not its memory any more. */
|
|
SYS_SetArena1Hi(s_stage);
|
|
if (out_max) *out_max = NB_STAGE_MAX;
|
|
return s_stage;
|
|
}
|
|
|
|
static int overlaps(u32 a, u32 alen, u32 b, u32 blen)
|
|
{
|
|
if (!alen || !blen) return 0;
|
|
return a < b + blen && b < a + alen;
|
|
}
|
|
|
|
int netboot_launch(const void *dol)
|
|
{
|
|
const dolhdr_t *h = (const dolhdr_t *)dol;
|
|
const u8 *base = (const u8 *)dol;
|
|
u32 reserved = (u32)s_stage;
|
|
u32 reserved_len = NB_STAGE_MAX + NB_TAIL;
|
|
u32 stub_len = (u32)((const u8 *)nb_stub_end - (const u8 *)nb_stub);
|
|
u32 *desc;
|
|
void (*stub)(void *);
|
|
int i, nsec = 0;
|
|
|
|
if (!s_stage) return -1;
|
|
|
|
/* Refuse a mis-parsed or truncated header rather than jumping into it:
|
|
* a bad jump hangs the console hard enough to need a power cycle. */
|
|
if ((h->entry >> 24) != 0x80 || h->text_len[0] == 0 ||
|
|
(h->text_addr[0] >> 24) != 0x80)
|
|
return -2;
|
|
|
|
/* Build the copy table in the reserved tail, where the image cannot
|
|
* reach it. While doing so, verify no section lands on the reserved
|
|
* region -- that would clobber the staged image, the stub we are about
|
|
* to run from, or this very table. */
|
|
desc = (u32 *)(s_tail + 0x1000);
|
|
for (i = 0; i < 7; i++) {
|
|
if (!h->text_len[i]) continue;
|
|
if (overlaps(h->text_addr[i], h->text_len[i], reserved, reserved_len))
|
|
return -3;
|
|
desc[5 + nsec * 3 + 0] = (u32)(base + h->text_off[i]);
|
|
desc[5 + nsec * 3 + 1] = h->text_addr[i];
|
|
desc[5 + nsec * 3 + 2] = h->text_len[i];
|
|
nsec++;
|
|
}
|
|
for (i = 0; i < 11; i++) {
|
|
if (!h->data_len[i]) continue;
|
|
if (overlaps(h->data_addr[i], h->data_len[i], reserved, reserved_len))
|
|
return -3;
|
|
desc[5 + nsec * 3 + 0] = (u32)(base + h->data_off[i]);
|
|
desc[5 + nsec * 3 + 1] = h->data_addr[i];
|
|
desc[5 + nsec * 3 + 2] = h->data_len[i];
|
|
nsec++;
|
|
}
|
|
if (h->bss_len &&
|
|
overlaps(h->bss_addr, h->bss_len, reserved, reserved_len))
|
|
return -3;
|
|
|
|
desc[0] = h->entry;
|
|
/* Hand the new image a stack inside the reserved tail, 64-byte aligned
|
|
* and clear of the descriptor. */
|
|
desc[1] = ((reserved + reserved_len) - 64) & ~63u;
|
|
desc[2] = (u32)nsec;
|
|
desc[3] = h->bss_addr;
|
|
desc[4] = h->bss_len;
|
|
|
|
/* Relocate the copier out of harm's way and make it executable there. */
|
|
stub = (void (*)(void *))s_tail;
|
|
memcpy(s_tail, (const void *)nb_stub, stub_len);
|
|
DCFlushRange(s_tail, NB_TAIL);
|
|
ICInvalidateRange(s_tail, NB_TAIL);
|
|
|
|
/* No interrupts from here on: we are about to overwrite the memory this
|
|
* program is running from, and the scheduler must not preempt us into
|
|
* code that no longer exists. */
|
|
IRQ_Disable();
|
|
stub(desc);
|
|
return 0; /* not reached */
|
|
}
|
|
|
|
/* ── SD card: save the new build, then boot a known-good image ───────────
|
|
* The update deliberately does NOT launch what it just received. It writes
|
|
* the image to the card and then boots /boot.dol (Swiss), so a broken test
|
|
* build is recoverable: Swiss comes up regardless and can load the previous
|
|
* one. The only image we ever jump into is one that was already known good. */
|
|
|
|
static int s_sd_mounted;
|
|
|
|
static int sd_mount(void (*status)(const char *msg))
|
|
{
|
|
/* Try every interface a GameCube might have. Auto-detecting beats
|
|
* asking: SD Gecko can be in either memory-card slot, and SD2SP2 is on
|
|
* its own EXI channel. */
|
|
static const struct { const char *name; const DISC_INTERFACE *io; } ifs[] = {
|
|
{ "SD2SP2", &__io_gcsd2 },
|
|
{ "SD Gecko slot B", &__io_gcsdb },
|
|
{ "SD Gecko slot A", &__io_gcsda },
|
|
};
|
|
unsigned i;
|
|
char line[64];
|
|
|
|
if (s_sd_mounted) return 0;
|
|
for (i = 0; i < sizeof(ifs) / sizeof(ifs[0]); i++) {
|
|
if (fatMountSimple("sd", ifs[i].io)) {
|
|
if (status) {
|
|
snprintf(line, sizeof(line), "SD mounted (%s)", ifs[i].name);
|
|
status(line);
|
|
}
|
|
s_sd_mounted = 1;
|
|
return 0;
|
|
}
|
|
}
|
|
if (status) status("no SD card found (SD2SP2 or SD Gecko)");
|
|
return -1;
|
|
}
|
|
|
|
/* Build "<base>.tmp" and "<base>.prev.dol" from "<base>.dol".
|
|
*
|
|
* The temp file deliberately does NOT end in .dol: Swiss lists .dol files, and
|
|
* a half-written image that Swiss offers to boot is exactly what we are trying
|
|
* to prevent. The backup DOES end in .dol, because booting it from Swiss is
|
|
* the whole point of keeping it. */
|
|
static void derive_names(const char *path, char *tmp, size_t ntmp,
|
|
char *prev, size_t nprev)
|
|
{
|
|
char base[80];
|
|
size_t n = strlen(path);
|
|
|
|
if (n >= sizeof(base)) n = sizeof(base) - 1;
|
|
memcpy(base, path, n);
|
|
base[n] = 0;
|
|
if (n > 4 && strcasecmp(base + n - 4, ".dol") == 0)
|
|
base[n - 4] = 0;
|
|
|
|
snprintf(tmp, ntmp, "sd:%s.tmp", base);
|
|
snprintf(prev, nprev, "sd:%s.prev.dol", base);
|
|
}
|
|
|
|
int netboot_sd_write(const char *path, const void *buf, u32 len,
|
|
void (*status)(const char *msg))
|
|
{
|
|
char final[96], tmp[112], prev[112], line[160];
|
|
FILE *f;
|
|
u32 done;
|
|
int rc = 0;
|
|
|
|
if (sd_mount(status) < 0) return -1;
|
|
snprintf(final, sizeof(final), "sd:%s", path);
|
|
derive_names(path, tmp, sizeof(tmp), prev, sizeof(prev));
|
|
|
|
/* Write to a TEMP file and verify it BEFORE touching the existing
|
|
* build. Ordering matters: if the transfer, the write or the verify
|
|
* fails, the copy you are running from is still on the card untouched,
|
|
* so you can always try the update again. Only once the new image is
|
|
* known good do we rotate. */
|
|
if (status) { snprintf(line, sizeof(line), "writing %s", tmp); status(line); }
|
|
f = fopen(tmp, "wb");
|
|
if (!f) { if (status) status("open for write failed"); return -2; }
|
|
done = (u32)fwrite(buf, 1, len, f);
|
|
fclose(f);
|
|
if (done != len) {
|
|
remove(tmp);
|
|
if (status) status("short write - existing build left alone");
|
|
return -3;
|
|
}
|
|
|
|
/* Read it back. A half-written .dol that still parses is the failure
|
|
* this design exists to avoid, and the card is the one component here
|
|
* with no other integrity check. */
|
|
if (status) status("verifying...");
|
|
f = fopen(tmp, "rb");
|
|
if (!f) { remove(tmp); if (status) status("reopen for verify failed"); return -4; }
|
|
{
|
|
static u8 chunk[4096];
|
|
const u8 *src = (const u8 *)buf;
|
|
u32 off = 0;
|
|
while (off < len) {
|
|
u32 want = len - off;
|
|
if (want > sizeof(chunk)) want = sizeof(chunk);
|
|
if (fread(chunk, 1, want, f) != want) { rc = -5; break; }
|
|
if (memcmp(chunk, src + off, want) != 0) { rc = -6; break; }
|
|
off += want;
|
|
}
|
|
}
|
|
fclose(f);
|
|
if (rc < 0) {
|
|
remove(tmp);
|
|
if (status) status("VERIFY FAILED - existing build left alone");
|
|
return rc;
|
|
}
|
|
|
|
/* Rotate: keep exactly one generation back. remove()/rename() of a
|
|
* file that is not there is fine and ignored -- that is just the first
|
|
* update on a fresh card. */
|
|
remove(prev);
|
|
rename(final, prev);
|
|
if (rename(tmp, final) != 0) {
|
|
/* Should not happen, but if it does the old build is sitting in
|
|
* .prev.dol and is still bootable from Swiss -- say so rather
|
|
* than leaving the user to guess. */
|
|
if (status) {
|
|
snprintf(line, sizeof(line),
|
|
"rename failed; previous build is at %s", prev);
|
|
status(line);
|
|
}
|
|
return -7;
|
|
}
|
|
|
|
if (status) {
|
|
snprintf(line, sizeof(line), "wrote %u bytes; previous build kept as %s",
|
|
(unsigned)len, prev);
|
|
status(line);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int netboot_boot_file(const char *path, void *dst, u32 maxlen,
|
|
void (*status)(const char *msg))
|
|
{
|
|
char full[96], line[128];
|
|
FILE *f;
|
|
long sz;
|
|
u32 got;
|
|
|
|
if (sd_mount(status) < 0) return -1;
|
|
snprintf(full, sizeof(full), "sd:%s", path);
|
|
|
|
f = fopen(full, "rb");
|
|
if (!f) {
|
|
if (status) { snprintf(line, sizeof(line), "%s not found", full);
|
|
status(line); }
|
|
return -2;
|
|
}
|
|
fseek(f, 0, SEEK_END);
|
|
sz = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (sz <= 0 || (u32)sz > maxlen) {
|
|
fclose(f);
|
|
if (status) status("boot image size implausible");
|
|
return -3;
|
|
}
|
|
got = (u32)fread(dst, 1, (size_t)sz, f);
|
|
fclose(f);
|
|
if (got != (u32)sz) {
|
|
if (status) status("short read of boot image");
|
|
return -4;
|
|
}
|
|
|
|
if (status) { snprintf(line, sizeof(line), "booting %s (%u bytes)",
|
|
full, (unsigned)got); status(line); }
|
|
|
|
/* Unmount before we disable interrupts and jump: leaving the card
|
|
* mounted hands the next image a filesystem that libfat still thinks it
|
|
* owns, on an EXI channel it is about to reinitialise. */
|
|
fatUnmount("sd");
|
|
s_sd_mounted = 0;
|
|
|
|
return netboot_launch(dst);
|
|
}
|