Add on-console GameCube test suite + devkitPPC in the devcontainer

gc_bba_test/ builds a .dol that drives the adapter over raw EXI from the
console and reports pass/fail per test on the TV, navigable with the pad.

Uses libogc's EXI API against EXI0:2 (Serial Port 1) rather than its bba
network driver: driving the register file directly gives each check its own
verdict, so a failure points at a specific register instead of at
'networking'.  Six suites - identity, registers, reset/control, EXI link
integrity (including an EXI clock-index sweep), ethernet TX/RX, SPRAM/DMA
streaming.

Encodes the hardware lessons from bring-up so the app cannot mislead:
  * the device ID is a one-shot cleared by the first NCRA reset, so it is
    reported SKIP on a re-run rather than FAIL
  * TI means the frame reached the W5100, NOT the wire - the TX test says so
  * register reads clock exactly the requested length (over-clocking
    desynchronises the next transaction)
  * TXDATA is written one byte per transaction (eff_addr auto-increments)

.devcontainer/Dockerfile gains the devkitPro gamecube-dev group.  devkitPro
publishes no .deb on GitHub, so this uses their installer script against
apt.devkitpro.org -- which is 403 from inside the running container's egress
but reachable from a host-side docker build.  Rebuild the container to get it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:59:37 +02:00
parent f2a721cb1f
commit ccf4eb4996
8 changed files with 934 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
/* 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
*/
#include <gccore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "tests.h"
#include "bba.h"
static void *xfb = NULL;
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\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);
}
int main(void)
{
int screen = 0; /* 0 = main, 1 = suite detail */
int sel = 0, ssel = 0, cur = 0;
char status[80] = "";
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_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;
}