Files
rebbarb/gc_bba_test/source/main.c
T
Roflin 799692a5f4 netboot: stop with the summary before booting Swiss
The transfer completes on hardware -- 293/293 blocks, 299908 bytes in 15.7 s --
but roughly a third of blocks needed a retry, and the numbers that say WHY are
printed by the GC and then scrolled away when Swiss boots.  So the one side
that knows whether an ACK failed to transmit or was lost on the wire could not
be read.

netboot now stops after writing the SD card and waits for a button press, and
the summary carries the counters that separate the cases:

    desc@+0=N +1=M  bad=N  lapped=N  txto=N

txto is the discriminator: it counts ACKs where TI never set, meaning the
frame never reached the W5100.  High txto alongside the host's retries points
at the TX path; txto near zero points at frames sent but lost.

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

292 lines
10 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);
}
/* Hold the screen until a button is pressed. Without this the caller's
* draw_main() redraws immediately and wipes every diagnostic line netboot
* just printed -- which is exactly why the first two failures were reported
* as "a short flash" with no detail. */
static void nb_pause(const char *why)
{
printf("\n" C_DIM " %s -- press any button to return to the menu\n" C_RESET,
why);
for (;;) {
VIDEO_WaitVSync();
PAD_ScanPads();
if (PAD_ButtonsDown(0)) return;
}
}
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");
/* Deliberately terse: the console holds ~25 lines and the diagnostics
* printed during a failed transfer are what matter. Explanatory text
* here pushed them off the top of the screen, which cost two blind
* round trips. */
printf(C_DIM " temp+verify+rotate; keeps <name>.prev.dol; boots /boot.dol\n"
C_RESET);
printf(" Blocks until done or it times out (30 s idle, 5 s mid-xfer).\n"
" Stops with the summary before booting Swiss.\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);
nb_pause("transfer failed");
return;
}
rc = netboot_sd_write(netboot_dest_path(), s_stage, (u32)len, nb_status);
if (rc < 0) {
nb_pause("SD write failed");
/* 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) - existing build untouched", rc);
return;
}
/* Stop here so the transfer summary can actually be READ. Booting
* straight into Swiss scrolled it away, which meant the one set of
* numbers that says WHY blocks needed retrying -- txto, the descriptor
* split, bad/lapped counts -- was never visible. */
nb_pause("update written to the SD card");
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);
nb_pause("boot refused");
/* 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;
}