Skip to content

Lock WriteC64Spans slices to a sample store, and count the stores - #71

Merged
SensoriumEmbedded merged 5 commits into
SensoriumEmbedded:mainfrom
kfox:nmi-locked-spans
Oct 26, 2026
Merged

SensoriumEmbedded merged 5 commits into
SensoriumEmbedded:mainfrom
kfox:nmi-locked-spans

Conversation

@kfox

@kfox kfox commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

What this does

A C64 playing samples through $D418 from a CIA2 timer NMI loses a sample whenever one DMA halt spans two timer underflows: the NMI is edge-triggered, so the two collapse into one. WriteC64Spans already slices writes to shorten halts, but each slice still costs 70–80 cycles of thread-mode handshake, which is close to a whole NMI period at 12 kHz (85 cycles).

This adds a lock-to-store flag (0x01) to WriteC64Spans:

  • The first span entry names the player's sample store ({ $D418, 0 }), and the gap byte becomes the longest wait for a store.
  • Each slice starts on the cycle after the C64 next writes that address. It is sized by the sender to the cycles the player leaves free before the next underflow.
  • The interrupt handler runs assert → transfer → release itself (DMA_AutoRun), so the handshake no longer sits inside the halt.
  • A slice whose store does not arrive within the wait starts anyway. A stopped player costs time; it cannot hang a write.

It also adds SampleWatch (0x64F9). It counts the C64's writes to one address and reports intervals, total and longest interval, and locked/unlocked slice counts since the previous call. With the store's period, a host can count lost samples at the source.

docs/ControlComms.md documents both, including the probe: a flagged zero-span header is refused with Want 1 to 64 spans by this firmware and Flags must be 0 by older firmware, with nothing owed either way.

Measured

On an NTSC C64 with a 12 kHz NMI and 33-byte locked slices:

  • NMIs lost: 0.00% at 64–1024 B writes, 48 writes/s. Unsliced WriteC64Mem lost 0.27–5.06% at the same sizes. The SampleWatch count agreed with an independent count read from the C64 to two decimals in every condition.
  • Pitch modulation: captured audio showed no downshift and a 4–20 Hz modulation band at the no-traffic floor. The unsliced 1 KiB writes gave a 4.7 Hz downshift and a band of 11.5.
  • Throughput: 270–300 KiB/s for 1–2 KiB writes under a running NMI, above unlocked 32-byte slices (238–259 KiB/s).
  • Bitmap video: no change in tearing or in short-lived frames dropped, against unsliced.

Things a reviewer should weigh

  • Two lines reachable from isrPHI2. DMAWaitAssertReady() and the end of DMATransferISR() now select their next state on DMA_AutoRun.
    • They run only on cycles that already run DMA code: the assert, and the end of a transfer. Never on an ordinary bus cycle, and never before an IO1/ROM response.
    • There is no thread-mode version: the thread-mode handshake is the cost being removed.
    • docs/Architecture/Constraints.md records this as a deliberate exception, the way the _PreStart one is.
  • The bus snoop slot. SampleWatch borrows fBusSnoop the way BusAnalysis() does.
    • It only takes the slot when it is free, checked and installed with interrupts masked.
    • It refuses with Bus snoop in use otherwise, and clears the slot only if it still holds it.
    • Code that assigns the slot unconditionally (the PRG-load handshake, REU, KERNAL replace) can take it during a watch. Later locked slices then time out and run unlocked, which the counters report.
  • The existing commands are unchanged. DMA_AutoRun is set only for the length of a locked job, by a scope guard. With flags = 0, WriteC64Spans takes the same path as before, and the existing refusal texts are kept.

Possible follow-ups (not in this change)

  • SampleWatch interval times are 32-bit cycle-counter differences. A silence longer than about 5.3 s wraps, and the next report's lost-store figure for that read is wrong.
  • Under a running NMI, any sliced command carrying 4 KiB slows to about 0.7 ms per slice, locked or not (2 KiB costs about 0.12 ms per slice). This is pre-existing and not investigated.
  • BusAnalysis() checks the snoop slot before masking interrupts, the same race this change closes in SampleWatch.

