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

462 lines
14 KiB
C

/* SPDX-License-Identifier: GPL-2.0-only */
/*
* H-1A's Broadway side (docs/architecture/h-1a.md, G-H1A-2): crates/hw-bench's
* h1a::setup, mark, Broadway and collect in C, plus the SBL:H1A block. Built
* only with -DSTAROS_HW0C -DSTAROS_HW0D -DSTAROS_H1A, never with
* -DSTAROS_HW0E or -DSTAROS_HW0F; never run on the console without its own
* reviewed GO.
*
* Before the jump, h1a_clear zeroes the record, page 0's near scrub and pages
* 1 to 3, then writes Z in the pool's 188 lines. Once READY is seen,
* h1a_ready writes M in them (T4's publication). Then h1a_step posts the 3000
* rounds of the frozen schedule, one action a call, as h1a::Broadway does:
* - it waits D_POST (2 ms) from the answer it saw, reads the Starlet's line
* once more and posts nothing to an ended Starlet;
* - for a Broadway -> Starlet round it fills the round's area (each arm turns
* over the four areas) with words naming the round and the address, and
* publishes it; then it writes SCENARIO and STATE, publishes, writes ROUND
* last and publishes;
* - it waits for the answer (200 ms): ROUND and SCENARIO the round's, STATE
* ROUND or END (the Starlet's END keeps its last answer's words); for a
* Starlet -> Broadway round it then reads S2B and counts its stale and
* other words;
* - after the last round, a timeout or the Starlet's END, it writes its END.
* It then waits for the Starlet's END (or READY + 65 s), reads the record at
* most ATTEMPTS times and makes its end checks.
*
* A read of the record is COHERENT only when GEN before, the body's GEN and GEN
* after are equal, even and not 0, and the magic, version and GEN 2 are the
* record's (h1a::qualify). The words are printed as the last read took them,
* never judged here: tools/gecko/check_h1a.py judges them. No AHB, IPC or
* memory-controller register is touched. Every store is an aligned word.
*
* The block ends with a CRC-32 of its numbers as they are printed (T4's I-1).
*/
#include <stdint.h>
#include "board.h"
#include "gecko.h"
#include "h1a.h"
#include "staros_hw0c.h"
#include "staros_h1a.h"
#if !defined(STAROS_HW0C) || !defined(STAROS_HW0D)
#error "h1a.c builds on -DSTAROS_HW0C -DSTAROS_HW0D"
#endif
#if defined(STAROS_HW0E) || defined(STAROS_HW0F)
#error "h1a.c excludes -DSTAROS_HW0E and -DSTAROS_HW0F: their window areas overlap H-1A's"
#endif
#define AT(offset) (STAROS_HW0C_BASE + (offset))
#define AREA_BYTES (4u * STAROS_H1A_AREA_WORDS)
#define RESULTS_BYTES (4u * STAROS_H1A_RESULTS_WORDS)
#define LINE STAROS_HW0C_LINE
/* Broadway's wait for the Starlet's END, from READY: past the engine's 60 s
* guard, inside the loader's bench timeout with the wait for READY. */
#define H1A_END_TIMEOUT_MS 65000u
static const uint32_t areas[STAROS_H1A_AREA_COUNT] = STAROS_H1A_AREAS;
static const uint32_t near_scrubs[STAROS_H1A_AREA_COUNT] = STAROS_H1A_NEAR;
enum phase { IDLE, DELAY, POST, WAIT_ACK, ENDING, WAIT_END, DONE };
enum arm { ARM_F, ARM_W, ARM_N, ARM_P };
/* Broadway's own counts and where H-1A is (h1a::Log and h1a::Broadway). */
static struct {
enum phase phase;
uint32_t round;
uint64_t until, acked, ready;
uint32_t b2s[4];
uint32_t posts, last, acks, timeout, s2b_rounds, s2b_stale, s2b_other, starlet_end;
uint64_t delay_min, delay_max;
uint32_t started, timed_out, collected;
} b;
/* The last read of the record, and its end checks. */
static uint32_t results[STAROS_H1A_RESULTS_WORDS];
static uint32_t gen_before, gen_after, attempts, coherent;
static uint32_t check_sentinel, check_scrub, check_pool;
/* The CRC-32 of the numbers printed so far: only h1a_report touches it. */
static uint32_t block_crc;
#ifdef STAROS_H1A_HOST
/* tests/hardware-wii/h1a's host bench only: its model stands for the two
* cache instructions. */
void dcbf(uint32_t address);
void dcbi(uint32_t address);
#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");
}
#endif
static void publish(uint32_t offset, uint32_t bytes)
{
for (uint32_t at = 0; at < bytes; at += LINE)
dcbf(AT(offset + at));
sync();
}
static void refresh(uint32_t offset, uint32_t bytes)
{
for (uint32_t at = 0; at < bytes; at += LINE)
dcbi(AT(offset + at));
sync();
}
/* The schedule (h1a::s2b, scenario, arm). */
static int s2b(uint32_t round)
{
return round > STAROS_H1A_S1_ROUNDS && (round - STAROS_H1A_S1_ROUNDS) % 2u == 0;
}
static uint32_t scenario(uint32_t round)
{
return round <= STAROS_H1A_S1_ROUNDS ? STAROS_H1A_S1 : STAROS_H1A_S3;
}
static enum arm arm(uint32_t round)
{
uint32_t at = round - 1u;
uint32_t block = at / STAROS_H1A_BLOCK_ROUNDS, place = at % STAROS_H1A_BLOCK_ROUNDS;
uint32_t p = (block % 20u) * 2u;
if (place == p)
return ARM_P;
uint32_t k = place < p ? place : place - 1u;
static const enum arm turn[3] = {ARM_F, ARM_W, ARM_N};
return turn[(k + block) % 3u];
}
static uint32_t sentinel_word(uint32_t offset)
{
return STAROS_HW0C_PATTERN(STAROS_HW0C_TAG_SENTINEL, 0u, (offset - STAROS_HW0C_SENTINEL) / 4u);
}
/* The pool's words: page 2's tail, then page 3's after the far scrub. */
static void each_pool_word(void (*f)(uint32_t offset))
{
for (uint32_t w = 0; w < STAROS_H1A_AREA_LINES * STAROS_H1A_WORDS; w++)
f(STAROS_H1A_AREA_POOL + 4u * w);
for (uint32_t w = 0; w < STAROS_H1A_POLL_LINES * STAROS_H1A_WORDS; w++)
f(STAROS_H1A_POLL_POOL + 4u * w);
}
static void write_z(uint32_t offset)
{
write32(AT(offset), STAROS_H1A_Z(offset));
}
static void write_m(uint32_t offset)
{
write32(AT(offset), STAROS_H1A_M(offset));
}
static void check_m(uint32_t offset)
{
if (read32(AT(offset)) != STAROS_H1A_M(offset))
check_pool++;
}
/* The pool's span: from the area pool to page 3's end. */
#define POOLS_BYTES (4u * 0x1000u - STAROS_H1A_AREA_POOL)
void h1a_clear(void)
{
static const uint32_t cleared[3][2] = {
{STAROS_H1A_CLEAR0_START, STAROS_H1A_CLEAR0_END},
{STAROS_H1A_CLEAR1_START, STAROS_H1A_CLEAR1_END},
{STAROS_H1A_CLEAR2_START, STAROS_H1A_CLEAR2_END},
};
for (uint32_t r = 0; r < 3; r++) {
for (uint32_t offset = cleared[r][0]; offset < cleared[r][1]; offset += 4)
write32(AT(offset), 0);
publish(cleared[r][0], cleared[r][1] - cleared[r][0]);
}
each_pool_word(write_z);
publish(STAROS_H1A_AREA_POOL, POOLS_BYTES);
}
void h1a_ready(uint64_t now)
{
if (b.started)
return;
each_pool_word(write_m);
publish(STAROS_H1A_AREA_POOL, POOLS_BYTES);
b.started = 1;
b.ready = now;
b.round = 1;
b.acked = now;
b.until = now + (uint64_t)STAROS_H1A_D_POST_TB;
b.delay_min = UINT64_MAX;
b.phase = DELAY;
}
/* Broadway's post of the current round (h1a::Broadway's Post step). */
static void post(uint64_t now)
{
uint32_t round = b.round;
if (!s2b(round)) {
uint32_t slot = (uint32_t)arm(round);
uint32_t n = b.b2s[slot]++;
uint32_t base = areas[n % 4u];
for (uint32_t w = 0; w < STAROS_H1A_AREA_WORDS; w++) {
uint32_t offset = base + 4u * w;
write32(AT(offset), STAROS_HW0C_PATTERN(STAROS_HW0C_TAG_B2S, round, offset / 4u));
}
publish(base, AREA_BYTES);
}
const uint32_t line = STAROS_HW0C_BROADWAY;
write32(AT(line + STAROS_HW0C_C_SCENARIO), scenario(round));
write32(AT(line + STAROS_HW0C_C_STATE), STAROS_HW0C_STATE_ROUND);
publish(line, LINE);
write32(AT(line + STAROS_HW0C_C_ROUND), round);
publish(line, LINE);
uint64_t delay = now - b.acked;
if (delay < b.delay_min)
b.delay_min = delay;
if (delay > b.delay_max)
b.delay_max = delay;
b.posts++;
b.last = round;
b.until = now + (uint64_t)STAROS_H1A_ACK_TIMEOUT_TB;
b.phase = WAIT_ACK;
}
/* S2B once answered: its stale and other words (h1a::Broadway's WaitAck). */
static void read_s2b(uint32_t round)
{
refresh(STAROS_H1A_S2B, AREA_BYTES);
for (uint32_t w = 0; w < STAROS_H1A_AREA_WORDS; w++) {
uint32_t offset = STAROS_H1A_S2B + 4u * w;
uint32_t value = read32(AT(offset));
if (value == STAROS_HW0C_PATTERN(STAROS_HW0C_TAG_S2B, round, offset / 4u))
continue;
if ((value >> 28) == STAROS_HW0C_TAG_S2B && (value & 0x3FFFu) == ((offset / 4u) & 0x3FFFu) &&
((value >> 14) & 0x3FFFu) < (round & 0x3FFFu))
b.s2b_stale++;
else
b.s2b_other++;
}
b.s2b_rounds++;
}
static void wait_ack(uint64_t now)
{
const uint32_t line = STAROS_HW0C_STARLET;
refresh(line, LINE);
uint32_t state = read32(AT(line + STAROS_HW0C_C_STATE));
uint32_t round = b.round;
/* An answer counts with STATE ROUND, or END: the Starlet's END, written
* after its last answer, keeps that answer's ROUND and SCENARIO. */
int answered = (state == STAROS_HW0C_STATE_ROUND || state == STAROS_HW0C_STATE_END) &&
read32(AT(line + STAROS_HW0C_C_ROUND)) == round &&
read32(AT(line + STAROS_HW0C_C_SCENARIO)) == scenario(round);
int ended = state == STAROS_HW0C_STATE_END;
if (ended)
b.starlet_end = 1;
if (answered) {
b.acks++;
if (s2b(round))
read_s2b(round);
b.acked = now;
if (round >= STAROS_H1A_ROUNDS || ended) {
b.phase = ENDING;
} else {
b.round = round + 1u;
b.until = now + (uint64_t)STAROS_H1A_D_POST_TB;
b.phase = DELAY;
}
} else if (ended) {
b.phase = ENDING;
} else if (now >= b.until) {
b.timeout = round;
b.phase = ENDING;
}
}
static int starlet_ended(void)
{
refresh(STAROS_HW0C_STARLET, LINE);
return read32(AT(STAROS_HW0C_STARLET + STAROS_HW0C_C_STATE)) == STAROS_HW0C_STATE_END;
}
uint32_t h1a_sentinel_end(void)
{
uint32_t changed = 0;
refresh(STAROS_HW0C_SENTINEL, 4u * STAROS_HW0C_SENTINEL_WORDS);
for (uint32_t offset = STAROS_HW0C_SENTINEL; offset < STAROS_HW0C_SIZE; offset += 4) {
int inside = 0;
for (uint32_t a = 0; a < STAROS_H1A_AREA_COUNT; a++)
inside |= offset >= areas[a] && offset < areas[a] + AREA_BYTES;
if (!inside && read32(AT(offset)) != sentinel_word(offset))
changed++;
}
return changed;
}
/* The record read until one read qualifies (at most ATTEMPTS), then the end
* checks (h1a::collect). */
static void collect(void)
{
attempts = 0;
coherent = 0;
while (attempts < STAROS_H1A_ATTEMPTS && !coherent) {
refresh(STAROS_H1A_RESULTS, RESULTS_BYTES);
gen_before = read32(AT(STAROS_H1A_RESULTS + 4u * STAROS_H1A_F_GEN));
for (uint32_t w = 0; w < STAROS_H1A_RESULTS_WORDS; w++)
results[w] = read32(AT(STAROS_H1A_RESULTS + 4u * w));
gen_after = read32(AT(STAROS_H1A_RESULTS + 4u * STAROS_H1A_F_GEN));
attempts++;
coherent = gen_before == gen_after && gen_before % 2u == 0 && gen_before != 0 &&
results[STAROS_H1A_F_GEN] == gen_before && results[STAROS_H1A_F_MAGIC] == STAROS_H1A_MAGIC &&
results[STAROS_H1A_F_VERSION] == STAROS_H1A_VERSION && gen_before == STAROS_H1A_GEN;
}
check_sentinel = h1a_sentinel_end();
check_scrub = 0;
const uint32_t scrubs[3] = {STAROS_H1A_FAR, STAROS_H1A_NEAR_CONTROL, near_scrubs[0]};
for (uint32_t s = 0; s < 3; s++) {
refresh(scrubs[s], STAROS_H1A_BLOCK);
for (uint32_t w = 0; w < STAROS_H1A_BLOCK / 4u; w++)
if (read32(AT(scrubs[s] + 4u * w)) != 0)
check_scrub++;
}
check_pool = 0;
refresh(STAROS_H1A_AREA_POOL, POOLS_BYTES);
each_pool_word(check_m);
b.collected = 1;
}
int h1a_step(uint64_t now)
{
if (!b.started)
return 0;
int late = now - b.ready >= (uint64_t)H1A_END_TIMEOUT_MS * TICKS_PER_MS;
switch (b.phase) {
case IDLE:
return 0;
case DELAY:
if (now >= b.until)
b.phase = POST;
break;
case POST:
/* The Starlet's line once more: an ended Starlet waits for no post. */
if (starlet_ended()) {
b.starlet_end = 1;
b.phase = ENDING;
} else {
post(now);
}
break;
case WAIT_ACK:
wait_ack(now);
break;
case ENDING:
write32(AT(STAROS_HW0C_BROADWAY + STAROS_HW0C_C_STATE), STAROS_HW0C_STATE_END);
publish(STAROS_HW0C_BROADWAY, LINE);
b.phase = WAIT_END;
break;
case WAIT_END:
if (starlet_ended() || late) {
b.timed_out = !starlet_ended();
collect();
b.phase = DONE;
return b.timed_out ? -1 : 1;
}
break;
case DONE:
return b.timed_out ? -1 : 1;
}
/* Past READY + H1A_END_TIMEOUT_MS whatever is under way: Broadway ends. */
if (late && b.phase != WAIT_END && b.phase != DONE)
b.phase = ENDING;
return 0;
}
/* The block's CRC-32, word by word, most significant byte first (T4's). */
static void crc_word(uint32_t word)
{
for (int shift = 24; shift >= 0; shift -= 8) {
block_crc ^= (word >> shift) & 0xFFu;
for (int bit = 0; bit < 8; ++bit)
block_crc = (block_crc >> 1) ^ ((block_crc & 1u) ? 0xEDB88320u : 0u);
}
}
/* Every number of the block goes through one of these, in the order printed. */
static void hex_word(uint32_t value)
{
crc_word(value);
gecko_hex(value);
}
static void dec_word(uint32_t value)
{
crc_word(value);
gecko_dec(value);
}
static void wide(uint64_t value)
{
hex_word((uint32_t)(value >> 32));
hex_word((uint32_t)value);
}
void h1a_report(void)
{
/* A run that never reached READY reads the record anyway: what is there. */
if (!b.collected)
collect();
block_crc = 0xFFFFFFFFu;
gecko_puts("SBL:H1A B POSTS ");
dec_word(b.posts);
gecko_puts(" LAST ");
dec_word(b.last);
gecko_puts(" ACKS ");
dec_word(b.acks);
gecko_puts(" TIMEOUT ");
dec_word(b.timeout);
gecko_puts(" DELAY ");
wide(b.posts ? b.delay_min : UINT64_MAX);
gecko_puts(" ");
wide(b.delay_max);
gecko_puts(" S2B ");
dec_word(b.s2b_rounds);
gecko_puts(" ");
dec_word(b.s2b_stale);
gecko_puts(" ");
dec_word(b.s2b_other);
gecko_puts(" END ");
dec_word(b.starlet_end);
gecko_puts("\nSBL:H1A B CHECK SENTINEL ");
dec_word(check_sentinel);
gecko_puts(" SCRUB ");
dec_word(check_scrub);
gecko_puts(" POOL ");
dec_word(check_pool);
gecko_puts("\nSBL:H1A S GEN ");
hex_word(gen_before);
gecko_puts(" ");
hex_word(gen_after);
gecko_puts(" ATTEMPTS ");
dec_word(attempts);
crc_word(coherent);
gecko_puts(coherent ? " COHERENT\n" : " INCOMPLETE\n");
for (uint32_t row = 0; row < STAROS_H1A_RESULTS_WORDS / 8u; row++) {
gecko_puts("SBL:H1A S RESULTS ");
dec_word(row);
for (uint32_t i = 0; i < 8u; i++) {
gecko_puts(" ");
hex_word(results[8u * row + i]);
}
gecko_puts("\n");
}
gecko_puts("SBL:H1A END CRC ");
gecko_hex(~block_crc);
gecko_puts("\n");
}