Files
Roflin 1a1557ff54 Add R on the main menu to boot Swiss and leave the test app
Getting back to Swiss previously meant power-cycling the console.  R boots
/boot.dol from the SD card directly.

It reuses netboot_boot_file(), so there is ONE DOL launcher and one set of
safety checks (MEM1 entry point, section overlap against the reserved staging
region, libfat unmounted before the jump) rather than a second copy to keep in
step.

Confirmed rather than one-touch, and main-menu only: R is an easy trigger to
catch by accident, test results are not saved anywhere, and losing a full run
to a stray press is worse than one extra button press.  Inside a suite R would
be easy to hit while navigating, so it is ignored there.

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

346 lines
12 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)
* R boot Swiss (/boot.dol) and leave the test app
*/
#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 all Y clear "
"Z net-update R Swiss\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);
}
/* Boot Swiss from the SD card and leave the test app.
*
* Confirmed rather than one-touch: R is an easy trigger to catch by accident,
* test results are not saved anywhere, and losing a full run to a stray press
* is worse than one extra button. */
static void do_boot_swiss(char *status, size_t nstatus)
{
int rc;
printf("\n" C_HEAD " Boot Swiss (/boot.dol)" C_RESET "\n\n");
printf(" A = confirm, B = cancel\n");
printf(C_DIM " Test results are not saved and will be lost.\n" C_RESET);
for (;;) {
u16 d;
VIDEO_WaitVSync();
PAD_ScanPads();
d = PAD_ButtonsDown(0);
if (d & PAD_BUTTON_B) {
snprintf(status, nstatus, "boot cancelled");
return;
}
if (d & PAD_BUTTON_A)
break;
}
if (!s_stage) {
snprintf(status, nstatus, "staging region unavailable");
return;
}
printf("\n booting /boot.dol ...\n");
VIDEO_WaitVSync();
VIDEO_WaitVSync();
/* Same path netboot uses after an update, so there is one DOL launcher
* and one set of safety checks rather than two. */
rc = netboot_boot_file("/boot.dol", s_stage, s_stage_max, nb_status);
/* Only reached on refusal -- the launcher does not return on success. */
nb_pause("boot refused");
snprintf(status, nstatus, "/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 (!screen && (down & PAD_TRIGGER_R)) {
/* Main menu only: inside a suite R would be easy to hit
* while navigating. */
do_boot_swiss(status, sizeof(status));
draw_main(sel, status);
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;
}