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
4 changes: 3 additions & 1 deletion .claude/skills/framebuffer-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ blank; **~19 ms of compute every VBL** to draw your frame; chunked
drawing on the RP2040 (one byte per pixel) with the chunked → ST planar
conversion done for you in **~1 ms per VBL** (split across both cores);
sampled sound (8-bit 12.5 kHz through the DMA chip on an STE / Mega STE,
~6 kHz 6-bit out the YM2149 elsewhere); and the Atari ST
~6 kHz 6-bit out the YM2149 elsewhere; or, with `APP_PROFILE=PROFILE_25FPS`
at build time, 25 fps with 25 kHz / ~22 kHz sound: `rp/src/include/profile.h`);
and the Atari ST
keyboard, mouse and joysticks handled on the RP2040, delivered to your app
as key events, mouse movement and stick states.

Expand Down
25 changes: 18 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,31 @@
(`AUDIO_FIFO_SLICES`). When nothing is ready the last sample is held, never stale
sound, and `audioUnderruns` counts it.

### Two profiles: 50 fps, or 25 fps with rich sound

- Apps choose at compile time between 50 frames a second with the sound as before (the YM at
5,585 Hz, the DMA chip at 12,517 Hz), the default, and 25 frames a second with four times the
YM's rate and twice the DMA chip's (21,943 Hz and 25,033 Hz): `APP_PROFILE=PROFILE_25FPS` when
building (`rp/src/include/profile.h`). Story games with rich sound get the second; the ST
keeps about 5 ms of each 25 fps frame free on a plain ST, and a mouse moved fast costs no
frame there. The ST code is the same for both; `fb_publish()` paces the app at the profile's
rate, and `.YMS` files and the built-in jingle keep their 5,585 Hz (the RP resamples them).
- The audio buffer in the cartridge window grows to 4 KB, the DMA ring in the ST's RAM to 4 KB
(`$6F000`): `APP_FREE` starts at `$FA5180` (about 12.4 KB).
- The ST's stopwatch (`TIME_STUDY` in `userfw.s`, off by default) replaces the slack report:
MFP Timer-A at 4 µs a tick, reported at points of the ST's loop on one timeline;
`tools/dev/swd.py stopwatch` prints them.

### STE DMA sound

- On an STE or a Mega STE the sound plays through the DMA sound chip: 8-bit at 12,517 Hz, no
interrupt per sample. The ST copies each VBL's samples into a 2 KB ring in its RAM (below the
interrupt per sample. The ST copies each VBL's samples into a ring in its RAM (below the
screen pages); Timer-B stays off, which gives the ST 1.6 ms of every frame back: a mouse moved
fast no longer costs frames there. A plain ST or Mega ST plays through the YM as before, from
the same firmware.
- Apps give 8-bit PCM at any rate (`audio_play_pcm_loop()`, `audio_set_pcm_callback()`) or YM
pairs as before; either plays on both outputs, converted on the RP. `audio_prefer_ym()` keeps
an STE on the YM; `tools/wav_to_ym4.py --mode pcm` writes PCM headers.
- The audio buffer in the cartridge window grows to 2 KB: `APP_FREE` starts at `$FA4980`
(about 14.4 KB).

### Games

Expand All @@ -47,9 +60,7 @@
ST, a Mega ST fitted with a blitter) the 68000 still copies: the blitter holds the CPU off,
and the YM's samples come late and sound rough. `fb_set_copy_mode()` picks either; the
blitter on a Mega ST gives an app without sound about 3 ms more.
- `FB_SLACK_REPORT` now measures with MFP Timer-A, on both sound paths, and reports one byte
(`$FB8Bxx`, in 81 µs steps). The ST reports whether it has a blitter
(`st_session_features()`).
- The ST reports whether it has a blitter (`st_session_features()`).

### Mouse and joysticks

