Files
Yaya48andClaude Opus 5.5 f391ae6c85 StarletBootstrapProbe: --broadway-h1a
The probe stands in for Broadway with StarOS's own bench loader code, so
that StarOS's H-1A bench (docs/architecture/h-1a.md, D-H1A-7) runs whole:
its Starlet image on the ARM core, its Broadway side from StarOS's
boot/wii/loader hw0c.c, hw0d.c and h1a.c, vendored unchanged in
Tools/StarletBootstrapProbe/StarOSLoader (StarOS 480119d, stored -text so
their bytes stay StarOS's) with their contract headers, and built over a
host HAL in BroadwayHAL/. CMake computes the nine files' SHA-256s at
configure; the probe prints them when the mode starts.

As StarOS's loader around the jump: hw0c_setup() before the Starlet's
first instruction, one hw0c_step() between each two slices, the timebase
243 MHz / 4 from the Starlet's cycles, until it says 1; once the image's
trace is terminal and the bench is done, the trace, then the report
between main.c's status and END lines on stdout. If the budget runs out,
the report under SBL:TIMEOUT, and the run fails. The client reaches PPC
memory only in the bench window 0x13500000..0x13510000, by whole words:
the first access outside fails the run, and is not made. Dolphin's memory
is coherent: the run rehearses the protocol, the capture and the judge,
not the console's caches.

--cycles may reach 2^34 in this mode only (MAX_CLIENT_CYCLES), 2^30 as
before otherwise. --broadway-h1a takes neither --ipc-script nor
--stage-spk, whose behaviour is unchanged.

StarletProbeBroadwayTest: the window's bounds, the clock, Gecko's number
formats, the budgets, the built-in sources; and, each in a death test's
own process (the loader's state is global), the real loader code against
a scripted Starlet: 3000 rounds and a COHERENT record, no READY, no
Starlet END, a budget too short; a second client refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 18:00:12 +01:00

474 lines
14 KiB
C

/* SPDX-License-Identifier: GPL-2.0-only */
/*
* HW-M4-0C's Broadway side: crates/hw-bench's BroadwaySide in C, over the
* shared window of boot/contract/staros_hw0c.h. Built only with
* -DSTAROS_HW0C; never run on the console without its own reviewed GO.
*
* MSR=0: the window's addresses are physical. Whether Broadway's caches are
* on is not assumed: HID0 and L2CR are recorded in the header. publish is
* dcbf over the lines then sync; refresh is dcbi over the lines then sync,
* used only on lines the Starlet writes, or on the sentinel's lines, which
* were flushed at setup. Every store is an aligned word (write32).
*/
#include <stdint.h>
#include "board.h"
#include "gecko.h"
#include "hw0c.h"
#include "staros_hw0c.h"
#ifdef STAROS_HW0D
#include "hw0d.h"
#include "staros_hw0d.h"
#endif
#ifdef STAROS_HW0E
#include "hw0e.h"
#endif
#ifdef STAROS_HW0F
#include "hw0f.h"
#endif
#ifdef STAROS_H1A
#include "h1a.h"
#endif
#define HW0C_TIMEOUT_MS 2000u
/* The wait for READY, from the first step after the jump: the Starlet is
* ready only after the layout image's own checks, about 4 s on the console
* (tests/hardware-wii/adr-0003-layout-gates.md). */
#define HW0C_READY_TIMEOUT_MS 30000u
#define AT(offset) (STAROS_HW0C_BASE + (offset))
enum phase {
SETUP,
WAIT_READY,
POST,
WAIT_ACK,
END,
DONE,
#ifdef STAROS_HW0F
T4, /* HW-M4-0F: T4's counter instead of the rounds, from READY to its end */
#endif
#ifdef STAROS_H1A
H1A, /* H-1A: its own 3000 rounds instead of the bench's, from READY to its end */
#endif
};
struct tally {
uint32_t rounds, bad_rounds, stale, corrupt;
};
static const uint32_t run_ids[3] = {STAROS_HW0C_S1_B2S, STAROS_HW0C_S2_S2B, STAROS_HW0C_S3_SEQUENCE};
static const uint32_t run_rounds[3] = {STAROS_HW0C_S1_ROUNDS, STAROS_HW0C_S2_ROUNDS, STAROS_HW0C_S3_ROUNDS};
static struct {
enum phase phase;
uint32_t run, round;
uint64_t deadline;
struct tally tallies[4];
uint32_t ready, refused, sentinel_ready, sentinel_end;
uint32_t timed_out, timed_out_id, timed_out_round;
uint32_t first, first_id, first_round, first_index, first_value;
uint32_t hid0, l2cr, control, dacr, descriptor;
} bench;
#ifdef STAROS_HW0F_HOST
/* tests/hardware-wii/hw-m4-0f's host bench only: its model stands for the two
* cache instructions and the two special registers. */
void dcbf(uint32_t address);
void dcbi(uint32_t address);
uint32_t hid0(void);
uint32_t l2cr(void);
#else
static inline void dcbf(uint32_t address)
{
__asm__ volatile("dcbf 0,%0" ::"r"(address) : "memory");
}
static inline void dcbi(uint32_t address)
{
__asm__ volatile("dcbi 0,%0" ::"r"(address) : "memory");
}
static uint32_t hid0(void)
{
uint32_t value;
__asm__ volatile("mfspr %0,1008" : "=r"(value));
return value;
}
static uint32_t l2cr(void)
{
uint32_t value;
__asm__ volatile("mfspr %0,1017" : "=r"(value));
return value;
}
#endif
static void publish(uint32_t offset, uint32_t bytes)
{
for (uint32_t at = 0; at < bytes; at += STAROS_HW0C_LINE)
dcbf(AT(offset + at));
sync();
}
static void refresh(uint32_t offset, uint32_t bytes)
{
for (uint32_t at = 0; at < bytes; at += STAROS_HW0C_LINE)
dcbi(AT(offset + at));
sync();
}
static void fill(uint32_t tag, uint32_t round, uint32_t base, uint32_t words)
{
for (uint32_t i = 0; i < words; i++)
write32(AT(base + 4u * i), STAROS_HW0C_PATTERN(tag, round, i));
}
/* Compares `words` words with tag's round: mismatches, stale ones, the first. */
static uint32_t check(uint32_t tag, uint32_t round, uint32_t base, uint32_t words, uint32_t *stale,
uint32_t *first_index, uint32_t *first_value)
{
uint32_t mismatches = 0;
*stale = 0;
for (uint32_t i = 0; i < words; i++) {
uint32_t value = read32(AT(base + 4u * i));
if (value == STAROS_HW0C_PATTERN(tag, round, i))
continue;
if (mismatches++ == 0) {
*first_index = i;
*first_value = value;
}
if ((value >> 28) == tag && (value & 0x3FFFu) == (i & 0x3FFFu) && ((value >> 14) & 0x3FFFu) < (round & 0x3FFFu))
(*stale)++;
}
return mismatches;
}
static int broadway_writes(uint32_t id, uint32_t round)
{
return id == STAROS_HW0C_S1_B2S || (id == STAROS_HW0C_S3_SEQUENCE && (round & 1u));
}
static void count(uint32_t id, uint32_t round, uint32_t mismatches, uint32_t stale, uint32_t index, uint32_t value)
{
struct tally *tally = &bench.tallies[id & 3u];
tally->rounds++;
if (!mismatches)
return;
tally->bad_rounds++;
tally->stale += stale;
tally->corrupt += mismatches - stale;
if (!bench.first) {
bench.first = 1;
bench.first_id = id;
bench.first_round = round;
bench.first_index = index;
bench.first_value = value;
}
}
void hw0c_setup(void)
{
bench.hid0 = hid0();
bench.l2cr = l2cr();
fill(STAROS_HW0C_TAG_SENTINEL, 0, STAROS_HW0C_SENTINEL, STAROS_HW0C_SENTINEL_WORDS);
publish(STAROS_HW0C_SENTINEL, 4u * STAROS_HW0C_SENTINEL_WORDS);
const uint32_t lines[3] = {STAROS_HW0C_BROADWAY, STAROS_HW0C_STARLET, STAROS_HW0C_ENVIRONMENT};
for (uint32_t l = 0; l < 3; l++) {
for (uint32_t w = 0; w < STAROS_HW0C_LINE; w += 4)
write32(AT(lines[l] + w), 0);
publish(lines[l], STAROS_HW0C_LINE);
}
#ifdef STAROS_HW0D
hw0d_clear(); /* before the header: the Starlet starts after the jump */
#endif
#ifdef STAROS_HW0E
hw0e_clear(); /* the probe lines' marker A and a cleared record, before the jump */
#endif
#ifdef STAROS_HW0F
hw0f_clear(); /* T4's pages and record cleared, Z in its read lines, before the jump */
#endif
#ifdef STAROS_H1A
h1a_clear(); /* H-1A's pages and record cleared, Z in its pool, before the jump */
#endif
uint32_t plan = 1u << STAROS_HW0C_S0_SENTINEL;
/* T4 posts no round: its plan is S0 alone (G-T4-2, D5); nor does H-1A here, h1a.c posts its own. */
#if !defined(STAROS_HW0F) && !defined(STAROS_H1A)
for (uint32_t r = 0; r < 3; r++)
plan |= 1u << run_ids[r];
#endif
write32(AT(STAROS_HW0C_H_VERSION), STAROS_HW0C_VERSION);
write32(AT(STAROS_HW0C_H_SIZE), STAROS_HW0C_SIZE);
write32(AT(STAROS_HW0C_H_HID0), bench.hid0);
write32(AT(STAROS_HW0C_H_L2CR), bench.l2cr);
write32(AT(STAROS_HW0C_H_PLAN), plan);
write32(AT(STAROS_HW0C_H_ROUNDS), STAROS_HW0C_S1_ROUNDS);
publish(STAROS_HW0C_HEADER, STAROS_HW0C_LINE);
write32(AT(STAROS_HW0C_H_MAGIC), STAROS_HW0C_MAGIC);
publish(STAROS_HW0C_HEADER, STAROS_HW0C_LINE);
#ifdef STAROS_HW0D
hw0d_mark(STAROS_HW0D_E_B_SETUP, 0, 0);
#endif
bench.phase = WAIT_READY;
bench.deadline = 0; /* set by the first step, after the jump */
}
static void timeout_at(uint64_t now, uint32_t ms)
{
bench.deadline = now + (uint64_t)ms * TICKS_PER_MS;
}
int hw0c_step(uint64_t now)
{
const uint32_t starlet = STAROS_HW0C_STARLET;
if (bench.phase == WAIT_READY && !bench.deadline)
timeout_at(now, HW0C_READY_TIMEOUT_MS);
switch (bench.phase) {
case SETUP:
case DONE:
return 1;
case WAIT_READY: {
refresh(starlet, STAROS_HW0C_LINE);
uint32_t state = read32(AT(starlet + STAROS_HW0C_C_STATE));
if (state == STAROS_HW0C_STATE_READY) {
refresh(STAROS_HW0C_ENVIRONMENT, STAROS_HW0C_LINE);
bench.sentinel_ready = read32(AT(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_SENTINEL_BAD));
bench.control = read32(AT(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_CONTROL));
bench.dacr = read32(AT(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_DACR));
bench.descriptor = read32(AT(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_DESCRIPTOR));
bench.ready = 1;
bench.phase = POST;
#ifdef STAROS_HW0D
hw0d_mark(STAROS_HW0D_E_B_READY, 0, 0);
#endif
#ifdef STAROS_HW0F
bench.phase = T4;
hw0f_ready(now);
#endif
#ifdef STAROS_H1A
bench.phase = H1A;
h1a_ready(now);
#endif
} else if (state == STAROS_HW0C_STATE_REFUSED) {
bench.refused = 1;
bench.phase = END;
} else if (now >= bench.deadline) {
bench.timed_out = 1;
bench.timed_out_id = STAROS_HW0C_S0_SENTINEL;
bench.phase = END;
}
return 0;
}
case POST: {
if (bench.run >= 3) {
bench.phase = END;
return 0;
}
if (bench.round >= run_rounds[bench.run]) {
bench.run++;
bench.round = 0;
return 0;
}
uint32_t id = run_ids[bench.run];
uint32_t round = ++bench.round;
if (broadway_writes(id, round)) {
fill(STAROS_HW0C_TAG_B2S, round, STAROS_HW0C_B2S, STAROS_HW0C_DATA_WORDS);
publish(STAROS_HW0C_B2S, 4u * STAROS_HW0C_DATA_WORDS);
}
write32(AT(STAROS_HW0C_BROADWAY + STAROS_HW0C_C_SCENARIO), id);
write32(AT(STAROS_HW0C_BROADWAY + STAROS_HW0C_C_STATE), STAROS_HW0C_STATE_ROUND);
publish(STAROS_HW0C_BROADWAY, STAROS_HW0C_LINE);
write32(AT(STAROS_HW0C_BROADWAY + STAROS_HW0C_C_ROUND), round);
publish(STAROS_HW0C_BROADWAY, STAROS_HW0C_LINE);
#ifdef STAROS_HW0D
hw0d_mark(STAROS_HW0D_E_B_POSTED, id, round);
#endif
#ifdef STAROS_HW0E
hw0e_posted(id, round);
#endif
bench.phase = WAIT_ACK;
timeout_at(now, HW0C_TIMEOUT_MS);
return 0;
}
case WAIT_ACK: {
uint32_t id = run_ids[bench.run];
refresh(starlet, STAROS_HW0C_LINE);
if (read32(AT(starlet + STAROS_HW0C_C_STATE)) == STAROS_HW0C_STATE_REFUSED) {
#ifdef STAROS_HW0D
hw0d_mark(STAROS_HW0D_E_B_TIMEOUT,
STAROS_HW0D_LINE(STAROS_HW0C_STATE_REFUSED, read32(AT(starlet + STAROS_HW0C_C_SCENARIO))),
read32(AT(starlet + STAROS_HW0C_C_ROUND)));
#endif
bench.refused = 1;
bench.phase = END;
return 0;
}
if (read32(AT(starlet + STAROS_HW0C_C_ROUND)) != bench.round ||
read32(AT(starlet + STAROS_HW0C_C_SCENARIO)) != id) {
if (now >= bench.deadline) {
bench.timed_out = 1;
bench.timed_out_id = id;
bench.timed_out_round = bench.round;
#ifdef STAROS_HW0D
hw0d_mark(STAROS_HW0D_E_B_TIMEOUT,
STAROS_HW0D_LINE(read32(AT(starlet + STAROS_HW0C_C_STATE)),
read32(AT(starlet + STAROS_HW0C_C_SCENARIO))),
read32(AT(starlet + STAROS_HW0C_C_ROUND)));
#endif
bench.phase = END;
}
return 0;
}
#ifdef STAROS_HW0D
hw0d_mark(STAROS_HW0D_E_B_ANSWERED, id, bench.round);
#endif
uint32_t mismatches, stale = 0, index = 0, value = 0;
if (broadway_writes(id, bench.round)) {
mismatches = read32(AT(starlet + STAROS_HW0C_C_MISMATCHES));
stale = read32(AT(starlet + STAROS_HW0C_C_STALE));
index = read32(AT(starlet + STAROS_HW0C_C_FIRST_INDEX));
value = read32(AT(starlet + STAROS_HW0C_C_FIRST_VALUE));
} else {
refresh(STAROS_HW0C_S2B, 4u * STAROS_HW0C_DATA_WORDS);
mismatches = check(STAROS_HW0C_TAG_S2B, bench.round, STAROS_HW0C_S2B, STAROS_HW0C_DATA_WORDS, &stale,
&index, &value);
}
count(id, bench.round, mismatches, stale, index, value);
bench.phase = POST;
return 0;
}
#ifdef STAROS_HW0F
case T4: {
/* Until the Starlet's END, or READY + 45 s: then the record is read. A
* missing END is a timeout, so S0 is INCONCLUSIVE (G-T4-2, D7). */
int done = hw0f_step(now);
if (done < 0) {
bench.timed_out = 1;
bench.timed_out_id = STAROS_HW0C_S0_SENTINEL;
}
if (done)
bench.phase = END;
return 0;
}
#endif
#ifdef STAROS_H1A
case H1A: {
/* Until h1a.c has read the record back: after the Starlet's END, or READY
* + 65 s without one, which is a timeout, so S0 is INCONCLUSIVE. */
int done = h1a_step(now);
if (done < 0) {
bench.timed_out = 1;
bench.timed_out_id = STAROS_HW0C_S0_SENTINEL;
}
if (done)
bench.phase = END;
return 0;
}
#endif
case END: {
uint32_t stale, index, value;
write32(AT(STAROS_HW0C_BROADWAY + STAROS_HW0C_C_STATE), STAROS_HW0C_STATE_END);
publish(STAROS_HW0C_BROADWAY, STAROS_HW0C_LINE);
#ifdef STAROS_HW0D
hw0d_mark(STAROS_HW0D_E_B_END, 0, 0);
#endif
#ifdef STAROS_H1A
/* H-1A writes its areas A1 to A3 inside the sentinel after READY: the rest
* is checked, as h1a.c's own CHECK line counts it. */
(void)stale;
(void)index;
(void)value;
bench.sentinel_end = h1a_sentinel_end();
#else
refresh(STAROS_HW0C_SENTINEL, 4u * STAROS_HW0C_SENTINEL_WORDS);
bench.sentinel_end = check(STAROS_HW0C_TAG_SENTINEL, 0, STAROS_HW0C_SENTINEL, STAROS_HW0C_SENTINEL_WORDS,
&stale, &index, &value);
#endif
bench.phase = DONE;
return 1;
}
}
return 1;
}
static void line_hex(const char *label, uint32_t value)
{
gecko_puts(label);
gecko_hex(value);
}
static void line_dec(const char *label, uint32_t value)
{
gecko_puts(label);
gecko_dec(value);
}
/* One scenario's verdict, as hw_bench::Outcome::verdict gives it. */
static void verdict(uint32_t id)
{
struct tally *tally = &bench.tallies[id & 3u];
line_dec("SBL:HW0C S", id);
if (id == STAROS_HW0C_S0_SENTINEL) {
uint32_t bad = bench.sentinel_ready + bench.sentinel_end;
if (!bench.ready)
gecko_puts(" INCONCLUSIVE");
else if (bad)
line_dec(" FAIL CORRUPT ", bad);
else if (bench.timed_out || bench.refused)
gecko_puts(" INCONCLUSIVE");
else
gecko_puts(" PASS");
} else if (tally->bad_rounds) {
line_dec(" FAIL ROUNDS ", tally->bad_rounds);
line_dec(" STALE ", tally->stale);
line_dec(" CORRUPT ", tally->corrupt);
} else if (tally->rounds < run_rounds[id - 1u] || bench.refused) {
line_dec(" INCONCLUSIVE ROUNDS ", tally->rounds);
} else {
line_dec(" PASS ROUNDS ", tally->rounds);
}
gecko_puts("\n");
}
void hw0c_report(void)
{
line_hex("SBL:HW0C HID0 ", bench.hid0);
line_hex(" L2CR ", bench.l2cr);
gecko_puts("\n");
line_hex("SBL:HW0C ENV CTRL ", bench.control);
line_hex(" DACR ", bench.dacr);
line_hex(" PAGE ", bench.descriptor);
line_dec(" SENTINEL READY ", bench.sentinel_ready);
line_dec(" END ", bench.sentinel_end);
gecko_puts("\n");
for (uint32_t id = 0; id < 4u; id++)
verdict(id);
gecko_puts("SBL:HW0C S4 GATED IPC\nSBL:HW0C S5 GATED AHB\n");
if (bench.first) {
line_dec("SBL:HW0C FIRST S", bench.first_id);
line_dec(" ROUND ", bench.first_round);
line_dec(" INDEX ", bench.first_index);
line_hex(" VALUE ", bench.first_value);
gecko_puts("\n");
}
if (bench.timed_out) {
line_dec("SBL:HW0C TIMEOUT S", bench.timed_out_id);
line_dec(" ROUND ", bench.timed_out_round);
gecko_puts("\n");
}
if (bench.refused)
gecko_puts("SBL:HW0C REFUSED\n");
gecko_puts("SBL:HW0C END\n");
#ifdef STAROS_HW0D
hw0d_report();
#endif
#ifdef STAROS_HW0E
hw0e_report();
#endif
#ifdef STAROS_HW0F
hw0f_report();
#endif
#ifdef STAROS_H1A
h1a_report();
#endif
}