Files
Yaya48andClaude Opus 5.5 6ede30f99e StarletBootstrapProbe: wait for StarOS's whole panic path
--expect-panic passed as soon as __panic_state was nonzero at the end of
a 4096-cycle slice. StarOS's handler stores the guard first, then the
code, then publishes the SBR1 record, so the run could stop with the path
half done. The probe now keeps running until the path is finished and
still is two slices later, and fails if the run budget ends first:
- __panic_state 1 and __panic_code 1;
- with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and
  EXI_OWNER 1 (handed back to the PPC);
- with --sbr1-absent: every word of the record area still zero.
The failure names the first unmet condition. The check is
StarOSPanicCheck.h, pure, with the SBR1 constants moved there, and
StarletProbePanicCheckTest covers each condition; each of 7 mutants of
the check fails a test. Runs that do not expect a panic are unchanged.

With StarOS's panic test image (eb930ee), interpreter and JIT: the
scenarios pass, two slices later than before. Three images derived from
it pass the previous probe and now fail: EXI kept by the Starlet
(--sbr1-record), __panic_code never stored, the record written although
absent (--sbr1-absent). (StarOS MMU-001 review of F1.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 00:20:23 +02:00

90 lines
3.2 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <string>
#include <fmt/format.h>
#include "Common/CommonTypes.h"
// What StarletBootstrapProbe requires of a finished StarOS panic path (StarOS ADR-0014 R6). The
// path sets __panic_state first, then __panic_code, then publishes the SBR1 boot record when
// there is one, so the first nonzero guard can show it half done: the probe keeps running until
// every condition holds. Words come through a callback, so the check is tested without a CPU.
namespace StarletBootstrapProbe
{
// StarOS's SBR1 boot record in MEM1 (StarOS crates/boot-contract, report.rs).
enum class SBR1Mode
{
None,
// --sbr1-record: the probe creates the record as the loader does.
Record,
// --sbr1-absent: no record; its area must stay zero.
Absent,
};
constexpr u32 SBR1_BASE = 0x00200000;
constexpr u32 SBR1_SIZE = 0x1000;
constexpr u32 SBR1_MAGIC = 0x53425231;
constexpr u32 SBR1_VERSION = 1;
// Word offsets.
constexpr u32 SBR1_EXI_OWNER = 0x0c;
constexpr u32 SBR1_RELAY_STATUS = 0x10;
constexpr u32 SBR1_STARLET_STATE = 0x18;
constexpr u32 SBR1_FATAL_REASON = 0x1c;
constexpr u32 SBR1_FAULT_CODE = 0x20;
constexpr u32 SBR1_RING_LENGTH = 0x2c;
constexpr u32 SBR1_RING_DROPPED = 0x30;
// Values.
constexpr u32 SBR1_EXI_PPC = 1;
constexpr u32 SBR1_EXI_STARLET = 2;
constexpr u32 SBR1_RELAY_JUMPING = 4;
constexpr u32 SBR1_STATE_PANIC = 5;
// __panic_state once the path is entered; __panic_code of a Rust panic.
constexpr u32 PANIC_ENTERED = 1;
constexpr u32 PANIC_CODE_RUST = 1;
// The first condition of a finished panic path that does not hold, or an empty string:
// - __panic_state 1 and __panic_code 1;
// - with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and EXI_OWNER 1 (PPC);
// - with --sbr1-absent: every word of the record area still zero.
// read_mem1 returns the big-endian MEM1 word at an address.
inline std::string UnmetPanicCondition(u32 panic_state, u32 panic_code, SBR1Mode sbr1,
const std::function<u32(u32)>& read_mem1)
{
if (panic_state != PANIC_ENTERED)
return fmt::format("__panic_state is {}, not {}", panic_state, PANIC_ENTERED);
if (panic_code != PANIC_CODE_RUST)
return fmt::format("__panic_code is {}, not {}", panic_code, PANIC_CODE_RUST);
if (sbr1 == SBR1Mode::Record)
{
const struct
{
const char* name;
u32 offset;
u32 expected;
} fields[] = {{"STARLET_STATE", SBR1_STARLET_STATE, SBR1_STATE_PANIC},
{"FATAL_REASON", SBR1_FATAL_REASON, 0},
{"EXI_OWNER", SBR1_EXI_OWNER, SBR1_EXI_PPC}};
for (const auto& field : fields)
{
const u32 value = read_mem1(SBR1_BASE + field.offset);
if (value != field.expected)
return fmt::format("SBR1 {} is {}, not {}", field.name, value, field.expected);
}
}
if (sbr1 == SBR1Mode::Absent)
{
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
{
const u32 value = read_mem1(SBR1_BASE + offset);
if (value != 0)
return fmt::format("SBR1 area word +{:#x} is {:#x} without a record", offset, value);
}
}
return {};
}
} // namespace StarletBootstrapProbe