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
25 changes: 20 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ the version and stamps it with the date.
`[hardware].dump_char_rom = false` turns the automatic read off. `--doctor`
reports which ROM is in use and whether it verifies.
- **A second book: the Programmer's Reference Guide** (`docs/reference/`), the
volume you open at the page you need rather than read in order. Six chapters:
the configuration language and its precedence rules, the catalogue of every
scene and overlay, the display pipeline from frame to VIC-II register, the
sound path in both directions, what actually lands in the Commodore's memory,
and every input and output that reaches the show from outside. Its appendices
volume you open at the page you need rather than read in order. Seven
chapters: the configuration language and its precedence rules, the catalogue
of every scene and overlay, the display pipeline from frame to VIC-II
register, the sound path in both directions, the link into the Commodore's
memory and what lands there, every input and output that reaches the show from
outside, and how to extend the program itself. Its appendices
are *generated* from the code by `scripts/gen_reference_appendices.py`: every
configuration section and field, every scene key, every overlay parameter, the
overlay against display-mode matrix, every generator and effect, every
Expand Down Expand Up @@ -155,6 +156,20 @@ the version and stamps it with the date.
does, what is offered back at exit and what is not, and a warning that saving
rewrites the whole configuration file from the settings in memory and keeps
one `.bak` deep.
- **The reference guide's two vaguest chapter titles now say what is in them.**
"Inside the Machine" is *The Link and the Memory Map*, and "Everything
Outside" is *Inputs and Outputs* — which is what a reader scanning the
contents for MIDI, WLED or recording can actually find. The chapter numbers
are unchanged, so every cross-reference still lands where it did.
- **Extending c64cast is its own chapter** (7) rather than the tail of the
memory-map chapter. Writing a scene, an overlay, a generator or an effect is
contributor material, and it was sitting inside a user-facing chapter after a
write budget. It is appended rather than inserted, so chapters 1 to 6 keep
their numbers.
- ASID and the MIDI scene are no longer written out twice. Chapter 2's
catalogue entries state what a *configuration* needs — the keys, the extra,
the ports — and defer the mechanism to Chapter 4, which is the rule the
introduction sets and was the one place the book broke it.