Expand All @@ -68,7 +79,7 @@
counters of the input path.
- Every IKBD byte costs the ST an interrupt. With the full-screen blit and the sound, a mouse
moved fast makes the frame rate dip to 30-40 a second; the sound stays clean.
`FB_SLACK_REPORT` in `userfw.s` measures how much of each frame the ST has left.
The ST's stopwatch (`TIME_STUDY` in `userfw.s`) measures how much of each frame the ST has left.
- The release of keypad Enter is no longer dropped.

### For developers
Expand Down
54 changes: 33 additions & 21 deletions CLAUDE.md

Large diffs are not rendered by default.

49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ at 50 Hz.** No m68k assembly, no bus timing, no double-buffering to manage.
cores), so it barely eats into your frame budget.
- **Sampled sound**: 8-bit at 12.5 kHz through the DMA sound chip on an
STE or a Mega STE, ~6 kHz 6-bit out the YM2149 elsewhere — the same
asset plays on both.
asset plays on both. Or trade frames for sound: the **25 fps profile**
plays 25 kHz and ~22 kHz (see "Two profiles" in §5).
- **Atari ST keyboard, mouse and joysticks handled on the RP2040** —
decoded scancodes, mouse movement and stick states delivered straight to
your app.
Expand Down Expand Up @@ -337,7 +338,8 @@ They show what a game needs beyond the section above:

On an STE or a Mega STE the sound goes through the DMA sound chip (8-bit,
12,517 Hz, no interrupt per sample); on a plain ST or Mega ST it goes
through the YM2149 (~6-bit, 5,585 Hz). The template picks the output at
through the YM2149 (~6-bit, 5,585 Hz). Those are the 50 fps profile's
rates; the 25 fps profile doubles and quadruples them (below). The template picks the output at
every ST boot, and the RP writes the samples ahead of what the ST plays
from a timer interrupt, so the sound never tears and never repeats,
whatever your frame rate. Your audio is YM volume pairs or 8-bit PCM, and
Expand Down Expand Up @@ -374,9 +376,8 @@ audio_set_pcm_callback(my_pcm, AUDIO_DMA_RATE_HZ);

### Generate audio live (callback)

For dynamic sound, install a fill callback. The library calls it for each
VBL of samples it needs, with the exact byte count the m68k will consume
(224 = 112 two-channel samples at ~5,585 Hz):
For dynamic sound in YM pairs, install a fill callback. The library asks it
for pairs at 5,585 Hz, whatever the output's rate (it resamples them):

```c
static void my_fill(uint8_t *buf, uint32_t bytes) {
Expand All @@ -386,12 +387,39 @@ audio_set_fill_callback(my_fill); // pass NULL for silence
```

Either way, **`audio_render_frame()` must be called each loop iteration**:
it tops up a small FIFO (4 VBLs of samples) that the interrupt plays from.
it tops up a small FIFO (4 VBLs of samples, 3 on the YM at 25 fps) that the interrupt plays from.
Your loop may take up to 80 ms between two calls without the sound
noticing; a sample reaches the speaker at most 120 ms after your callback
made it on the YM, about 175 ms on the DMA chip. There's also `audio_play_yms_file(path)` to stream a `.YMS` file
from SD — see §7.

### Two profiles: 50 fps, or 25 fps with rich sound

The ST's time goes to two things: copying your frame to its screen (17.6 ms
for a full screen) and, on a plain ST or Mega ST, playing the sound
sample by sample through Timer-B (about 19 µs a sample). You choose the
trade at compile time with `APP_PROFILE` (`rp/src/include/profile.h`):

| | `PROFILE_50FPS` (default) | `PROFILE_25FPS` |
|---|---|---|
| Frames | every VBL: 50 fps | every second VBL: 25 fps, on every machine |
| YM (plain ST, Mega ST) | 5,585 Hz | 21,943 Hz |
| DMA chip (STE, Mega STE) | 12,517 Hz | 25,033 Hz |
| Good for | arcade games, full-screen effects | story games, laserdisc-style |

```bash
APP_PROFILE=PROFILE_25FPS ./build.sh pico_w release 44444444-4444-4444-8444-444444444444
```

The same ST code serves both: the RP tells the ST the profile at boot.
`fb_publish()` then paces your loop at the profile's frame rate. Measured on
a Mega ST at 25 fps, the full copy and 22 kHz sound leave about 5.5 ms of
each 40 ms frame free, and a mouse moved flat out costs no frame. Your
sources keep their rates: `AUDIO_DMA_RATE_HZ` follows the profile, and YM
pairs and PCM at other rates are resampled. If your game steps its state
once a frame, it moves half as fast at 25 fps: scale by
`PROFILE_VBLS_A_FRAME`, or count time (`time_us_32()`).

---

## 6. Keyboard, mouse and joysticks (`ikbd.h`)
Expand Down Expand Up @@ -451,10 +479,11 @@ moves fast (up to 9 bytes a VBL: the mouse modes set a threshold of 4 counts
per packet) some blits end after the next VBL and that frame waits one: on a
Mega ST, 50 frames a second at rest, 30-40 while the mouse moves flat out.
The sound stays clean. On an STE or a Mega STE the DMA chip plays the sound
and the blitter copies the frame, and the mouse costs no frames.
`FB_SLACK_REPORT` in `userfw.s` measures the slack your app leaves
(`tools/dev/README.md`); blitting fewer lines (`FB_COPY_LINES`, about 88 µs
per line) buys more.
and the blitter copies the frame, and the mouse costs no frames; nor does
it in the 25 fps profile (§5). The ST's stopwatch (`TIME_STUDY` in
`userfw.s`, `tools/dev/swd.py stopwatch`) measures the slack your app
leaves (`tools/dev/README.md`); blitting fewer lines (`FB_COPY_LINES`,
about 88 µs per line) buys more.

---

Expand Down
14 changes: 10 additions & 4 deletions examples/hello_text/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ if (NOT DEFINED ENV{PICO_MALLOC_PANIC})
set(ENV{PICO_MALLOC_PANIC} "0")
endif()
add_compile_definitions(PICO_MALLOC_PANIC=$ENV{PICO_MALLOC_PANIC})

# The app's profile (include/profile.h): PROFILE_50FPS, the default, or
# PROFILE_25FPS, from the environment (APP_PROFILE=PROFILE_25FPS ./build.sh).
if (DEFINED ENV{APP_PROFILE})
add_compile_definitions(APP_PROFILE=$ENV{APP_PROFILE})
endif()
# Creates a pico-sdk subdirectory in our project for the libraries
pico_sdk_init()

Expand Down Expand Up @@ -295,14 +301,14 @@ target_link_options(${PROJECT_NAME} PRIVATE "-Wl,--gc-sections")
# the git commit) are regenerated on every build; see build_id.cmake. They are
# rewritten only when the ID changes, so only their users recompile. The symbol
# is kept through --gc-sections so the SWD tools can read it from flash. A
# debug build's ID ends in +debug, and a build of another CMake type than
# Release carries that type, so builds of one tree cannot be taken for each
# other on the device.
# debug build's ID ends in +debug, a build of another CMake type than
# Release carries that type and a 25 fps build (APP_PROFILE) +25fps, so builds
# of one tree cannot be taken for each other on the device.
set(BUILD_ID_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/build_id)
add_custom_target(build_id
COMMAND ${CMAKE_COMMAND} -DSRC_DIR=${CMAKE_CURRENT_LIST_DIR}
-DOUT_DIR=${BUILD_ID_DIR} -DDEBUG_BUILD=${_DEBUG}
-DCMAKE_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_TYPE=${CMAKE_BUILD_TYPE} -DAPP_PROFILE=$ENV{APP_PROFILE}
-P ${CMAKE_CURRENT_LIST_DIR}/build_id.cmake
BYPRODUCTS ${BUILD_ID_DIR}/build_id.h ${BUILD_ID_DIR}/build_id.c
COMMENT "Checking the build ID")
Expand Down
14 changes: 10 additions & 4 deletions examples/mini_game/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ if (NOT DEFINED ENV{PICO_MALLOC_PANIC})
set(ENV{PICO_MALLOC_PANIC} "0")
endif()
add_compile_definitions(PICO_MALLOC_PANIC=$ENV{PICO_MALLOC_PANIC})

# The app's profile (include/profile.h): PROFILE_50FPS, the default, or
# PROFILE_25FPS, from the environment (APP_PROFILE=PROFILE_25FPS ./build.sh).
if (DEFINED ENV{APP_PROFILE})
add_compile_definitions(APP_PROFILE=$ENV{APP_PROFILE})
endif()
# Creates a pico-sdk subdirectory in our project for the libraries
pico_sdk_init()

Expand Down Expand Up @@ -299,14 +305,14 @@ target_link_options(${PROJECT_NAME} PRIVATE "-Wl,--gc-sections")
# the git commit) are regenerated on every build; see build_id.cmake. They are
# rewritten only when the ID changes, so only their users recompile. The symbol
# is kept through --gc-sections so the SWD tools can read it from flash. A
# debug build's ID ends in +debug, and a build of another CMake type than
# Release carries that type, so builds of one tree cannot be taken for each
# other on the device.
# debug build's ID ends in +debug, a build of another CMake type than
# Release carries that type and a 25 fps build (APP_PROFILE) +25fps, so builds
# of one tree cannot be taken for each other on the device.
set(BUILD_ID_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/build_id)
add_custom_target(build_id
COMMAND ${CMAKE_COMMAND} -DSRC_DIR=${CMAKE_CURRENT_LIST_DIR}
-DOUT_DIR=${BUILD_ID_DIR} -DDEBUG_BUILD=${_DEBUG}
-DCMAKE_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_TYPE=${CMAKE_BUILD_TYPE} -DAPP_PROFILE=$ENV{APP_PROFILE}
-P ${CMAKE_CURRENT_LIST_DIR}/build_id.cmake
BYPRODUCTS ${BUILD_ID_DIR}/build_id.h ${BUILD_ID_DIR}/build_id.c
COMMENT "Checking the build ID")
Expand Down
14 changes: 10 additions & 4 deletions rp/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ if (NOT DEFINED ENV{PICO_MALLOC_PANIC})
endif()
add_compile_definitions(PICO_MALLOC_PANIC=$ENV{PICO_MALLOC_PANIC})

# The app's profile (include/profile.h): PROFILE_50FPS, the default, or
# PROFILE_25FPS, from the environment (APP_PROFILE=PROFILE_25FPS ./build.sh).
if (DEFINED ENV{APP_PROFILE})
add_compile_definitions(APP_PROFILE=$ENV{APP_PROFILE})
endif()

# Creates a pico-sdk subdirectory in our project for the libraries
pico_sdk_init()

Expand Down Expand Up @@ -303,14 +309,14 @@ target_link_options(${PROJECT_NAME} PRIVATE "-Wl,--gc-sections")
# the git commit) are regenerated on every build; see build_id.cmake. They are
# rewritten only when the ID changes, so only their users recompile. The symbol
# is kept through --gc-sections so the SWD tools can read it from flash. A
# debug build's ID ends in +debug, and a build of another CMake type than
# Release carries that type, so builds of one tree cannot be taken for each
# other on the device.
# debug build's ID ends in +debug, a build of another CMake type than
# Release carries that type and a 25 fps build (APP_PROFILE) +25fps, so builds
# of one tree cannot be taken for each other on the device.
set(BUILD_ID_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/build_id)
add_custom_target(build_id
COMMAND ${CMAKE_COMMAND} -DSRC_DIR=${CMAKE_CURRENT_LIST_DIR}
-DOUT_DIR=${BUILD_ID_DIR} -DDEBUG_BUILD=${_DEBUG}
-DCMAKE_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_TYPE=${CMAKE_BUILD_TYPE} -DAPP_PROFILE=$ENV{APP_PROFILE}
-P ${CMAKE_CURRENT_LIST_DIR}/build_id.cmake
BYPRODUCTS ${BUILD_ID_DIR}/build_id.h ${BUILD_ID_DIR}/build_id.c
COMMENT "Checking the build ID")
Expand Down
Loading
Loading