Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **AccuracySNES: `C7.06` — Time Over reads set by `V = OBJ.YLOC + 1, H = 0`.** `C7` on-cart

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the unsupported H = 0 precision.

The C7.06 test samples $213E at an IRQ service point on V = 101. It does not assert that Time Over becomes set at H = 0. The plan explicitly treats (101, 0) as one permitted implementation, not as the measured position.

Proposed wording
-- **AccuracySNES: `C7.06` — Time Over reads set by `V = OBJ.YLOC + 1, H = 0`.**
+- **AccuracySNES: `C7.06` — Time Over reads set by `V = OBJ.YLOC + 1`.**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **AccuracySNES: `C7.06` — Time Over reads set by `V = OBJ.YLOC + 1, H = 0`.** `C7` on-cart
- **AccuracySNES: `C7.06` — Time Over reads set by `V = OBJ.YLOC + 1`.**
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 14, Update the AccuracySNES C7.06 changelog entry to
remove the unsupported “H = 0” claim, while retaining the measured V = OBJ.YLOC
+ 1 behavior and the IRQ service-point context. Do not present (101, 0) as the
asserted timing position.

Source: Path instructions

coverage 7 → 8 of 16.

**8x8 sprites can never reach Time Over.** The budget is 34 tiles per line but range evaluation
stops at the 33rd in-range sprite, so 8x8 caps at 32 — under the limit, permanently. Twenty 16x16
sprites give 40 tiles while staying under the 32-sprite range limit, which also keeps the two flags
independently observable: the row asserts Time Over sets while Range Over stays **clear**, so a
core raising them together fails.

The bracket runs across the line boundary rather than within a line, since the asserted position is
fixed and there is no index to sweep — clear on the eval line `V = 100`, set on `V = 101`. Phase B
samples on line 101 deliberately: RustySNES raises the flag at `HBLANK_START_DOT` of the eval line,
*earlier* than the assertion requires, and pinning that dot would fail a core raising it at
`(101, 0)` exactly, which the assertion permits.

A low-tile control (the same 20 sprites at 8x8, 20 tiles) must read clear at phase B's sampling
point, or "set" would only have meant "sprites are present". Four injections, each failing its own
code: flagging on any sprite fails code 7 (the control), never setting fails code 5, and setting at
dot 0 fails code 2.

snes9x fails it and is recorded as an expected divergence (`SNES9X_KNOWN_FAILURES` 13 → 14): it
already reads Time Over set on `V = 100`, flagging the overflow a line early because it evaluates
and paints in one pass. The control passes there, so the position is wrong, not the budget.

- **AccuracySNES: `C7.05` — Range Over trips at the 33rd in-range sprite's evaluation cycle,
`H = OAM.INDEX * 2`.** A fixed bracket would be vacuous: an H-IRQ is serviced ~22-27 dots after its
`HTIME`, so one tight enough to pin dot 65 is inside the latency's own uncertainty and one loose
Expand Down
4 changes: 2 additions & 2 deletions docs/accuracysnes-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of
| `C4` | 5 | 0 | 5 | — |
| `C5` | 15 | 0 | 12 | C5.06, C5.07, C5.15 |
| `C6` | 7 | 0 | 6 | C6.07 |
| `C7` | 16 | 7 | 6 | C7.06, C7.07, C7.12 |
| `C7` | 16 | 8 | 6 | C7.07, C7.12 |
| `C8` | 12 | 0 | 11 | C8.09 |
| `C9` | 8 | 2 | 0 | C9.01, C9.02, C9.03, C9.06, C9.07, C9.08 |
| `C10` | 5 | 0 | 3 | C10.03, C10.04 |
Expand All @@ -52,7 +52,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of
| `F1` | 22 | 13 | 0 | F1.13, F1.15, F1.16, F1.17, F1.18, F1.19, F1.20, F1.21, F1.22 |
| `G1` | 18 | 15 | 0 | G1.06, G1.13, G1.18 |

**293 of 443** enumerated assertion rows covered by an on-cart test, plus **53** covered only by a rendered scene (`docs/adr/0013`) — **346 of 443** in total.
**294 of 443** enumerated assertion rows covered by an on-cart test, plus **53** covered only by a rendered scene (`docs/adr/0013`) — **347 of 443** in total.

The two columns are kept apart on purpose. An on-cart result means the same thing on any emulator and on real hardware; a rendered scene needs a host holding the golden. Adding them into one figure would quietly change what the number claims.

Expand Down
55 changes: 54 additions & 1 deletion docs/accuracysnes-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ before the H counter wraps and silently returns a plausible small number.
| `A5.18` | **parked** — needs a cheaper instrument | The design is sound and the numbers came out exactly as predicted, but a `BRK` round trip is 39 dots and only 3 fit inside the budget, leaving 6 dots of signal against `TOL` 2 in a difference-of-differences. See below. |
| `A6.15` | needs its own design | "all 256 opcodes defined" is a *definedness* assertion, not a timing one, and `sweep.rs` deliberately covers only the unambiguous subset — its own module docs list why control flow, `BRK`/`COP`, `STP`, `WAI` and memory-addressing modes are excluded. Citing the sweep would be coverage by restatement. |
| `C7.05` | **landed** | Not a fixed bracket — the set dot has to *move with the index*. See below. |
| `C7.06` | reachable, same technique | `C7.05`'s far-IRQ shim and sprite setup are reusable; `time_over` needs >34 sprite-tiles, which 40 8x8 sprites do **not** produce (range evaluation caps at 32, so 32 tiles). Use larger sprites. |
| `C7.06` | **landed** | 8x8 sprites can *never* reach Time Over — evaluation stops at 32 sprites, so 32 tiles is the ceiling against a 34 budget. Twenty 16x16 sprites give 40 tiles while staying under the range limit, which also keeps the two flags independently observable. |
| `B2.07` | gated on `B2.02` | `B2.04` covers the 262-line count, not the clock total, so the frequency is not implied by it. Getting to 60.0988 Hz needs clocks-per-frame, and the frame alternates 357,368/357,364 because of the short scanline — which *is* `B2.02`, a `T-06-A` dot-model residual scheduled for `v1.29.0`. |
| `B2.09` | `v1.29.0` by its own dossier note | the picture window is "not CPU-observable directly; reachable through the framebuffer oracle once the dot-resolution compositor lands". |

Expand Down Expand Up @@ -540,6 +540,59 @@ its Range Over does not track the index — a scanline-granularity flag. RustySN
anchored to MesenCE on the *line* by `scripts/probes/eval-line-213e`; the *dot* is
documentation-anchored only, because the Mesen2 headless runner times out in this environment.

#### The two `C7` rows still uncovered, and why the new machinery does not reach them

`C7.05`/`C7.06` landed on the far-IRQ shim, so it is worth saying what that shim does **not**
unblock.

- **`C7.12`** (16x32 under OBJ interlace renders as 16x16) is a **scene**, and needs the cart to
publish a scene only on a *known field* — it was written once and produced three different hashes
on three emulators because it asked a question whose answer alternates every frame. That is
`v1.29.0` machinery (`run_scenes`, not a scene), shared with `C9.03`/`C9.06`.

- **`C7.07`** (Time Over false positive: first sprite 16x16+ at `X = 0-255` with others at negative
X) is reachable *as a test* with `C7.06`'s setup, but it would fail — because RustySNES does not
model the errata. `compute_over_flag_dots` skips a fully-offscreen sprite outright
(`obj.x > 256 && obj.x + w - 1 < 512`), before either counter, so no false Time Over can arise.
The row therefore needs a **PPU behaviour change first**, not a cart test.

That change should not be made on this evidence. It rests on a single errata line, and the only
available cross-check is snes9x — which is already known to be scanline-granular here and fails
both `C7.05` and `C7.06` for that reason, making it a poor arbiter for a third sprite-flag quirk.
Mesen2 cannot arbitrate while its headless runner times out in this environment. Encoding an
unvalidated behaviour attribution is what the `$F8`/`$F9` retraction in the CHANGELOG exists to
warn against. Get a working Mesen2 (or another oracle) first.

#### `C7.06` — 8x8 sprites cannot reach Time Over at all

The tile budget is 34 per line, but range evaluation **stops** at the 33rd in-range sprite, so 8x8
sprites cap out at 32 tiles — under the limit, permanently. `C7.05`'s forty 8x8 sprites set Range
Over and leave Time Over clear, and the `eval-line-213e` probe reports the same for the same reason.
Twenty 16x16 sprites give 40 tiles while staying under the 32-sprite range limit, so Time Over trips
and Range Over must **not** — which the row asserts, since a core raising the two together fails it.

The bracket is across the line boundary rather than within a line, because the asserted position is
fixed and there is no index to sweep: clear on the eval line `V = 100`, set on `V = 101 = YLOC + 1`.
Phase B deliberately samples on line 101 rather than late on line 100 — RustySNES raises the flag at
`HBLANK_START_DOT` of the eval line, *earlier* than the assertion requires, and pinning that exact
dot would fail a core that raises it at `(101, 0)` exactly, which the assertion permits.

A low-tile control completes it: the same 20 sprites at 8x8 (20 tiles) must read Time Over **clear**
at phase B's sampling point. Without it, a core reporting Time Over whenever any sprite is on the
line passes both A and B. Four injections, each failing its own code — flag on any sprite fails **code 7** (the control),
never setting fails **code 5**, and setting at dot 0 fails **code 2**. Codes 1, 3 and 4 are the
per-phase sentinels below.

Each phase also carries a **sentinel** on the sampled byte (codes 1, 3, 4). Without it a never-fired
IRQ leaves the poison `$FF` in the slot, and `$FF` satisfies or violates the flag tests by accident —
phase A would blame "Time Over already set", phase B would pass bit 7 and then blame "Range Over set
alongside". Both name a cause that did not happen, and `ERROR_CODES.md` is supposed to be the
complete account of failure bytes.

snes9x fails this row too, and is recorded as an expected divergence: it already reads Time Over set
on `V = 100`, flagging the overflow a line early because it evaluates and paints in one pass. The
control passes there, so it is the position that is wrong, not the budget.

#### `C7.05`/`C7.06` — the probe, and the two sampling traps

An earlier revision of the table above said RustySNES sets `range_over` a line late against MesenCE.
Expand Down
9 changes: 8 additions & 1 deletion scripts/accuracysnes/crossval.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@ ran=0
# (range evaluation walks OAM two cycles per sprite). RustySNES's per-dot position is anchored to
# MesenCE on the *line* by scripts/probes/eval-line-213e; the *dot* itself is documentation-anchored
# only, because the Mesen2 headless runner times out in this environment. Region-independent.
SNES9X_KNOWN_FAILURES=13
# snes9x, +1 test (C7.06 "TimeOver by YLOC+1", fails phase A with code 1): the same scanline-granular
# sprite model as C7.05, seen from the other side. The row requires Time Over to be clear while the
# sprites' OWN line is being evaluated (V = 100) and set once the line they paint on has begun
# (V = 101 = YLOC + 1); snes9x already reads it set on V = 100, i.e. it flags the tile-budget
# overflow a line early because it evaluates and paints in the same pass. The 20-sprite/8x8 control
# passes there, so this is the position being wrong, not the budget. Documented by nocash fullsnes
# and the SNESdev Wiki (34-tile budget, raised by the fetch phase). Region-independent.
SNES9X_KNOWN_FAILURES=14
Comment on lines +159 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a traceable source citation.

The known failure is recorded correctly, but the comment only names “fullsnes” and “the SNESdev Wiki.” Add the exact URL and section or page for the 34-tile budget and fetch-phase timing. This lets reviewers distinguish a documented reference divergence from an accidental expected failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/accuracysnes/crossval.sh` around lines 159 - 166, Update the C7.06
known-failure comment adjacent to SNES9X_KNOWN_FAILURES to include exact URLs
and section or page references for nocash fullsnes and the SNESdev Wiki covering
the 34-tile budget and fetch-phase timing. Preserve the existing failure
description and ensure the citations clearly support the documented timing
divergence.

Source: Path instructions


# --- snes9x, via the libretro host --------------------------------------------------------------
if [[ -f $SNES9X ]]; then
Expand Down
14 changes: 14 additions & 0 deletions tests/roms/AccuracySNES/ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,20 @@ Provenance: **Documented** (fullsnes and the SNESdev Wiki: range evaluation walk
| 2 | `$04` | Range Over was already set at dot ~105 with the 33rd in-range sprite at OAM index 72 (dot 145) — the set dot does not track OAM.INDEX * 2 |
| 3 | `$06` | Range Over never set at all in phase B, so its clear reading at dot ~105 proved nothing |

### C7.06 — TimeOver by YLOC+1

Provenance: **Documented** (fullsnes and the SNESdev Wiki: the sprite tile budget is 34 per line and Time Over is raised by the fetch phase, so it reads set by the start of the line the sprites paint on). Kind: scored.

| Code | Byte | Meaning |
|---|---|---|
| 1 | `$02` | the H/V IRQ never fired, so STAT77 was never sampled at all |
| 2 | `$04` | Time Over was already set on the sprites' own line (V = 100), earlier than V = YLOC + 1 |
| 3 | `$06` | the H/V IRQ never fired, so STAT77 was never sampled at all |
| 4 | `$08` | the H/V IRQ never fired, so STAT77 was never sampled at all |
| 5 | `$0A` | Time Over had not set by V = YLOC + 1 with 40 sprite-tiles due on the line |
| 6 | `$0C` | Range Over set alongside Time Over with only 20 sprites on the line — the two flags are being raised together instead of on their own conditions |
| 7 | `$0E` | Time Over set with only 20 sprite-tiles due, so phase B's reading meant 'sprites are present', not 'the tile budget was exceeded' |

### C7.02 — Time Over is slivers

Provenance: **Documented** (SNESdev Wiki, Sprites; fullsnes; anomie). Kind: scored.
Expand Down
Loading
Loading