mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
runtime: stop __divide / __multiply from stomping ZP_CURRENT_STATE
Root cause: `ZP_DIV_REMAINDER`, `ZP_MUL_RESULT_HI`, and `ZP_CURRENT_STATE` all live at `$03`. The divide routine was zeroing the byte on entry (`LDA #0; STA $03`) and writing the running remainder there on every one of its 8 iterations; the multiply routine accumulated its running product there. Any multi-state program doing `u8 / 10` in `on_frame` had its state ID clobbered on the way out of the routine — the next main-loop dispatch read `$03 == 0` (or whatever the remainder happened to be), matched state 0, and handed control to `Title` instead of the current state. The platformer's HUD hit this once per blink period: Playing survived exactly one frame, then Title took over for 20 frames, then the cycle repeated. Fix: rewrite both runtime routines to keep their running accumulators in register A instead of `$03`. The new contracts: - `__divide`: input `A = dividend`, `$02 = divisor`. Output `A = remainder`, `$04 = quotient`. The algorithm shifts the dividend-turning-into-quotient through `$04` (same as before) and rotates the extracted bits into `A`, comparing and subtracting directly without ever touching `$03`. - `__multiply`: input `A = multiplicand`, `$02 = multiplier`. Output `A = product` (low 8 bits — high byte discarded for `u8 * u8 → u8` as before, but not via a `$03` write). The multiplicand gets shifted left each iteration via `$04` and the running sum stays in `A`. `IrOp::Div` lowering gains one extra `LDA $04` after the JSR to pick up the quotient; `IrOp::Mod` loses the old `LDA $03` since the remainder is already in A. Net callsite cost is one instruction either way. Added two regression tests — `divide_routine_does_not_touch_zp_03` and `multiply_routine_does_not_touch_zp_03` — that walk the emitted instruction stream and fail loudly on any ZeroPage($03) access, so a future refactor can't silently reintroduce the alias. Rebuilt the three ROMs that use `/` or `*` (bitwise_ops, mmc1_banked, platformer) and re-baselined the platformer audio golden — the new instruction count shifts vblank-relative audio timing by a few cycles, as the CLAUDE.md audio-churn note warns. Pixel goldens and docs/platformer.gif stay byte-identical. The platformer HUD is back on native `stomp_count / 10` + `% 10`; the subtraction-loop workaround is gone. docs/future-work.md gains a new section describing the planned sprite-0 hit upgrade for the platformer HUD (carry-over task from the branch).
This commit is contained in:
parent
b83b944570
commit
91f82c169a
9 changed files with 160 additions and 108 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -496,36 +496,11 @@ var lives: u8 = 3 // lives remaining; HUD heart readout
|
|||
// so the HUD stays pinned while the background scrolls under it.
|
||||
// y = 16 is above the brick row, so the white digits read cleanly
|
||||
// against the universal-sky backdrop.
|
||||
// Split a 0..99 count into its tens and ones decimal digits via
|
||||
// repeated subtraction — the runtime's software divide has a known
|
||||
// issue that scribbles over state-local memory (tracked in
|
||||
// docs/future-work.md §G), and the HUD is hot enough that the
|
||||
// miscompile sends the state machine spinning. Manual repeated
|
||||
// subtraction is two dozen cycles for the worst case of a two-digit
|
||||
// score, which is cheap for a once-per-frame call.
|
||||
fun tens_digit(n: u8) -> u8 {
|
||||
var t: u8 = 0
|
||||
var r: u8 = n
|
||||
while r >= 10 {
|
||||
r -= 10
|
||||
t += 1
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
fun ones_digit(n: u8) -> u8 {
|
||||
var r: u8 = n
|
||||
while r >= 10 {
|
||||
r -= 10
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
fun draw_hud() {
|
||||
// Score side (left): coin + tens + ones.
|
||||
draw Tileset at: (16, 16) frame: TILE_COIN
|
||||
draw Tileset at: (28, 16) frame: TILE_DIGIT_0 + tens_digit(stomp_count)
|
||||
draw Tileset at: (36, 16) frame: TILE_DIGIT_0 + ones_digit(stomp_count)
|
||||
draw Tileset at: (28, 16) frame: TILE_DIGIT_0 + (stomp_count / 10)
|
||||
draw Tileset at: (36, 16) frame: TILE_DIGIT_0 + (stomp_count % 10)
|
||||
|
||||
// Lives side (right): heart + digit.
|
||||
draw Tileset at: (208, 16) frame: TILE_HEART
|
||||
|
|
@ -843,8 +818,8 @@ state GameOver {
|
|||
// final score and the remaining heart total are visible at
|
||||
// the exact same position as in Playing — no awkward jump.
|
||||
draw Tileset at: (16, 16) frame: TILE_COIN
|
||||
draw Tileset at: (28, 16) frame: TILE_DIGIT_0 + tens_digit(stomp_count)
|
||||
draw Tileset at: (36, 16) frame: TILE_DIGIT_0 + ones_digit(stomp_count)
|
||||
draw Tileset at: (28, 16) frame: TILE_DIGIT_0 + (stomp_count / 10)
|
||||
draw Tileset at: (36, 16) frame: TILE_DIGIT_0 + (stomp_count % 10)
|
||||
draw Tileset at: (208, 16) frame: TILE_HEART
|
||||
draw Tileset at: (224, 16) frame: TILE_DIGIT_0 + lives
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue