Files
Roflin d83f7beef8 Fix the RX ring descriptor to the format libogc actually parses
The assembler wrote 00 00 len[11:8] len[7:0] with bytes 0 and 1 zero.  The real
format, read out of libogc's own BBA driver, is a PACKED 32-BIT LITTLE-ENDIAN
word: next_packet_ptr in bits 0-11, packet_len in bits 12-23, status in bits
24-31.  Under that driver our descriptor for a 342-byte frame decoded as a
12-byte frame with a next-pointer of 0 into the reserved page, so Swiss and
anything else built on libogc would have initialised perfectly and then
misparsed every received frame.

Found by disassembling /opt/devkitpro/libogc/lib/cube/libbba.a (gcif.o,
bba_interrupt) rather than by guessing:

  lwbrx   r9,0,r9              # 4 bytes as a little-endian word
  rlwinm  r4,r9,20,20,31       # packet_len = (word >> 12) & 0xFFF
  addi    r4,r4,-4             # payload = packet_len - 4
  cmpwi   r4,1540
  srwi    r9,r9,24             # status = word >> 24
  and.    r9,r9,r18            # r18 = 0x..84 -> errors are 0x80 | 0x04

and the low 12 bits are written straight back to RRP (0x0018/0x0019), which is
why next_packet_ptr has to be there at all -- it is the driver's read pointer.

The length semantics and addressing were already correct (descriptor at
page<<8, body at +4, length including the descriptor); only the packing was
wrong and next_packet_ptr was never written.

RXFrameAssembler now computes the next page BEFORE writing the descriptor
(CALC_NEXT, formerly ADVANCE_RWP, moved ahead of the backfill) and writes bytes
0-3 as WRITE_D0..WRITE_D3.

WRITE_D3 fixes a latent bug this exposed: WRITE_PLACEHOLDER issues four writes
but the first lands twice, so desc_base+3 was NEVER written.  That was harmless
while the length write covered byte 3.  As the status byte it would have been
whatever the previous frame left in that cell, and a stale 0x80 or 0x04 makes
the driver discard the frame.

Readers updated to decode identically: netboot (which also now takes RRP from
the descriptor's own next_packet_ptr, as libogc does, instead of recomputing
the page span) and t_dma, including its page map.  netboot's +0/+1 offset probe
is removed -- the underrun that shifted descriptors is fixed and confirmed on
console, and under a packed format a +1 read is not a shifted descriptor but
garbage.

The assembler's testbench now decodes with libogc_decode(), reproducing the
instruction sequence above, so the tests check the DRIVER'S algorithm rather
than our own packing.  Every module testbench and bba_top at EXI_HALF=1/2/8
pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 10:27:45 +02:00

773 lines
27 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];
/* How often the descriptor was found at each page offset. The assembler is
* supposed to put it at +0 always; hardware shows it sometimes landing at +1,
* varying frame to frame, so count both rather than assume either. */
static u32 s_desc_ok;
/* ACKs where TI never set -- the frame did not reach the W5100. */
static u32 s_tx_timeouts;
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[8];
u32 rpage, wpage, dlen, flen, addr;
u32 desc, nextp;
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, 8) < 0) return -1;
memcpy(s_last_desc, hdr, 4);
s_last_page = rpage;
/* The descriptor is a PACKED 32-BIT LITTLE-ENDIAN WORD -- the format
* libogc's own BBA driver reads, so this is what GC software expects:
*
* bits 0-11 next_packet_ptr : the PAGE the NEXT frame starts at
* bits 12-23 packet_len : total bytes, INCLUDING these 4
* bits 24-31 status : 0x80 and 0x04 are errors
*
* See the note in exi_bba/rx_frame_assembler.py for the disassembly this
* was taken from. The old reader assumed [00 00 len_hi len_lo] and
* probed offsets +0 and +1 to work around the tx-FIFO-underrun bug that
* shifted descriptors; that bug is fixed (confirmed on console, +1=0) and
* the probe is gone -- under the packed format a +1 read is not a shifted
* descriptor, just garbage. */
desc = (u32)hdr[0] | ((u32)hdr[1] << 8) |
((u32)hdr[2] << 16) | ((u32)hdr[3] << 24);
dlen = (desc >> 12) & 0xFFF;
nextp = desc & 0xFFF;
if (((desc >> 24) & 0x84) || 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;
}
s_desc_ok++;
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 to the descriptor's OWN next_packet_ptr, which is what
* libogc does, rather than recomputing the page span here. The writer
* put that value in the descriptor, so the two cannot disagree. */
{
u32 next = nextp;
if (next < 1 || next > 15) next = 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];
int i, ir = 0;
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);
/* THE FULL SEQUENCE, matching t_tx() which is proven on hardware.
* bba_tx_bytes() only streams bytes into the TXDATA FIFO -- it does NOT
* set the length or fire the transmit. An earlier version of this
* function called it alone, so every ACK was written into the FIFO and
* never put on the wire: the host retried block 0 twenty times, saw no
* ACK and gave up, while the GC reported frames arriving normally
* ("22 seen, 0 bad" against the sender's 20 retries).
*
* TWD must be written BEFORE streaming: that is what queues the length
* so TXFrameDrain can consume concurrently with the EXI writes. Without
* it the drain waits for a length that only arrives at the ST trigger and
* the frame has to fit the 16-deep FIFO whole -- the truncate-at-16-bytes
* bug. The NCRA ST write afterwards is the fallback trigger. */
bba_w8(REG_IR, 0xFF); /* clear TI/RI */
bba_w8(REG_TWD_LO, (u8)(sizeof(f) & 0xFF));
bba_w8(REG_TWD_HI, (u8)((sizeof(f) >> 8) & 0xFF));
if (bba_tx_bytes(f, sizeof(f)) < 0) return -1;
bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0);
/* Poll for TI WITHOUT sleeping. usleep(500) x 20 meant every ACK cost
* up to 10 ms even when the transmit succeeded immediately, and the
* transfer is round-trip bound -- that is 10 ms on every one of ~292
* blocks. A bba_r8 is itself tens of microseconds, so a bare loop polls
* often enough and finishes as soon as TI sets.
*
* Absence of TI is NOT treated as fatal: the frame may still have gone
* out (TI only means TXFrameDrain handed it to the W5100), and if it did
* not, the host's retransmit covers it. Blocking longer here would only
* slow the common case to protect against the rare one. */
/* Poll a FEW times, not 200. Each bba_r8 is a whole EXI transaction
* (lock, select, header, gap, read, deselect, unlock) at tens of
* microseconds, so 200 of them is ~12 ms burned on the ONE path the host
* is waiting on -- and it only burns them when TI is slow, which is
* exactly when the host is closest to timing out. That turns a slow
* transmit into a missed deadline instead of just a slow one.
*
* TI is not needed for correctness here: the host retransmits anything
* it does not get an ACK for, and the GC re-ACKs duplicates. A handful
* of polls is enough to count genuine failures in txto. */
for (i = 0; i < 8; i++) {
ir = bba_r8(REG_IR);
if (ir >= 0 && (ir & IR_TI))
return 0;
}
s_tx_timeouts++;
return -1;
}
/* 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;
/* Clear any stale interrupt flags, RBFI (bit 7) included: the gateware
* now reports a ring-full drop there, and we would rather see a fresh
* one than inherit a set bit from an earlier run. */
bba_w8(REG_IR, 0xFF);
/* 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;
/* Keep this to ONE line. The console holds about 25 lines and the
* failure diagnostics are what matter, so guidance text here just
* pushes them off the top of the screen. */
if (status) status("waiting for host (send-dol.py)...");
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 ok=%u, txto=%u)",
got ? "timeout mid-transfer" : "timeout: no match",
(unsigned)seen, (unsigned)bad,
(unsigned)s_desc_ok,
(unsigned)s_tx_timeouts);
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);
/* txto distinguishes the two reasons an ACK can go
* missing: TI never set (the frame did not reach the
* W5100) versus sent-but-lost. The host reports the
* retries; only the GC knows which kind they were. */
snprintf(line, sizeof(line),
"desc ok=%u bad=%u lapped=%u txto=%u",
(unsigned)s_desc_ok,
(unsigned)bad, (unsigned)lapped,
(unsigned)s_tx_timeouts);
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);
}