Port Potato Chips to the disting NT plug-in API #1

Merged
peterswimm merged 7 commits from claude/port-distong-nt-native-c-2yibw6 into master 2026-08-19 22:32:40 +00:00
peterswimm commented 2026-08-19 15:29:55 +00:00 (Migrated from github.com)

Description

Ports all sixteen Potato Chips modules to the Expert Sleepers disting NT plug-in API as native C++ algorithms, one .o per chip.

The emulators are reused as-is: everything under src/dsp is header-only and already independent of VCV Rack, so the port re-implements only the module layer — the part that reads a panel and drives the chip's registers. Two small pieces bridge the gap to a bare-metal Cortex-M7 target:

  • distingNT/include/nt_potatochips/compat.hpp — supplies inert stand-ins for the three exception types the emulators name on their out-of-range paths (the NT builds with -fno-exceptions) and defines rack::dsp::FREQ_C4, the one Rack constant the DSP layer reads.
  • distingNT/include/nt_potatochips/chip.hpp — stands in for the Rack build's ChipModule: bus resolution, coarse/fine pitch conversion, BLIPBuffer rendering at the 768kHz chip clock, the /16 control-rate divider, and the output normalling that mixes an unpatched output into the next patched one.

The API headers are vendored as the dep/distingNT_API submodule so make -C distingNT works out of the box. distingNT/README.md documents the build, the install, the parameter conventions, the memory each algorithm asks for, and every departure from the Rack build.

Nothing in the port allocates. The built plug-ins reference no allocator and no C++ ABI runtime — only NT_globals and standard library symbols the firmware provides (memcpy, memmove, memset, cos, sin, pow, powf, log, log2f).

Memory errors fixed in shared code

distingNT/test (below) runs every algorithm under ASan and UBSan. Getting all sixteen to pass turned up memory errors in the emulators. Every one of these affects the Rack build too:

  • BLIPBuffer::read_sample() moved one element too many and cleared one element past the end of its buffer — on every sample, of every buffer. In the Rack build that write lands in the next buffer's first field; on the NT it landed in the chip emulator that follows the buffers and zeroed an oscillator's output pointer, crashing Mega Tone on its first block.
  • BLIPSynthesizer::adjust_impulse() ran its phase loop one step wide at both ends, reading before and writing past its impulse table (into the adjacent kernel_unit member) — and, from the same off-by-one, never reached the p == p2 case that halves the error correction for the phase-0.5 impulse.
  • Ricoh2A03::Oscillator::reset() was missing its = 0: it copied the uninitialised fourth register across the other three instead of clearing them, and left the reg_written bools holding whatever was in memory.
  • NintendoGBS sized its register array as ADDR_END - ADDR_START, one short of the inclusive range its own write() accepts, so writing the last wave-RAM byte wrote past the array.
  • DigitalOscillator took the note that selects its band-limited wave-table straight from an unbounded pitch. A zero frequency or a large control voltage indexed past the 16-entry table and segfaulted on the resulting pointer.
  • The S-DSP, the echo and the YM2612 shifted signed values left and overflowed signed products in their filter and modulation paths. The shifts are now multiplies by the same powers of two (exact at these magnitudes) and the products are computed wide before being clamped to 16 bits.

Also, for the port's own needs: the 13KB built-in sample moves off SuperSampler's stack into dsp/sony_s_dsp/hyaw_sample.hpp, shared by both builds, with an accessor that reads zero past the end (the last BRR block runs into it); and dsp/atari_pokey.hpp gains a by-reference constructor so a heapless caller cannot land on its new Engine fallback.

Deliberate departures

  • Super VCA passes its frequency to the filter in Hz. The Rack build converts to a pitch register value first and then hands that to setFrequency(), which converts it again, leaving the filter about eight times too slow and quantised too coarsely to track V/Oct.
  • Super Sampler skips the phase modulation input on its first voice, which has no preceding voice to modulate it; the Rack build reads one voice below its array there.

Two things that look wrong were left alone, because changing them changes what the modules sound like — both written up in the README: Super VCA's loudness compensation rises with the filter mode while the filter weakens (so the mode labelled quietest is unfiltered and boosted 8×), and Super Sampler's built-in sample is packed into BRR nibbles without masking, so a negative sample's sign extension sets the neighbouring nibble.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

  • All sixteen algorithms run and pass under ASan + UBSan, over ten random seeds: make -C distingNT/test. The harness allocates exactly the memory each algorithm declares with guard regions around it, poisons it first so nothing can rely on zeroed memory, and renders 256 steps in three passes — defaults, every input saturated at 5V, and eight rounds of pseudo-random parameters. It checks the parameter tables against their pages, that nothing writes outside its allocation, that every sample is finite and not a runaway, and that an instrument-tagged algorithm makes a sound.
  • All sixteen compile clean for the Cortex-M7 with the API examples' own flags, no warnings: git submodule update --init dep/distingNT_API && make -C distingNT
  • Every object exports pluginEntry, and each one's undefined-symbol set was checked with arm-none-eabi-nm -u against the set the API's own examples produce — no operator new, no malloc, no __cxa_*
  • The edited shared headers and the edited block of SuperSampler.cpp were compiled natively with -Wall -Wextra away from Rack, clean
  • A static_assert in each algorithm ties its parameter table to its parameter enum, so a mismatched page index fails the build

Not tested on hardware. The plug-ins have not been loaded on a disting NT, and no algorithm's sound has been compared against its Rack counterpart — the harness cannot check that.

Test Configuration

  • Operating System: Linux 6.18.5
  • C++ compiler version: arm-none-eabi-g++ 13.2.1 (target), g++ 14 with ASan/UBSan (host harness)

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation following the
    Doxygen style
  • I have tested that my fix is effective or that my feature works