kfox added 4 commits October 22, 2026 19:18
… stores

A sample player on the C64 drives the SID's volume DAC from a CIA2 NMI, and a
DMA halt that spans two timer underflows collapses them into one NMI, so the
player loses a sample.  Slicing bounds each halt but cannot place it: a slice
can still straddle an underflow, and the host cannot see where the NMI is.

The board can.  A new WriteC64Spans flag, LockToStore, names the address the
player stores each sample to (the first span-list entry carries it), and each
slice starts on the cycle after the next store, where the rest of the period is
free.  The gap field becomes the longest wait for a store, after which a slice
starts anyway, so a write never waits on audio that has stopped.  Firmware
without the flag refuses it with "Flags must be 0"; a flagged zero-span header
tells the two apart with nothing owed.

A locked slice is run and released by the PHI2 ISR itself (DMA_AutoRun), since
handing each step back to thread mode costs 70-80 cycles of halt per slice,
nearly a whole NMI period at 12 kHz.

SampleWatch (0x64F9) installs the store watch on the bus snoop hook and reports
the stores counted, the time they spanned and the slices that did and did not
find a store, so a host can count lost NMIs at the source.
ControlComms.md gains the LockToStore flag, its probe, and the
SampleWatch command; Constraints.md lists SampleWatch among the users
that borrow the bus snoop slot.
@kfox
kfox marked this pull request as ready for review October 24, 2026 15:51

@SensoriumEmbedded SensoriumEmbedded left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you for the locked slices and SampleWatch, no lost NMIs at 12 kHz with 1 KB writes going through is a great result. Looking forward to hearing the results. :)

There's one thing I'd like you to look at before merging.

1. The extension image carries the SampleWatch snoop but never calls it (DMAControl.c:267-396)

VMBoot.ino:28 includes DMAControl.c, so its contents are built into the extension image too. Nothing in the extension reaches WriteC64Spans or SampleWatch, since their only caller is SerUSBIO.ino in the main image. They're still in the extension's ELF, because FASTRUN and FLASHMEM functions go into the shared .fastrun and .flashmem sections, and the linker doesn't drop those one function at a time. SampleStoreSnoop costs the extension 128 B of ITCM, plus about 40 B of DTCM for SampleStats and the watch state. The extension's ITCM code has a hard 64 KB limit, and with this PR its headroom drops from 1,112 B to 920 B.

Suggested fix: the four suggestions inline wrap the store watch (DMAControl.c:267-359) and the locked branch of WriteC64Spans (DMAControl.c:377-396) in #ifndef MinimumBuild. Committing them as a batch makes one commit. That saves the extension 128 B of ITCM and about 40 B of DTCM, plus some flash. It's safe because the main image doesn't define MinimumBuild, and the extension, which does, has no caller. DMA_AutoRun lives in Common_Defs.h, so the interrupt-side reads are unaffected.

First time I've tried the suggested fixes, let me know if I did it wrong... :)

Thank you!

@kfox
kfox marked this pull request as draft October 26, 2026 19:48
@kfox

kfox commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, good catch. Your four suggestions are in 7a01c30, unchanged.

Measured with npm run build:tr-plus, on the PR head before and after:

Extension image (VMBoot) Before After
ITCM code 64,552 64,424 (−128)
ITCM headroom (padding) 984 1,112
DTCM variables 50,080 50,048 (−32)
Flash code 70,516 69,876 (−640)

The main TR+ and TR images are byte-for-byte the same size as before. npm run build:tr and npm test pass.

@SensoriumEmbedded SensoriumEmbedded left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

corrected suggestions

@kfox
kfox marked this pull request as ready for review October 26, 2026 19:51
@SensoriumEmbedded
SensoriumEmbedded merged commit 444f01b into SensoriumEmbedded:main Oct 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants