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
9 changes: 9 additions & 0 deletions c64cast/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1325,12 +1325,21 @@ def _launch_sid_player(
MC at $C300 survives) and RUNs the stub; BASIC's SYS jumps to the player
MC, which installs the IRQ and spins forever (never re-entering BASIC).

Blanks the display first (same guard as `reset()`): run_prg's reset has
its own reset-latency window during which the VIC still holds the
OUTGOING scene's mode/bank, so without this a bitmap/hires scene (e.g.
another waveform or a video scene) flashes its leftover bitmap RAM until
the kernal reinitializes VIC and, in turn, `_setup_hires()` re-engages
the scope. This is the same class of glitch `reset()`'s pre-blank fixes;
`run_prg` here is a parallel reset path that needs the same guard.

`avoid` is unused here (no trampoline to place). `defer_audio` is ignored:
run_prg is a synchronous reset+RUN that also re-inits VIC to text mode, so
there's no loaded-but-silent window to hold — audio starts here. Returns
True so `run_sid_player` runs the standard finalize (timestamp + divider).
WaveformScene's `begin_sid_audio()` is then a no-op, and it (re)asserts the
bitmap display *after* this call as it always has."""
self.blank_display()
self._write_sid_blobs(parsed, layout, mc, reinit)
self.flush()
basic_stub = _build_basic_sys_stub(layout.player_base)
Expand Down
13 changes: 13 additions & 0 deletions docs/caveats.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ screen. The kernal IRQ keeps firing regardless, so PLAY runs at the
system rate and `$028D` keeps updating for the keyboard poller.
Audio still comes from the real SID chip.

**Pre-blank before the kick (Ultimate only).** `runners:run_prg` soft-resets
the C64, and like any reset it has a reset-latency window during which the
VIC still holds the *outgoing* scene's mode/bank/bitmap — so without a guard,
launching the SID player over a previous hires/mhires scene (another
waveform, or a video/generative bitmap scene) briefly flashes that scene's
leftover bitmap RAM before the kernal reinitializes VIC and `WaveformScene`
re-engages its own bitmap mode. `Ultimate64API._launch_sid_player` blanks the
display (`blank_display()`, DEN off) immediately before DMA'ing the SID
blobs, the same guard `reset()` uses for the same reason. The TeensyROM
backend never does this (see the vector-swap note below): its "kick" doesn't
reset the machine, and turning DEN off there would stall the cycle-clean DMA
gate.

### Per-call memory banking (`$01`)

The player banks the 6510 CPU port at `$0001` **per call**, matching the
Expand Down
19 changes: 15 additions & 4 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
_PlayerLayout,
parse_psid_for_player,
)
from c64cast.c64 import CPU, VECTORS
from c64cast.c64 import CPU, VECTORS, VIC
from c64cast.socket_dma import SocketDMAError


Expand Down Expand Up @@ -177,10 +177,21 @@ def setUp(self):
self.api = Ultimate64API("http://example.invalid")
# Stub the wire-level write + flush + REST POST so the test runs
# against in-process state only.
# dma_writes tracks the SID-upload contract this class tests (payload
# / player MC / re-INIT stub); the pre-flight blank_display() write to
# $D011 (see _launch_sid_player) is recorded separately since it's
# orthogonal to that contract and would otherwise shift every
# index-based assertion below.
self.dma_writes: list[tuple[int, bytes]] = []
self.api._emit = lambda addr, payload: ( # type: ignore[method-assign]
self.dma_writes.append((addr, bytes(payload)))
)
self.blank_writes: list[tuple[int, bytes]] = []

def _fake_emit(addr, payload):
if addr == VIC.D011_CONTROL_1:
self.blank_writes.append((addr, bytes(payload)))
else:
self.dma_writes.append((addr, bytes(payload)))

self.api._emit = _fake_emit # type: ignore[method-assign]
patch.object(self.api, "flush").start()
self.posts: list[tuple[str, bytes]] = []

Expand Down