### Description Ports all sixteen Potato Chips modules to the [Expert Sleepers disting NT](https://www.expert-sleepers.co.uk/distingNT.html) plug-in API as native C++ algorithms, one `.o` per chip. The emulators are reused as-is: everything under `src/dsp` is header-only and already independent of VCV Rack, so the port re-implements only the module layer — the part that reads a panel and drives the chip's registers. Two small pieces bridge the gap to a bare-metal Cortex-M7 target: - `distingNT/include/nt_potatochips/compat.hpp` — supplies inert stand-ins for the three exception types the emulators name on their out-of-range paths (the NT builds with `-fno-exceptions`) and defines `rack::dsp::FREQ_C4`, the one Rack constant the DSP layer reads. - `distingNT/include/nt_potatochips/chip.hpp` — stands in for the Rack build's `ChipModule`: bus resolution, coarse/fine pitch conversion, `BLIPBuffer` rendering at the 768kHz chip clock, the /16 control-rate divider, and the output normalling that mixes an unpatched output into the next patched one. The API headers are vendored as the `dep/distingNT_API` submodule so `make -C distingNT` works out of the box. `distingNT/README.md` documents the build, the install, the parameter conventions, the memory each algorithm asks for, and every departure from the Rack build. Nothing in the port allocates. The built plug-ins reference no allocator and no C++ ABI runtime — only `NT_globals` and standard library symbols the firmware provides (`memcpy`, `memmove`, `memset`, `cos`, `sin`, `pow`, `powf`, `log`, `log2f`). ### Memory errors fixed in shared code `distingNT/test` (below) runs every algorithm under ASan and UBSan. Getting all sixteen to pass turned up memory errors in the emulators. **Every one of these affects the Rack build too:** - **`BLIPBuffer::read_sample()`** moved one element too many and cleared one element past the end of its buffer — on every sample, of every buffer. In the Rack build that write lands in the next buffer's first field; on the NT it landed in the chip emulator that follows the buffers and zeroed an oscillator's output pointer, crashing Mega Tone on its first block. - **`BLIPSynthesizer::adjust_impulse()`** ran its phase loop one step wide at both ends, reading before and writing past its impulse table (into the adjacent `kernel_unit` member) — and, from the same off-by-one, never reached the `p == p2` case that halves the error correction for the phase-0.5 impulse. - **`Ricoh2A03::Oscillator::reset()`** was missing its `= 0`: it copied the uninitialised fourth register across the other three instead of clearing them, and left the `reg_written` bools holding whatever was in memory. - **`NintendoGBS`** sized its register array as `ADDR_END - ADDR_START`, one short of the inclusive range its own `write()` accepts, so writing the last wave-RAM byte wrote past the array. - **`DigitalOscillator`** took the note that selects its band-limited wave-table straight from an unbounded pitch. A zero frequency or a large control voltage indexed past the 16-entry table and segfaulted on the resulting pointer. - **The S-DSP, the echo and the YM2612** shifted signed values left and overflowed signed products in their filter and modulation paths. The shifts are now multiplies by the same powers of two (exact at these magnitudes) and the products are computed wide before being clamped to 16 bits. Also, for the port's own needs: the 13KB built-in sample moves off `SuperSampler`'s stack into `dsp/sony_s_dsp/hyaw_sample.hpp`, shared by both builds, with an accessor that reads zero past the end (the last BRR block runs into it); and `dsp/atari_pokey.hpp` gains a by-reference constructor so a heapless caller cannot land on its `new Engine` fallback. ### Deliberate departures - **Super VCA** passes its frequency to the filter in Hz. The Rack build converts to a pitch register value first and then hands that to `setFrequency()`, which converts it again, leaving the filter about eight times too slow and quantised too coarsely to track V/Oct. - **Super Sampler** skips the phase modulation input on its first voice, which has no preceding voice to modulate it; the Rack build reads one voice below its array there. Two things that look wrong were **left alone**, because changing them changes what the modules sound like — both written up in the README: Super VCA's loudness compensation rises with the filter mode while the filter weakens (so the mode labelled quietest is unfiltered and boosted 8×), and Super Sampler's built-in sample is packed into BRR nibbles without masking, so a negative sample's sign extension sets the neighbouring nibble. ### Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) ### How Has This Been Tested? - [x] **All sixteen algorithms run and pass** under ASan + UBSan, over ten random seeds: `make -C distingNT/test`. The harness allocates exactly the memory each algorithm declares with guard regions around it, poisons it first so nothing can rely on zeroed memory, and renders 256 steps in three passes — defaults, every input saturated at 5V, and eight rounds of pseudo-random parameters. It checks the parameter tables against their pages, that nothing writes outside its allocation, that every sample is finite and not a runaway, and that an instrument-tagged algorithm makes a sound. - [x] All sixteen compile clean for the Cortex-M7 with the API examples' own flags, no warnings: `git submodule update --init dep/distingNT_API && make -C distingNT` - [x] Every object exports `pluginEntry`, and each one's undefined-symbol set was checked with `arm-none-eabi-nm -u` against the set the API's own examples produce — no `operator new`, no `malloc`, no `__cxa_*` - [x] The edited shared headers and the edited block of `SuperSampler.cpp` were compiled natively with `-Wall -Wextra` away from Rack, clean - [x] A `static_assert` in each algorithm ties its parameter table to its parameter enum, so a mismatched page index fails the build **Not tested on hardware.** The plug-ins have not been loaded on a disting NT, and no algorithm's sound has been compared against its Rack counterpart — the harness cannot check that. ### Test Configuration - Operating System: Linux 6.18.5 - C++ compiler version: arm-none-eabi-g++ 13.2.1 (target), g++ 14 with ASan/UBSan (host harness) ### Checklist - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation following the [Doxygen](https://www.doxygen.nl/manual/docblocks.html) style - [x] I have tested that my fix is effective or that my feature works
Sign in to join this conversation.
No description provided.