Files
rebbarb/gc_bba_test/source/main.c
T
Roflin 30563b166d Self-update writes the new build to SD, then boots known-good /boot.dol
Previous version launched the image it had just received, which is the one
thing that cannot be made safe: a broken build hangs the console and the
checks can only catch a truncated or non-DOL image, never a plausible but
broken one.

The flow is now receive -> write to SD -> boot /boot.dol:
  - netboot_sd_write() mounts the card (auto-detecting SD2SP2, then SD Gecko
    slot B, then slot A), writes the image to the path the host asked for,
    and READS IT BACK AND COMPARES.  A half-written .dol that still parses is
    exactly the failure this design exists to avoid, and the card is the one
    component here with no other integrity check.
  - netboot_boot_file() then loads /boot.dol -- a known-good Swiss -- and
    launches that instead.

So the only image this ever jumps into is one that was already known good.  A
broken test build costs nothing: Swiss comes up and can load anything,
including the previous build.  If the SD write or verify fails, nothing is
launched at all and the menu is still there.

The destination path travels in the DONE frame rather than being baked in, so
the host decides where the build lands (send-dol.py --dest, default
/gc_bba_test.dol).

libfat is unmounted before the jump: leaving the card mounted hands the next
image a filesystem libfat still thinks it owns, on an EXI channel it is about
to reinitialise.

Note the SD Gecko in memory-card slot A shares EXI channel 0 with the BBA
(channel 0, device 2).  libogc arbitrates, but SD2SP2 is on its own channel
and avoids the question -- hence the detection order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 11:23:38 +02:00

264 lines
9.0 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 " The received build is WRITTEN TO THE SD CARD and verified;\n"
" it is never launched directly. This then boots /boot.dol --\n"
" your known-good Swiss -- so a broken test build costs nothing:\n"
" Swiss comes up and can load whatever you like.\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;
}
rc = netboot_sd_write(netboot_dest_path(), s_stage, (u32)len, nb_status);
if (rc < 0) {
/* Nothing has been launched and nothing is half-written that we
* know of, so staying in the menu is the safe outcome. */
snprintf(status, nstatus, "SD write failed (%d) - nothing launched", rc);
return;
}
printf("\n booting /boot.dol ...\n");
VIDEO_WaitVSync();
VIDEO_WaitVSync();
/* Reuse the staging region: the new build is already on the card and
* verified, so the copy in RAM is finished with. */
rc = netboot_boot_file("/boot.dol", s_stage, s_stage_max, nb_status);
/* Only reached on refusal. The new build IS on the card either way,
* so this is recoverable by rebooting into Swiss by hand. */
snprintf(status, nstatus,
"update saved, but /boot.dol would not boot (%d)", rc);
}
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;
}