4831c1353c
Hardware with RBFI active: "timeout mid-transfer (5 seen, 18 bad desc, 0
lapped)", plus a frame dump of "1E FF FF FF FF FF FF 00".
Three things that tells us:
- lapped = 0, so the ring is no longer being overwritten. RBFI works and
overflow is eliminated as the cause.
- got > 0 ("mid-transfer"), so some blocks DID arrive and match.
- 0x1E is 286 & 0xFF, the low byte of total_len, followed by the full
six-byte broadcast MAC and then 0x00. The frame body is intact but starts
one byte later than the reader expects.
18 of 23 frames rejected while 5 parsed in the SAME run means the offset
VARIES frame to frame -- a race in the assembler, not a constant. Simulation
does not reproduce it: T2/T5 put the descriptor at +0 and the body at +4 with
the write port both free-running and stalled 1-in-3, and the mutation controls
for both known handshake bugs fail as expected. So the cause is still unknown
and I am not going to guess at it a fifth time.
ring_next() now LOCATES the descriptor instead of assuming +0: it probes
offsets 0 and 1 and takes whichever yields a plausible length (> 4 descriptor
bytes, <= a legal frame), then reads the body and advances RRP from that same
offset. A length is a strong discriminator, so this is a measurement rather
than a guess, and it both lets the transfer proceed and COUNTS the split --
"desc@+0=N +1=M" on success and on timeout. That distribution is the
measurement needed to find the real gateware bug.
Also trimmed the on-screen guidance: the console holds ~25 lines and the
explanatory text was pushing the failure diagnostics off the top, which cost
two blind round trips. Status during a transfer is now one line, and the
menu blurb one line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
285 lines
9.7 KiB
C
285 lines
9.7 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\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;
|
|
}
|
|
|
|
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;
|
|
}
|