Skip to content

Fix: Octatrack card builds, too many module voices no longer stall the unit (frame guard) - #1030

Merged
repeat98 merged 3 commits into
mainfrom
claude/card-frame-guard
Oct 28, 2026
Merged

repeat98 merged 3 commits into
mainfrom
claude/card-frame-guard

Conversation

@repeat98

Copy link
Copy Markdown
Owner

Fixes the card-build half of #844 and #1015 (and probably #981).

What was wrong

Card builds have no base, so the frame guard from #874 never reached them. With POLY8, FM Synth or VECTOR voices in a busy project, the frame interrupt ran close to a whole frame on every frame. The DSP then stopped until a power cycle. #1015's log shows exactly that on a card build: POLY8 reaches its 8-voice cap, then its render counter and the sequencer's command counter stop for good while jobs and USB keep logging.

What this changes

Every card build carries the core logger (sdk/runtime/logging), so the guard goes in there and no module changes:

  • frame_guard.c is the USB base's guarded() / mw_frame_exit (same budget: 80 % of a frame, up to half a frame saved, a probe every 1,024 frames within 10 %). A module machine track whose render would overdraw it renders stock's silent source (0x400047f0).
  • Four hooks, all hash-guarded and replayed from the user's own OS. The frame interrupt's entry 0x4000aad0 and exit 0x4000d9a6 are the same as the USB base's FRAME_SITES, same hashes. The record packer's two renderer calls are 0x4000d43a (replays 4 of 6 bytes) and 0x4000d524 (replays 8 of 10 bytes); the new replay field in stock-guards.json keeps stock's argument pushes and drops the jsr.
  • Which tracks: STATIC or FLEX tracks whose Part slot carries PL, FM or S2, the same test the card machine registry (shared-machine.c owner()) and FM Synth (fm_is_native) use. The mask is computed once a frame at the exit, so any other track's render costs 4 instructions in the hook. Analog BD (its voice runs on the DSP) and stock tracks always render.
  • Neighbours: USB Audio's frame site 0x4000d9a0 rejoins at 0x4000d9a6, the guard's exit. FM Synth's packer site 0x4000d514 rejoins at 0x4000d51c, before the second call. Mute Modes' 0x4000d498 sits between the two calls.
  • GRD 0001 (W) in OCTAMOD.LOG reports the silenced renders and which tracks, at most one record per 10 s, from the card build's idle hook (olog_frame_report), never from the interrupt. The site's log parser takes it as is.
  • Logger 0.3.0; core-logger.ts and build_local.py install the four new hooks.
  • The USB base keeps its own guard. It compiles the logger's C and reuses package.hooks(), so build_core.py now skips frame_guard.c and asks for the hooks without the guard (frame_guard=False). The base still hashes every file in the logger folder, so the beta base is regenerated. Its code and data are unchanged; only its identity strings move, 88c68eed → 9b7e0eb3, and the browser .bin still equals build_core.py's. Testers would see one base-update prompt for nothing new. If you'd rather avoid that, merge this with the next real base change (Octatrack base: PERSONALIZE rows as a base table (ot_personalize_rows), so table owners no longer pin package hashes #1023, say) and regenerate once.

How I checked it

  • sh sdk/runtime/logging/tests/run_host.sh: all pass, including the new guard_test.c (which also checks the GRD record's count, track bits and 10 s spacing). Under a simulated frame timer and Part, a heavy POLY8 track is silenced on some renders while the stock track beside it renders every time. A light one is never silenced. FM and S2 tracks are guarded; AB, an unsigned slot, a THRU track with stale bytes and an unloaded bank pointer are not.
  • package.py: main's 0.2.0 sources rebuild byte-identical to the committed package with this toolchain, so the new bytes come only from the change.
  • node scripts/verify-core-logger-native.mjs: 12 native link cases match GNU bytes and symbols.
  • composeStaticOs from the local 1.40C, ten selections. Core only, POLY8, FM Synth, VECTOR, the three together, USB Audio + POLY8, Mute Modes + FM Synth, Analog BD, Analog BD + POLY8 + FM Synth, and [poly8] Audio freezes and sequencer won't play #1015's eight modules all compose, with the four sites patched. Standalone MIDI Scenes composes too.
  • ot_emu --mkii --dsp --frame with [poly8] Audio freezes and sequencer won't play #1015's set and no card. Frames run at 44.1 kHz / 16 through all four hooks, and idle after 4.9 s matches main's image of the same set (21,978 vs 21,979). With PL,1 and FLEX poked into T1's Part slot, the mask reads 1 and the guard's render runs every frame for 2 s with no fault.
  • npm run pr:ready: Ready.

Not tested: any unit. Nobody has yet seen an overload silenced (or a stall prevented) with this guard on hardware. Also untested: an MKI, a POLY8 project in the emulator, and a GRD record on a real card. Before merging, it needs a card image of an overloaded POLY8 project on the MKII; that's a flash, so it needs your go-ahead.

Limits

  • As on USB bases, the guard silences the first module track in the frame, not the most expensive one.
  • Existing card files need to be rebuilt to get the guard.

What the guides lacked

Two lines said card builds have no guard: in docs/OCTATRACK_HARDWARE_TRAPS.md and in the machines guide's "bounded render" item. Both now say the card build's logger has the same guard. The logger's README and FORMAT.md describe the guard and the GRD record.

🤖 Generated with Claude Code

repeat98 and others added 3 commits October 28, 2026 16:47
…e unit (frame guard in the core logger)

Card builds have no base, so the USB base's frame guard (#874) never
reached them: a busy project with POLY8, FM Synth or VECTOR voices kept
the frame interrupt near a whole frame and the DSP stopped until a power
cycle (#844, #1015). The core logger every card build carries now holds
the same guard: it stamps the frame interrupt's entry and exit and wraps
the record packer's two renderer calls. A module machine track (Part
signature PL, FM or S2, read once a frame) whose render would overdraw
80 % of the frame renders stock's silent source instead. Stock tracks
and Analog BD always render; their renders cost four instructions in the
hook. GRD records in OCTAMOD.LOG count the dropouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(identity only)

The USB base compiles the logger's C and reuses its hook generator, and
has its own frame guard (machine_registry.c). build_core.py now skips
frame_guard.c and asks package.hooks() for the hooks without the guard,
and the GRD report moves from the recorder into frame_guard.c, called
from the card build's idle hook. The base still hashes every file of the
logger folder, so it is regenerated: its code and data are unchanged,
only the identity strings move (88c68eed -> 9b7e0eb3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant