4fd337fb24
The launcher committed in b438960 would have hung the console, and reviewing
it against Dennis's suggestion to test with Swiss's boot.dol is what surfaced
it -- Swiss is known-good, so a hang could only have been the launcher, but I
would not have been able to tell that from "the transfer is broken".
Two distinct defects, both from the same oversight -- a DOL's sections load
from 0x80003100 upward, which is exactly where this program is running:
1. The copier was an ordinary C function in .text, so it overwrote its OWN
instructions partway through the copy.
2. The image was staged in a memalign'd buffer, which the destination sections
can overwrite while it is still being read from.
Fixes:
- netboot_stub.S: the copy/zero/cache-flush/jump sequence in position
independent assembly, copied into reserved memory and executed THERE. It
has no globals, no literal pool, no TOC references and no calls, because
any of those would read from the address it was LINKED at rather than the
one it is RUNNING at. It also does the dcbf/icbi pass the C version was
doing through DCFlushRange -- without it the CPU executes stale icache
lines at the entry point.
- netboot_reserve(): carves the top of MEM1 and hands it to
SYS_SetArena1Hi() so neither libogc nor malloc allocates there. Called
FIRST in main(), before the framebuffer.
- netboot_launch(): verifies every text/data section AND bss against the
reserved region and refuses (-3) if any would land on it, instead of
corrupting the staged image or the copier mid-flight. Also hands the new
image a stack inside the reserved tail.
The app now tells you on screen to send Swiss's boot.dol first, and the README
explains why: the launcher and the transfer can each be broken independently
and a hang looks identical, so the first send should be an image whose success
is unmistakable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
257 lines
8.8 KiB
C
257 lines
8.8 KiB
C
/* re-bba-rb on-console test suite — GameCube
|
|
*
|
|
* Put the adapter in Serial Port 1, run this from Swiss.
|
|
*
|
|
* D-pad up/down move
|
|
* A open suite / rerun the selected test
|
|
* X run the selected suite
|
|
* START run everything
|
|
* Y clear all results
|
|
* B back
|
|
* Z self-update over the BBA, then relaunch (see netboot.c)
|
|
*/
|
|
#include <gccore.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <malloc.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include "tests.h"
|
|
#include "bba.h"
|
|
#include "netboot.h"
|
|
|
|
static void *xfb = NULL;
|
|
static void *s_stage = NULL; /* netboot staging region (top of MEM1) */
|
|
static u32 s_stage_max = 0;
|
|
static GXRModeObj *rmode = NULL;
|
|
|
|
#define C_RESET "\x1b[37;0m"
|
|
#define C_HEAD "\x1b[36;1m"
|
|
#define C_SEL "\x1b[30;47m" /* black on white */
|
|
#define C_PASS "\x1b[32;1m"
|
|
#define C_FAIL "\x1b[31;1m"
|
|
#define C_SKIP "\x1b[33;1m"
|
|
#define C_DIM "\x1b[37;0m"
|
|
|
|
static const char *st_tag(tstatus_t s)
|
|
{
|
|
switch (s) {
|
|
case T_PASS: return C_PASS "PASS" C_RESET;
|
|
case T_FAIL: return C_FAIL "FAIL" C_RESET;
|
|
case T_SKIP: return C_SKIP "SKIP" C_RESET;
|
|
default: return C_DIM " -- " C_RESET;
|
|
}
|
|
}
|
|
|
|
static void suite_tally(const suite_t *s, int *p, int *f, int *k)
|
|
{
|
|
int i; *p = *f = *k = 0;
|
|
for (i = 0; i < s->ntests; i++)
|
|
switch (s->tests[i].status) {
|
|
case T_PASS: (*p)++; break;
|
|
case T_FAIL: (*f)++; break;
|
|
case T_SKIP: (*k)++; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static void cls(void) { printf("\x1b[2J\x1b[1;1H"); }
|
|
|
|
static void banner(void)
|
|
{
|
|
printf(C_HEAD " re-bba-rb -- on-console test suite" C_RESET "\n");
|
|
printf(C_DIM " GameCube Serial Port 1 (EXI0:2), clock index %u\n" C_RESET,
|
|
(unsigned)bba_speed);
|
|
printf(" ------------------------------------------------------------\n");
|
|
}
|
|
|
|
static void draw_main(int sel, const char *status)
|
|
{
|
|
int i, p, f, k, tp, tf, tk, tpend;
|
|
cls();
|
|
banner();
|
|
for (i = 0; i < g_nsuites; i++) {
|
|
suite_tally(&g_suites[i], &p, &f, &k);
|
|
printf("%s %-14s" C_RESET " %s%2d" C_RESET "/%s%2d" C_RESET "/%s%2d" C_RESET
|
|
" %s%s\n",
|
|
i == sel ? C_SEL : " ", g_suites[i].name,
|
|
C_PASS, p, C_FAIL, f, C_SKIP, k,
|
|
C_DIM, g_suites[i].what);
|
|
}
|
|
tests_tally(&tp, &tf, &tk, &tpend);
|
|
printf(" ------------------------------------------------------------\n");
|
|
printf(" total " C_PASS "%d passed" C_RESET " " C_FAIL "%d failed" C_RESET
|
|
" " C_SKIP "%d skipped" C_RESET " %d not run\n", tp, tf, tk, tpend);
|
|
if (tf) {
|
|
printf(C_FAIL " failing:" C_RESET);
|
|
for (i = 0; i < g_nsuites; i++) {
|
|
int j;
|
|
for (j = 0; j < g_suites[i].ntests; j++)
|
|
if (g_suites[i].tests[j].status == T_FAIL)
|
|
printf(" %s/%s", g_suites[i].name, g_suites[i].tests[j].name);
|
|
}
|
|
printf("\n");
|
|
}
|
|
printf("\n" C_DIM " A open X run suite START run all Y clear Z net-update\n" C_RESET);
|
|
if (status && *status) printf("\n %s\n", status);
|
|
}
|
|
|
|
static void draw_suite(int si, int sel, const char *status)
|
|
{
|
|
suite_t *s = &g_suites[si];
|
|
int i, p, f, k;
|
|
cls();
|
|
banner();
|
|
suite_tally(s, &p, &f, &k);
|
|
printf(C_HEAD " %s" C_RESET " -- %s\n", s->name, s->what);
|
|
printf(" " C_PASS "%d passed" C_RESET " " C_FAIL "%d failed" C_RESET
|
|
" " C_SKIP "%d skipped" C_RESET " of %d\n\n", p, f, k, s->ntests);
|
|
for (i = 0; i < s->ntests; i++) {
|
|
printf("%s %s %-16s" C_RESET "\n",
|
|
i == sel ? C_SEL : " ", st_tag(s->tests[i].status), s->tests[i].name);
|
|
printf(C_DIM " %s\n" C_RESET, s->tests[i].what);
|
|
if (s->tests[i].msg[0])
|
|
printf(" -> %s\n", s->tests[i].msg);
|
|
}
|
|
printf("\n" C_DIM " A rerun this test X run whole suite B back\n" C_RESET);
|
|
if (status && *status) printf("\n %s\n", status);
|
|
}
|
|
|
|
/* Progress callback: netboot runs with the console as the only output, so
|
|
* print straight to it rather than buffering -- a stalled transfer should show
|
|
* where it stalled. */
|
|
static void nb_status(const char *msg)
|
|
{
|
|
printf(" %s\n", msg);
|
|
}
|
|
|
|
static void do_netboot(char *status, size_t nstatus)
|
|
{
|
|
int len, rc;
|
|
|
|
printf("\n" C_HEAD " Self-update over the BBA" C_RESET "\n\n");
|
|
printf(" On the host: sudo ./scripts/send-dol.py <iface> <file.dol>\n\n");
|
|
printf(C_DIM " Send a KNOWN-GOOD image the first time -- Swiss's boot.dol\n"
|
|
" is ideal: it is unmistakable when it boots, so it tells you the\n"
|
|
" transfer AND the launcher both work before you trust either\n"
|
|
" with an untested build.\n" C_RESET "\n");
|
|
printf(" Blocks until done or it times out (30 s idle, 5 s mid-xfer).\n\n");
|
|
|
|
if (!s_stage) {
|
|
snprintf(status, nstatus, "net-update: staging region unavailable");
|
|
return;
|
|
}
|
|
|
|
len = netboot_receive(s_stage, s_stage_max, nb_status);
|
|
if (len <= 0) {
|
|
snprintf(status, nstatus, "net-update failed (%d)", len);
|
|
return;
|
|
}
|
|
|
|
printf("\n launching %d bytes...\n", len);
|
|
/* Give the console a frame to actually show that before we disable
|
|
* interrupts and overwrite ourselves. */
|
|
VIDEO_WaitVSync();
|
|
VIDEO_WaitVSync();
|
|
|
|
rc = netboot_launch(s_stage);
|
|
/* Only reached on refusal. -2 = header implausible (truncated or not a
|
|
* DOL); -3 = a section would land on the reserved staging region, which
|
|
* would corrupt the image or the copier mid-flight. */
|
|
snprintf(status, nstatus,
|
|
rc == -3 ? "refused: image overlaps staging region"
|
|
: "refused: not a plausible DOL");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int screen = 0; /* 0 = main, 1 = suite detail */
|
|
int sel = 0, ssel = 0, cur = 0;
|
|
char status[80] = "";
|
|
|
|
/* Reserve the netboot staging region FIRST, before libogc allocates the
|
|
* framebuffer or anything else out of the arena. A staged image must
|
|
* not live in ordinary heap memory: DOL sections load from 0x80003100
|
|
* upward and would overwrite it partway through the copy. */
|
|
s_stage = netboot_reserve(&s_stage_max);
|
|
|
|
VIDEO_Init();
|
|
PAD_Init();
|
|
rmode = VIDEO_GetPreferredMode(NULL);
|
|
xfb = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
|
|
console_init(xfb, 20, 20, rmode->fbWidth, rmode->xfbHeight,
|
|
rmode->fbWidth * VI_DISPLAY_PIX_SZ);
|
|
VIDEO_Configure(rmode);
|
|
VIDEO_SetNextFramebuffer(xfb);
|
|
VIDEO_SetBlack(FALSE);
|
|
VIDEO_Flush();
|
|
VIDEO_WaitVSync();
|
|
if (rmode->viTVMode & VI_NON_INTERLACE) VIDEO_WaitVSync();
|
|
|
|
snprintf(status, sizeof(status), "ready - press START to run everything");
|
|
draw_main(sel, status);
|
|
|
|
for (;;) {
|
|
u16 down;
|
|
VIDEO_WaitVSync();
|
|
PAD_ScanPads();
|
|
down = PAD_ButtonsDown(0);
|
|
if (!down) continue;
|
|
|
|
if (down & PAD_TRIGGER_Z) {
|
|
/* Self-update: pull a new .dol over the BBA and launch it.
|
|
* Staged in a malloc'd buffer rather than written in place --
|
|
* netboot_launch() copies sections to their load addresses,
|
|
* which overlap the memory this program is running from. */
|
|
do_netboot(status, sizeof(status));
|
|
screen = 0;
|
|
draw_main(sel, status);
|
|
continue;
|
|
}
|
|
|
|
if (down & PAD_BUTTON_START) {
|
|
snprintf(status, sizeof(status), "running all suites...");
|
|
if (screen) draw_suite(cur, ssel, status); else draw_main(sel, status);
|
|
tests_run_all();
|
|
snprintf(status, sizeof(status), "done");
|
|
screen = 0;
|
|
draw_main(sel, status);
|
|
continue;
|
|
}
|
|
|
|
if (screen == 0) {
|
|
int n = g_nsuites;
|
|
if (down & PAD_BUTTON_DOWN) sel = (sel + 1) % n;
|
|
if (down & PAD_BUTTON_UP) sel = (sel + n - 1) % n;
|
|
if (down & PAD_BUTTON_A) { screen = 1; cur = sel; ssel = 0; status[0] = 0;
|
|
draw_suite(cur, ssel, status); continue; }
|
|
if (down & PAD_BUTTON_X) { snprintf(status, sizeof(status), "running %s...",
|
|
g_suites[sel].name);
|
|
draw_main(sel, status);
|
|
tests_run_suite(&g_suites[sel]);
|
|
snprintf(status, sizeof(status), "%s done",
|
|
g_suites[sel].name); }
|
|
if (down & PAD_BUTTON_Y) { tests_reset_status();
|
|
snprintf(status, sizeof(status), "results cleared"); }
|
|
draw_main(sel, status);
|
|
} else {
|
|
suite_t *s = &g_suites[cur];
|
|
if (down & PAD_BUTTON_DOWN) ssel = (ssel + 1) % s->ntests;
|
|
if (down & PAD_BUTTON_UP) ssel = (ssel + s->ntests - 1) % s->ntests;
|
|
if (down & PAD_BUTTON_B) { screen = 0; status[0] = 0;
|
|
draw_main(sel, status); continue; }
|
|
if (down & PAD_BUTTON_A) { snprintf(status, sizeof(status), "running %s...",
|
|
s->tests[ssel].name);
|
|
draw_suite(cur, ssel, status);
|
|
tests_run_test(&s->tests[ssel]);
|
|
status[0] = 0; }
|
|
if (down & PAD_BUTTON_X) { snprintf(status, sizeof(status), "running %s...", s->name);
|
|
draw_suite(cur, ssel, status);
|
|
tests_run_suite(s);
|
|
status[0] = 0; }
|
|
draw_suite(cur, ssel, status);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|