## [0.1.0] - 2026-07-30

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ directory of numbered Markdown plus a `book.toml` that
[scripts/build_book.py](scripts/build_book.py) renders through the one shared
[template](docs/shared/template.typ): [docs/guide/](docs/guide/README.md) (the
User's Guide, read in order), [docs/reference/](docs/reference/README.md) (the
Programmer's Reference Guide — six chapters over nine appendices, of which A–H
Programmer's Reference Guide — seven chapters over nine appendices, of which A–H
are generated by [scripts/gen_reference_appendices.py](scripts/gen_reference_appendices.py)
and regenerated with `make reference-appendices`), and
[docs/card/](docs/card/README.md) (the two-page Performance Card, whose live-target
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/01-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ referred to from the other.

Chapters 1 to 6 are prose: the rules of the configuration language, the
vocabulary of scenes and overlays, the display pipeline from frame to VIC-II
register, the sound path, what actually lands in the Commodore's memory, and
everything that reaches c64cast from outside or leaves it.
register, the sound path, the link into the Commodore's memory and what lands
there, and everything that reaches c64cast from outside or leaves it.

Chapter 7 is the exception, and is for a different reader: it is what you need
to add a scene, an overlay, a generator or an effect to c64cast itself. Nothing
in the first six chapters depends on it.

Appendices A to H are not prose and are not written by hand. They are generated
from the same definitions the program answers `--describe`, `--compat` and
Expand Down
28 changes: 13 additions & 15 deletions docs/reference/03-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,14 @@ Plogue chipsynth C64, an Elektron with ASID-XP. On macOS enable the IAC driver
in Audio MIDI Setup; on Linux `modprobe snd-virmidi`.

This scene has no synthesiser knobs, because ASID carries the whole tune's
register state and c64cast is only relaying it. Multi-SID streams are honoured
on the Ultimate: extra chips are routed to their own addresses, preferring
physical sockets, and the scope subdivides each voice row into one window per
chip. `asid_multi_sid` and `asid_max_sids` gate and cap that; on a backend
without a configuration API the extra chips downmix to the primary SID.
`asid_buffered_player` chooses between a cycle-accurate Commodore-side ring
player and host-side coalescing — the ring player is what keeps multispeed
tunes' arpeggios and hard restarts intact, and `"auto"` selects it whenever
the backend has an REU. OPL-FM (`0x60`) is the one command that is recognised
and dropped.
register state and c64cast is only relaying it. What it has instead is three
keys about how that state is delivered. `asid_multi_sid` and `asid_max_sids`
gate and cap the routing of a multi-SID stream onto extra chips, which needs
a machine whose SID addresses c64cast can configure. `asid_buffered_player`
chooses between the two ways of playing what arrives, and its default `"auto"`
takes the accurate one wherever the machine can carry it. Chapter 4 has what
those two ways are, why one of them needs expansion memory, and what the
protocol does and does not carry.

### `blank`

Expand Down Expand Up @@ -296,11 +294,11 @@ midi_filter_cutoff = 1024
Bitmap-only: `display` is ignored.*

Each voice can hold its own waveform, and an entry may be a `+`-combination
for the chip's combined waveforms. In the default `shared` voice mode one MIDI
channel spreads across all three voices — held notes keep their voice, so a
pad survives while a melody cycles on top. With `multitimbral`, channels route
to fixed voices, each monophonic with last-note priority, and notes on
unmapped channels are ignored.
for the chip's combined waveforms. `midi_voice_mode` picks between the default
`shared`, where one channel spreads across all three voices, and
`multitimbral`, where `midi_voice_channels` pins a channel to each. Chapter 4
has how voices are allocated and stolen under each mode, what pitch-bend and
velocity reach, and the controller map.

> [!NOTE]
> On a 6581 the waveform outputs share a bus and combine by AND, and any
Expand Down
9 changes: 5 additions & 4 deletions docs/reference/05-sound-and-music.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,9 @@ Two scenes drive the real SID from somewhere else entirely. Both need the
### ASID

ASID packs SID register writes into MIDI system-exclusive messages. An ASID
*host* — DeepSID in a browser, SIDFactory II, Plogue chipsynth C64, an Elektron
with ASID-XP — sends the stream; the `asid` scene receives it and plays it on
the chip. It is a new input, not a fidelity change: the protocol carries only
*host* sends the stream and the `asid` scene receives it and plays it on the
chip; Chapter 2's entry for that scene names the hosts and how to open a port
for them. It is a new input, not a fidelity change: the protocol carries only
what a SID can synthesise, never sampled audio.

There are two ways to play what arrives, chosen by `asid_buffered_player`.
Expand Down Expand Up @@ -466,7 +466,8 @@ that starts open means a lowpass patch is neutral until you sweep it.

In the default `shared` voice mode one MIDI channel spreads across all three
voices. With `multitimbral`, `midi_voice_channels` routes channels to fixed
voices, each monophonic, and notes on unmapped channels are ignored.
voices, each monophonic with last-note priority, and notes on unmapped channels
are ignored.

## Listening Back

Expand Down
122 changes: 4 additions & 118 deletions docs/reference/06-under-the-hood.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@
number: 5
---

# Inside the Machine
# The Link and the Memory Map

Everything in the previous two chapters ends with bytes arriving in a
Commodore's memory. This chapter is about that arrival: how the bytes get
there, where they land, how c64cast avoids sending the ones that have not
changed, and what a frame can afford. It closes with what you need to know to
add a scene, an overlay or a generator of your own.
changed, and what a frame can afford.

None of it is required to use c64cast. It is required to reason about why a
scene is slow, why two features refuse to run together, and what your own code
may safely touch.
scene is slow, why two features refuse to run together, and what a picture
costs before you ask for it.

## Getting Bytes In

Expand Down Expand Up @@ -314,116 +313,3 @@ In order of what they buy against what they cost:

What not to give up: coalescing and the dirty cache are automatic, and any new
code path that issues many small writes per frame is the first thing to look at.

## Writing Your Own Scene

A scene produces one frame of *content* per call. The playlist wraps it with
setup, overlays, pacing and teardown, so a scene never needs to know that
overlays exist.

```python
class MyScene(Scene):
def __init__(self, api, audio, display_mode, name="My scene"):
super().__init__(api, audio, display_mode, name)
self.target_fps = 30.0 # only if it can't sustain system rate

def setup(self):
super().setup()
self.display_mode.setup(self.api)

def process_frame(self, current_time: float) -> bool:
frame_bgr = self._produce_frame()
self.display_mode.render(self.api, _crop_to_aspect(frame_bgr))
return True # False means finished

def teardown(self):
super().teardown()
```

Then add a branch to the configuration loader's scene factory, and any fields
the scene takes to the scene dataclass, so they round-trip through TOML.

Four things to honour:

- **`audio` may be `None`.** It is `None` whenever audio is off, the scene sets
`audio = false`, or another system in an ensemble holds the audio slot.
- **Return `False` when finished,** or set `is_done`. The skip path sets it
externally; you may too.
- **Every byte goes through the region-cached write calls.** Opening your own
HTTP session bypasses both the shared connection's mutex and the dirty cache,
and the DMA service accepts one connection.
- **Invalidate the cache** if you change what a cached region means. A display
mode's setup does that for you.

Set `target_fps` only when the scene genuinely cannot sustain the system rate.
The defaults already account for the link, and a scene that pins a low rate for
no reason simply looks worse.

## Writing Your Own Overlay or Generator

Both are small, and both are registered by a decorator rather than by editing a
table.

### An Overlay

Three methods, plus the class attributes that declare where it may run:

```python
@register("blink")
class BlinkOverlay(Overlay):
REQUIRES_PETSCII = False # only touches $D020
REQUIRES_AUDIO = False

def setup(self, api, scene): ...
def process_frame(self, api, scene, t): ...
def teardown(self, api, scene): ...
```

| Attribute | Meaning |
|---|---|
| `REQUIRES_PETSCII` | It writes PETSCII codes to screen and colour RAM, so it needs a character mode |
| `COMPATIBLE_MODES` | An explicit whitelist, for an overlay that is not a clean fit for that split |
| `REQUIRES_AUDIO` | It cannot work at all without the audio streamer; refused at load when audio is off |
| `WANTS_AUDIO` | It uses the streamer when there is one and has a fallback; never refused |

Appendix D's compatibility matrix is built from those, and they are checked when
the configuration loads rather than when the overlay would first draw.

**An overlay that paints characters should not write them itself.** Setting
`PAINTS_INTO_BUFFERS = True` and implementing `compose(buffers, scene, t)` gets
its glyphs folded into the scene's own frame, so scene and overlays go out as
one upload. Writing screen memory from `process_frame` instead races the scene's
own write and flickers. A register write — a border colour, say — is the case
where `process_frame` is the right method.

Two base classes cover most of what people write: one for single-line corner
text, which brings change detection with it, and one for a scrolling ticker.

### A Generator or an Effect

A generator renders 320×200 and returns it; an effect takes a frame and returns
a frame. Both declare their live-tunable parameters as one class attribute:

```python
LIVE_PARAMS = {"speed": (0.1, 4.0), "scale": (0.5, 8.0)}
```

That line is the whole wiring. It puts the parameter in Appendix F, on a MIDI
knob, in the web console's effect rack, and under the WLED sliders, with nothing
else to register. A discrete choice rather than a number goes in `LIVE_CHOICES`,
as a tuple of the values it accepts.

Only declare **independent single-numeric fields** there. A live write is one
attribute assignment, which is atomic; two fields that must change together are
not.

Two behavioural rules matter more than the code. A generator should be
**deterministic in time** — the frame at a given moment the same frame however
you arrived at it — because that is what makes an offline render reproducible;
the two shipped exceptions carry real simulation state and say so. And a
reactive generator must **fall back to its time-driven behaviour** at rest, so a
silent scene is still the generator you asked for.

`docs/extending.md` carries the working examples, the display-mode and
interstitial-background surfaces, and the testing patterns that keep the suite
hardware-free.
2 changes: 1 addition & 1 deletion docs/reference/07-inputs-and-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
number: 6
---

# Everything Outside
# Inputs and Outputs

A running show is not a closed loop. Keys are pressed, cameras and microphones
feed it, controllers and phones drive it, LED fixtures react to it, a recorder
Expand Down
Loading