Add Phase 10: code review findings as pending tasks

Second round of 4-agent parallel code review found several
critical bugs that were not caught in the first review:

Critical (game-breaking):
1. Screen RAM writes go to VIC registers, not DRAM — with
   $01=$35, writes to $D000 hit VIC-II registers, not screen
   RAM. Also disables raster IRQ ($D01A overwritten). Works in
   oscar64 emulator (doesn't simulate banking).
2. vic_setup_mcm() clears RST8 on every screen change — moves
   raster IRQ from line 311 to line 55 (visible display).
3. $D41B random still frozen — audio_stop() re-zeroes voice 3
   freq; NOISE waveform bit never set. WAIT always 100 frames.
4. Makefile: $(PRG) has no rule — make run-vice fails on clean
   checkout.
5. Makefile: .PHONY lists non-existent targets.

Medium (spec deviations, edge cases):
- DRAW fault >500 vs >=500 (1 frame off)
- Counter minimum value 2 not 1
- WAIT upper bound 249 not 250
- DRAW strobe 5 frames not 4
- GAMEOVER resets scores immediately (spec: show final score)
- ADSR decay comments inconsistent with constants
- clear_color_ram() 4x redundant
- memmap_setup() redundant MMAP_RAM call
- Makefile: no VICE/c1541/$DISPLAY checks, setsid PID fragile,
  parallel race, oscar64 guard duplicated

Low (cosmetic, docs):
- Doc 'WHACKED' references, font array signedness,
  memmap_restore dead code, PROG_C64 banking oversimplification,
  Makefile clean @ prefix, undocumented intermediates

No fixes applied — these are tasks for a future Phase 10 work
session.
This commit is contained in:
ballz
2026-07-18 18:36:22 +02:00
parent f0c2ca07f2
commit 97d4f4216b
+216 -1
View File
@@ -360,7 +360,222 @@ These items were done after Phase 8 was marked complete:
--- ---
## Phase 9(Optional) extra features ## Phase 10Code review fixes (pending)
Findings from the second round of 4-agent parallel code review.
**Do not apply fixes yet — these are tasks to be worked through.**
### Critical (game-breaking bugs)
- [ ] **Screen RAM writes go to VIC registers, not DRAM**
`src/screens.c:182` (`copy_bytes(s->attr, (char *)0xD000, 1000)`),
`src/screens.c:198-200` (`show_white_screen` clears `$D000-$D3E7`),
and `src/score.c:154-165` (`setup_score_bar_cells` writes
`SCORE_SCREEN_BASE[i] = 0` for i=0..39 to `$D000-$D027`).
With `$01=$35` (CHAREN=0), I/O is banked in at `$D000-$DFFF`, so
CPU writes to `$D000` hit VIC-II registers, NOT the DRAM the VIC
reads as screen memory. Effect on real hardware: (a) screens
display garbage (`.attr` data never reaches screen RAM), (b)
`$D01A` (`vic.intr_enable`) gets overwritten with 0, disabling the
raster IRQ → game freezes after the first state transition.
Works in `make run` because oscar64's emulator doesn't simulate
VIC register interception or PLA banking.
**Fix:** relocate screen RAM to a CPU-writable address under `$35`
(e.g. `$0400` or `$C000`), update `vic.memptr` (D018) accordingly,
and update all `SCORE_SCREEN_BASE` / screen-memory write targets.
- [ ] **`vic_setup_mcm()` clears RST8 on every screen change** —
`src/screens.c:160` writes `vic.ctrl1 = 0x38` (BMM|DEN|RSEL),
bit 7 (RST8) = 0. `tick.c:123` only sets RST8 once at boot.
Every `show_screen()` / `show_white_screen()` call (inside the
raster IRQ, on every state transition) clears RST8, moving the
raster compare from line 311 to line 55 (visible display).
Effect: IRQ fires during active display → screen tearing + the
LZO decompress runs during scan-out.
**Fix:** preserve RST8 in `vic_setup_mcm()`:
`vic.ctrl1 = VIC_CTRL1_RST8 | VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL;`
or re-assert `vic.ctrl1 |= VIC_CTRL1_RST8;` after every screen change.
- [ ] **`$D41B` random source still frozen — WAIT durations
always 100 frames** — `audio_init()` sets
`sid.voices[2].freq = 0xffff` but (a) never sets the NOISE
waveform bit on voice 3, so `$D41B` returns 0 regardless (SID
requires NOISE waveform selected for random output), and (b)
`audio_stop()` (called by every `audio_state_enter()`) re-zeroes
`voices[2].freq`, undoing the fix on the very first state
transition. The KNOWN_ISSUES.md #5 "fix" is ineffective on real
hardware. `game_enter_ready()` samples `sid.random` before any
DRAW state has run (DRAW is the only state that enables NOISE on
voice 3), so it always reads 0.
**Fix:** in `audio_init()`, also set
`sid.voices[2].ctrl = SID_CTRL_NOISE;` (no GATE → silent, LFSR
runs). Best: exempt voice 3 from `audio_stop()` entirely (don't
zero `voices[2].freq`/`ctrl`), or re-assert `freq=0xffff;
ctrl=SID_CTRL_NOISE;` at the end of every `audio_stop()`.
- [ ] **Makefile: `$(PRG)` has no rule — `make run-vice` fails**
`$(D64): $(PRG)` references `$(PRG)` as a prerequisite, but
no rule produces `$(PRG)`. Only the phony `compile` target
creates it as a side effect. `run-vice` / `run-vice-cycle`
only depend on `$(D64)`, not `compile`. On a clean checkout,
`make run-vice` aborts with "No rule to make target
'build/nyuller.prg'". `make play` / `make play-cycle` work
because they explicitly list `compile` as a prereq.
**Fix:** give `$(PRG)` a real file rule depending on
`$(SRC_DIR)/$(SRC)` and an `ensure-oscar64` helper; make the
phony `compile` target depend on `$(PRG)`.
- [ ] **Makefile: `.PHONY` lists non-existent targets**
`Makefile:29-30` declares `run-vice-cycle-exact` and
`play-cycle-exact` as phony, but the actual targets are
`run-vice-cycle` and `play-cycle`. The real targets are not
declared phony.
**Fix:** change `.PHONY` to:
`help compile run run-vice run-vice-cycle play play-cycle kill clean ensure-build-dir`
### Medium (spec deviations, edge cases, robustness)
- [ ] **DRAW fault uses `> 500` instead of `>= 500`**
`src/game.c:393`. All other state timeouts use `>=`
(READY `>= 60`, WIN `>= 100`, GAMEOVER `>= 300`). DRAW fault
fires at elapsed=501 (501 visible frames = 10.02 sec) instead
of elapsed=500 (500 frames = 10.00 sec). 1 frame longer than
spec and inconsistent with the rest of the codebase.
**Fix:** change `> 500` to `>= 500`, or document the choice.
- [ ] **Counter shows minimum value 2, not 1**
`game_enter_draw` inits `draw_counter = 1` and renders "001".
`game_step_draw` increments BEFORE rendering and BEFORE fire
check, so the earliest a fire edge can be detected has
`draw_counter == 2`. A perfect 1-frame draw reports "2", not
"1". Spec (GAME.md §3) implies a perfect draw should show 001.
**Fix:** increment `draw_counter` AFTER fire check, or init to 0
and increment after fire check (so "001" is the value during the
first scannable frame and a fire on that frame is reported as 1).
- [ ] **WAIT duration upper bound is 249, not 250**
`src/game.c:196`: `100 + (sid.random % 150)` produces [100, 249].
Spec says 100..250 frames (2.05.0 sec).
**Fix:** use `100 + (sid.random % 151)` for [100, 250], or
document the exclusive upper bound.
- [ ] **DRAW border strobe is 5 frames, not 4**
`src/game.c:377-380`: `if (elapsed < 5)` strobes for elapsed
0..4 = 5 visible frames (W/B/W/B/W). Comment says "first 4
frames"; spec says "4-frame stab".
**Fix:** change `< 5` to `< 4`, or update comment/spec to "5".
- [ ] **GAMEOVER resets scores immediately, spec says show final
score** — `game_enter_gameover` (game.c:273-274) resets
`score_p1 = score_p2 = 0` on entry. GAME.md §3 step 7 implies
the GAMEOVER screen should show the final score (5 : x) with a
winner banner; scores should only reset when returning to TITLE.
**Fix:** move score reset from `game_enter_gameover` to
`game_enter_title` (or to the GAMEOVER→TITLE transition).
- [ ] **ADSR decay comments inconsistent with constants**
`src/audio.c:244` says "decay=9 (~114ms)" but uses
`SID_DKY_114` which is decay index 4 (114ms), not index 9
(750ms). `src/audio.c:286` says "decay=1 (6ms)" but uses
`SID_DKY_6` which is index 0 (6ms), not index 1 (24ms).
**Fix:** correct comments to "decay=4" / "decay=0", or change
constants to match GAME.md §6 if the spec's "9"/"1" refer to
register values.
- [ ] **`clear_color_ram()` is 4x redundant** —
`src/screens.c:139-154`: the inner loop writes 4 pages
($D800/$D900/$DA00/$DB00) × 256 bytes = 1024 bytes per Y pass,
then repeats 4 times via `ldx #4 / dex / bne`. Total 4096
stores instead of 1024 (~16ms instead of ~4ms). Functionally
correct (idempotent) but slower than documented.
**Fix:** drop the outer `ldx #4` loop, or change to `ldx #1`.
- [ ] **`memmap_setup()` redundant `MMAP_RAM` call** —
`src/memmap.c:6` sets `$01=$30`, then line 7 immediately
overwrites with `$01=$35`. The `$30` window does no useful
work. Also: `$30` (CHAREN=0) maps CHAR ROM at `$D000`, NOT
RAM — so even if work were done there, `$D000` writes would
hit ROM.
**Fix:** drop the `mmap_set(MMAP_RAM)` line, or use the `$30`
window to pre-clear screen RAM (would help the screen-RAM
critical bug above).
- [ ] **Makefile: no VICE binary check**`make run-vice`,
`make play`, etc. don't check if `x64`/`x64sc` is in PATH.
Old `build.sh` had `command -v "$PLAY_VICE"`.
**Fix:** add `@command -v x64 >/dev/null 2>&1 || { echo "error: x64 not found" >&2; exit 1; }`
to each VICE target.
- [ ] **Makefile: no `$DISPLAY` check** — old `build.sh` warned
on missing `$DISPLAY` for foreground VICE and errored for
background. Makefile omits both. On headless, `make play`
silently launches x64 with no window.
**Fix:** add `@[ -n "$$DISPLAY" ] || { echo "error: no \$DISPLAY" >&2; exit 1; }`
to VICE GUI targets.
- [ ] **Makefile: `setsid` PID capture is fragile**
`$$!` captures the `setsid` wrapper PID, not the `x64` child.
`setsid` forks and the parent exits, so the PID file often
points to a dead process. `make kill` uses `pgrep` (correct),
but the `kill -0` "did it start?" check in `play`/`play-cycle`
may falsely report failure.
**Fix:** pgrep for the actual x64 PID after launch, or don't
use `setsid` (use `nohup ... & disown`).
- [ ] **Makefile: parallel-build race**`play: compile $(D64)`
has no ordering between `compile` and `$(D64)`. Under
`make -j`, `$(D64)` may start before `compile` writes
`$(PRG)`. Root cause same as `$(PRG)` having no rule.
**Fix:** same as critical #4 — make `$(PRG)` a real file target.
- [ ] **Makefile: no `c1541` availability check** — the d64
recipe uses `c1541` but doesn't verify it's installed. Old
`build.sh` had `command -v c1541`.
**Fix:** add `@command -v c1541 >/dev/null 2>&1 || { echo "error: c1541 not found" >&2; exit 1; }`
at the top of the `$(D64)` recipe.
- [ ] **Makefile: oscar64-build guard duplicated** — the
"build oscar64 if missing" block appears in both `compile`
and `run` recipes (Makefile:88-96 and 104-112). DRY
violation.
**Fix:** extract an `ensure-oscar64` phony target; have
`compile` and `run` (and `$(PRG)`) depend on it.
- [ ] **WAIT transition stinger uses voice 2 (the RNG source)**
`src/game.c:123` picks voice 2 for the WAIT stinger. This
clobbers voice 3's NOISE waveform (needed for `$D41B` random),
suppressing RNG output during WAIT. Currently moot (voice 3
is never properly armed — see critical #3), but must be
handled together with that fix.
**Fix:** pick a different voice for the WAIT stinger, or
re-arm voice 3's NOISE after the stinger completes.
### Low (cosmetic, doc, defensive)
- [ ] **Doc-only "WHACKED" references**`GAME.md:33` and
`src/AGENT_CONTEXT.md:142` describe the title screen as
showing a "WHACKED" logo. The image asset still says this
(visual, not code). Update docs or update the PNG.
- [ ] **`font` array uses `char` instead of `unsigned char`** —
`src/score.c:36`, `src/banner.c:29`. Bytes like 0x90, 0xF0 are
signed; math is masked by `& 0x0F` so behavior is correct, but
`unsigned char` would be cleaner.
- [ ] **`memmap_restore()` is dead code** — never called; main
loop is infinite. Document as intentional or wire up an exit
path (e.g. NMI handler).
- [ ] **PROG_C64.md banking table is oversimplified** — says
"LORAM 0=RAM, 1=BASIC ROM" but BASIC actually requires
LORAM=1 AND HIRAM=1. The `$BC00` fix relies on HIRAM=0.
- [ ] **Makefile: `clean` first line lacks `@` prefix**
echoes the long `rm -f ...` command. Minor inconsistency.
- [ ] **Makefile: undocumented intermediate files**
`clean` removes `nyuller.int`, `nyuller.dbj`, `nyuller.csz`
but `help` doesn't list them as output files.
- [ ] **Makefile: `make OPT=` (empty) produces `-` flag**
edge case. Guard with `OPT_FLAGS := $(if $(OPT),-$(OPT))`.
- [ ] **Audio schedule off-by-one** (already documented in
KNOWN_ISSUES.md #3) — WIN/GAMEOVER notes are 1 frame short per
note (~10% deviation). Cosmetic.
Pick from these based on time and interest. None of these are Pick from these based on time and interest. None of these are
required for the game to be done. required for the game to be done.