IOS LLE: checkpoint NAND conformance and diagnostic tooling

Preserve formatted NAND boot and Wii Remote pairing, add DMA/SRAM conformance tests and a Wii comparison probe. Keep P1 profiling tools and native update A/B evidence; reject the unhelpful targeted SRAM shortcut.
This commit is contained in:
Yaya48 committed 2026-09-23 13:30:17 +02:00
1 parent b931671fde
commit c230971f07
64 files changed
+6488 -238

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
build/
__pycache__/
*.elf
*.map
/build/
/apps/
/captures/
/__pycache__/
/wii-conformance.dol
/wii-conformance.elf
/wii-conformance.map
+45
View File
@@ -0,0 +1,45 @@
.SUFFIXES:
ifeq ($(strip $(DEVKITPPC)),)
$(error Set DEVKITPPC to your devkitPPC installation)
endif
include $(DEVKITPPC)/wii_rules
TARGET := wii-conformance
BUILD := build
SOURCES := source
CFLAGS := -O2 -g -Wall -Wextra $(MACHDEP) $(INCLUDE)
LDFLAGS = -g $(MACHDEP) -Wl,-Map,$(OUTPUT).map
LIBS := -lwiiuse -lbte -lfat -logc -lm
ifneq ($(BUILD),$(notdir $(CURDIR)))
export OUTPUT := $(CURDIR)/$(TARGET)
export VPATH := $(CURDIR)/source
export DEPSDIR := $(CURDIR)/$(BUILD)
export OFILES := main.o
export INCLUDE := -I$(LIBOGC_INC)
export LIBPATHS := -L$(LIBOGC_LIB)
export LD := $(CC)
.PHONY: all clean
all: | $(BUILD)
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
@mkdir -p apps/wii-conformance
@cp $(TARGET).dol apps/wii-conformance/boot.dol
@cp meta.xml apps/wii-conformance/meta.xml
$(BUILD):
@mkdir -p $@
clean:
@rm -rf $(BUILD) $(OUTPUT).elf $(OUTPUT).dol $(OUTPUT).elf.map
else
DEPENDS := $(OFILES:.o=.d)
$(OUTPUT).dol: $(OUTPUT).elf
$(OUTPUT).elf: $(OFILES)
-include $(DEPENDS)
endif
+59
View File
@@ -0,0 +1,59 @@
# Wii IOS conformance probe
This is the first black-box comparison probe for the original IOS running on a
real Wii and under this fork's Starlet LLE mode. Build **one** DOL and run that
same file in both places. It records 64 rounds of IPC-backed resource-manager
open/close calls in all four open modes, missing-device errors, root directory
counts, installed-title counts, and an SD write/read/hash/cleanup round trip.
The output contains 1,860 numbered observations. The probe requests no NAND
writes, installs no IOS, and does not blindly scan MMIO registers. It creates and
removes only `sd:/wii-conformance-data.bin` on the SD card.
## Build
Install devkitPPC and libogc/libfat using devkitPro, set `DEVKITPRO` and
`DEVKITPPC` in the build shell, then run `make` in this directory. The output is
`wii-conformance.dol`. On this Windows machine, the following command was used
successfully from this directory in PowerShell:
```powershell
& 'C:\devkitPro\msys2\usr\bin\bash.exe' -c 'export DEVKITPRO=/c/devkitPro DEVKITPPC=/c/devkitPro/devkitPPC; export PATH=/c/devkitPro/devkitPPC/bin:/c/devkitPro/tools/bin:/usr/bin:/bin; make'
```
The ready-to-copy Homebrew Channel folder is `apps/wii-conformance/`, containing
`boot.dol` and `meta.xml`. This build has not yet been run on a physical Wii.
## Capture
1. Use a Wii and a Dolphin LLE profile with the same IOS version and revision.
For exact directory/title counts, use the same NAND snapshot, starting state,
and loaded title. The SD cards need enough free space for a 4 KiB test file
and the CSV output.
2. Copy `apps/wii-conformance/` to the `apps/` folder on each SD card and launch
it through the Homebrew Channel. Use the exact same `boot.dol` on both runs.
Dolphin LLE requires a configured writable virtual SD image.
3. After the program returns, copy `sd:/wii-conformance.csv` from each SD card
to separate files such as `wii.csv` and `dolphin.csv`.
4. Compare them with `python compare.py wii.csv dolphin.csv`. Exit code 0 means
all recorded observations agree, 1 reports differences, and 2 means the
captures are invalid or use different IOS versions.
The CSV contains no keys or NAND contents. An allocated IOS file descriptor is
normalized to success/failure because its numeric value depends on unrelated
open handles. Call return codes and output counts remain exact. Time is not
compared in this first suite because scheduling and output I/O introduce noise.
`captures/` is kept local and ignored by Git until each capture's console or
emulator provenance, IOS revision, probe binary hash, and initial NAND state
are documented. A filename alone is not evidence that a capture came from a
physical Wii.
## Interpretation and next suites
A mismatch identifies a reproducible behavior to investigate; it does not by
itself locate a bad register. Trace the first failing request inside Dolphin's
Wii IPC, Starlet memory bus, and the relevant device. The next test suites
should use explicit stimulus/response cases for IPC flag acknowledgements,
shared-memory cache visibility, DMA, IRQ ordering, and individual devices.
PPC homebrew cannot inspect every Starlet register while Nintendo IOS is
running. A separate Starlet-side probe under MINI/BootMii can test raw
hardware registers, but its results must be labeled as MINI-mode tests.
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Compare two runs of the identical Wii conformance DOL."""
import argparse
import csv
import sys
from pathlib import Path
METADATA = ("schema", "rounds", "observations", "ios_version", "ios_revision")
HEADER = ["seq", "test", "round", "value", "extra"]
def read_capture(path: Path):
with path.open(newline="", encoding="utf-8") as stream:
rows = csv.reader(stream)
metadata = {}
for key in METADATA:
row = next(rows, None)
if row is None or len(row) != 2 or row[0] != key:
raise ValueError(f"{path}: expected metadata row {key!r}")
metadata[key] = int(row[1])
if next(rows, None) != HEADER:
raise ValueError(f"{path}: invalid observation header")
observations = []
for line, row in enumerate(rows, start=7):
if len(row) != 5:
raise ValueError(f"{path}:{line}: expected 5 columns")
seq, test, round_number, value, extra = row
if int(seq) != len(observations):
raise ValueError(f"{path}:{line}: sequence is not contiguous")
observations.append((test, int(round_number), int(value), int(extra)))
if len(observations) != metadata["observations"]:
raise ValueError(
f"{path}: expected {metadata['observations']} observations, "
f"found {len(observations)}"
)
return metadata, observations
def compare(reference, candidate, limit=20):
reference_meta, reference_rows = reference
candidate_meta, candidate_rows = candidate
for key in METADATA:
if reference_meta[key] != candidate_meta[key]:
print(f"Incomparable runs: {key}: Wii={reference_meta[key]}, Dolphin={candidate_meta[key]}")
return 2
differences = 0
for index in range(max(len(reference_rows), len(candidate_rows))):
expected = reference_rows[index] if index < len(reference_rows) else None
actual = candidate_rows[index] if index < len(candidate_rows) else None
if expected == actual:
continue
differences += 1
if differences <= limit:
print(f"#{index} Wii={expected} Dolphin={actual}")
if differences:
print(f"{differences} differences across {max(len(reference_rows), len(candidate_rows))} observations")
return 1
print(f"Identical: {len(reference_rows)} observations")
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("wii", type=Path, help="CSV from the real Wii")
parser.add_argument("dolphin", type=Path, help="CSV from Dolphin")
args = parser.parse_args()
try:
return compare(read_capture(args.wii), read_capture(args.dolphin))
except (OSError, ValueError) as error:
print(error, file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<app version="1">
<name>Wii IOS Conformance Probe</name>
<coder>Dolphin Emulator Project</coder>
<version>0.1</version>
<short_description>Compare IOS behavior on Wii and Dolphin</short_description>
<long_description>Runs repeatable IPC, IOS, and SD tests and saves wii-conformance.csv to the SD card.</long_description>
</app>
+172
View File
@@ -0,0 +1,172 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fat.h>
#include <gccore.h>
#include <ogc/es.h>
#include <ogc/ios.h>
#include <ogc/ipc.h>
#include <ogc/isfs.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// The same DOL must be run on the console and in Dolphin. Do not depend on
// timings or on allocated IOS file descriptor numbers in the comparison.
#define SCHEMA_VERSION 1
#define ROUNDS 64
#define RECORDS_PER_ROUND 29
#define SKIPPED (-32768)
#define SD_TEST_FILE "sd:/wii-conformance-data.bin"
static FILE* output;
static unsigned int sequence;
static void record(const char* test, unsigned int round, int value, int extra)
{
if (output)
fprintf(output, "%u,%s,%u,%d,%d\n", sequence, test, round, value, extra);
++sequence;
}
static void probe_open(const char* name, const char* path, unsigned int round, unsigned int mode)
{
char test[48];
const s32 fd = IOS_Open(path, mode);
snprintf(test, sizeof(test), "open_%s_mode%u", name, mode);
// File descriptor allocation can legitimately differ between runs.
record(test, round, fd >= 0 ? 0 : fd, 0);
snprintf(test, sizeof(test), "close_%s_mode%u", name, mode);
record(test, round, fd >= 0 ? IOS_Close(fd) : SKIPPED, 0);
}
static void probe_sd(unsigned int round)
{
static u8 expected[4096] __attribute__((aligned(32)));
static u8 actual[4096] __attribute__((aligned(32)));
const size_t length = 1 + ((round * 67) % sizeof(expected));
for (size_t i = 0; i < length; ++i)
expected[i] = (u8)(round * 131 + i * 17);
FILE* file = fopen(SD_TEST_FILE, "wb");
record("sd_open_write", round, file ? 0 : -1, (int)length);
record("sd_write_bytes", round, file ? (int)fwrite(expected, 1, length, file) : SKIPPED,
(int)length);
record("sd_close_write", round, file ? fclose(file) : SKIPPED, 0);
file = fopen(SD_TEST_FILE, "rb");
record("sd_open_read", round, file ? 0 : -1, 0);
const size_t read_count = file ? fread(actual, 1, length, file) : 0;
record("sd_read_bytes", round, file ? (int)read_count : SKIPPED, (int)length);
u32 hash = 2166136261u;
for (size_t i = 0; i < read_count; ++i)
hash = (hash ^ actual[i]) * 16777619u;
record("sd_read_hash", round, file ? (int)hash : SKIPPED, 0);
record("sd_data_equal", round,
file ? (read_count == length && memcmp(expected, actual, length) == 0) : SKIPPED, 0);
record("sd_close_read", round, file ? fclose(file) : SKIPPED, 0);
record("sd_remove", round, remove(SD_TEST_FILE), 0);
}
static void init_display(void)
{
VIDEO_Init();
GXRModeObj* mode = VIDEO_GetPreferredMode(NULL);
void* framebuffer = MEM_K0_TO_K1(SYS_AllocateFramebuffer(mode));
console_init(framebuffer, 20, 20, mode->fbWidth - 40, mode->xfbHeight - 40,
mode->fbWidth * VI_DISPLAY_PIX_SZ);
VIDEO_Configure(mode);
VIDEO_SetNextFramebuffer(framebuffer);
VIDEO_SetBlack(FALSE);
VIDEO_Flush();
VIDEO_WaitVSync();
if (mode->viTVMode & VI_NON_INTERLACE)
VIDEO_WaitVSync();
}
int main(void)
{
init_display();
printf("Wii IOS conformance probe v%d\n", SCHEMA_VERSION);
if (fatInitDefault())
output = fopen("sd:/wii-conformance.csv", "wb");
if (!output)
{
printf("Cannot write sd:/wii-conformance.csv\n");
printf("Insert a writable SD card and restart.\n");
for (int i = 0; i < 600; ++i)
VIDEO_WaitVSync();
return 1;
}
// Metadata is checked before comparing individual observations.
fprintf(output, "schema,%d\n", SCHEMA_VERSION);
fprintf(output, "rounds,%d\n", ROUNDS);
fprintf(output, "observations,%d\n", 4 + ROUNDS * RECORDS_PER_ROUND);
fprintf(output, "ios_version,%d\n", IOS_GetVersion());
fprintf(output, "ios_revision,%d\n", IOS_GetRevision());
fprintf(output, "seq,test,round,value,extra\n");
const s32 fs_init = ISFS_Initialize();
record("isfs_init", 0, fs_init, 0);
const s32 es_init = __ES_Init();
record("es_init", 0, es_init, 0);
for (unsigned int round = 0; round < ROUNDS; ++round)
{
// A missing resource is a useful stable IPC error-path test.
probe_open("missing", "/dev/wii_conformance_probe_absent", round, IPC_OPEN_NONE);
for (unsigned int mode = IPC_OPEN_NONE; mode <= IPC_OPEN_RW; ++mode)
{
probe_open("fs", "/dev/fs", round, mode);
probe_open("es", "/dev/es", round, mode);
}
if (fs_init >= 0)
{
u32 count __attribute__((aligned(32))) = 0;
const s32 result = ISFS_ReadDir("/", NULL, &count);
record("isfs_root_count", round, result, result >= 0 ? (int)count : 0);
}
else
{
record("isfs_root_count", round, SKIPPED, 0);
}
if (es_init >= 0)
{
u32 title_count __attribute__((aligned(32))) = 0;
const s32 title_result = ES_GetNumTitles(&title_count);
record("es_title_count", round, title_result,
title_result >= 0 ? (int)title_count : 0);
}
else
{
record("es_title_count", round, SKIPPED, 0);
}
probe_sd(round);
}
if (fs_init >= 0)
record("isfs_deinit", 0, ISFS_Deinitialize(), 0);
else
record("isfs_deinit", 0, SKIPPED, 0);
if (es_init >= 0)
record("es_deinit", 0, __ES_Close(), 0);
else
record("es_deinit", 0, SKIPPED, 0);
const int write_error = ferror(output);
const int close_error = fclose(output);
printf("%u observations. CSV: sd:/wii-conformance.csv\n", sequence);
printf("Write status: %d, close status: %d\n", write_error, close_error);
printf("Returning to loader in about 10 seconds.\n");
for (int i = 0; i < 600; ++i)
VIDEO_WaitVSync();
return write_error || close_error ? 1 : 0;
}
+48
View File
@@ -0,0 +1,48 @@
import contextlib
import io
import tempfile
import unittest
from pathlib import Path
from compare import compare, read_capture
def capture(version=58, value=0):
return ("schema,1\nrounds,64\nobservations,1\nios_version,%d\nios_revision,6176\n"
"seq,test,round,value,extra\n0,open_fs_mode0,0,%d,0\n" % (version, value))
class CompareTest(unittest.TestCase):
def test_identical_and_first_difference(self):
with tempfile.TemporaryDirectory() as directory:
wii = Path(directory) / "wii.csv"
dolphin = Path(directory) / "dolphin.csv"
wii.write_text(capture(), encoding="utf-8")
dolphin.write_text(capture(), encoding="utf-8")
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(compare(read_capture(wii), read_capture(dolphin)), 0)
dolphin.write_text(capture(value=-6), encoding="utf-8")
output = io.StringIO()
with contextlib.redirect_stdout(output):
self.assertEqual(compare(read_capture(wii), read_capture(dolphin)), 1)
self.assertIn("#0", output.getvalue())
def test_wrong_ios_version_is_incomparable(self):
with tempfile.TemporaryDirectory() as directory:
wii = Path(directory) / "wii.csv"
dolphin = Path(directory) / "dolphin.csv"
wii.write_text(capture(), encoding="utf-8")
dolphin.write_text(capture(version=80), encoding="utf-8")
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(compare(read_capture(wii), read_capture(dolphin)), 2)
def test_truncated_capture_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "short.csv"
path.write_text(capture().replace("observations,1", "observations,2"), encoding="utf-8")
with self.assertRaises(ValueError):
read_capture(path)
if __name__ == "__main__":
unittest.main()