ZoomMultistompZDL

Testing ZDLs against an emulated C674x

There is a working C674x emulator that runs our ZDLs headlessly: Ziddle (https://berbasoft.com/ziddle/, CC0, Rust). It loads a .ZDL, runs _init, runs the audio function over blocks, and lets you read the emulated memory afterwards. That turns “does this hang the DSP” from a pedal-bricking experiment into an error return.

Read section 4 before you trust a result. The emulator has told us a build was fine four times, and the pedal froze four times.


1. Getting it

Ziddle is not vendored here — it is a separate project with its own licence and it is large. Download the source from https://berbasoft.com/ziddle/ and unpack it somewhere outside this repo (the layout is a Cargo workspace with product/emu, product/formats, product/cpu, … ).

Our test rig lives in tools/emulator/ — two Rust source files and a Cargo.toml. Copy it into the workspace as product/zdlprobe/:

cp -r tools/emulator <ziddle>/product/zdlprobe
cd <ziddle>
cargo build --release --bin matcheck

Needs a Rust toolchain (rustup); the first build takes about a minute because it pulls in Cranelift.

The rig has twice been lost to a wiped scratchpad. It is in the repo now. Keep it there.

Ziddle itself lives in ~/ziddle/ziddle-source-1787802520 (with product/zdlprobe added to the workspace members) and standard inputs in ~/ziddle/inputs/. Never in /tmp: macOS clears it, and that has cost the whole setup twice. tools/nam_prototype/host_vs_emulator.py (TI vs host) and tools/nam_prototype/make_capture.cjs (loader-path capture builds) are in the repo.

2. matcheck — does _init materialize params?

# Use the full user-knob count from the effect manifest and an explicit
# expected params[0] load value. Example for an eight-knob custom effect:
<ziddle>/target/release/matcheck --params 8 --bypass 1 dist/Gyre.ZDL

Checks all N normalized custom-effect user parameters and params[0] immediately after init, then again after edit handlers. Known input values are 11,22,…; expected user values are 0.11,0.22,… . It never calls set_engaged to repair the bypass flag before observing it. The expected bypass value describes the load state being tested; the CLI does not configure a firmware patch’s on/off state.

Exit 0 requires both snapshots to match and execution checks to pass. Exit 1 means a mismatch, load/read/handler failure, missing/incomplete init, or unsupported init instructions; exit 2 means invalid arguments. Broken shipping initialization is expected to fail. This contract is specific to custom effects; stock effects with nonlinear parameter laws need their own expected values.

The verdict logic can be tested without Ziddle:

rustc --test tools/emulator/src/matcheck_assertions.rs -o /tmp/matcheck-tests
/tmp/matcheck-tests

3. Useful entry points

From ziddle_emu:

Call Use
AudioEngine::load(container, knobs, max_value, budget) load + run _init with knobs seeded
eng.load_report() init_present, init_completed, init_unimplemented_hits
eng.host_mut().harness.mem.read_u32(addr) read emulated memory
host.run_edit_handlers() / host.edit_pass() call every *_edit, and report which failed
process_zoom_block, set_engaged, set_firmware_mix drive audio
Harness::load(bytes) → init_function(), named_symbols_sorted() symbols without running anything

Constants: BLOCK_SAMPLES=8, CHANNELS=2, PARAM_USER_BASE=5, PARAMS_ADDR=0x20000100.

4. What the emulator CANNOT tell you

Correction: the null-callback explanation below is historical and unproven. Stock LineSel also delegates through state[7] during init. See the current investigation for a firmware wait path that is another possible explanation.

This is the important section. Ziddle models the C674x core faithfully. It does not model the pedal’s firmware, and that is where our failures live.

ZDL_PROFILE in product/emu/src/runtime/profile.rs models only these ctx service slots:

7, 8, 21, 31, 39, 40, 45, 46, 51

and everything else is Unmodeled::Zero. Two consequences:

That is exactly how four _init candidates passed here and bricked the pedal. See INIT-MATERIALIZATION.md §11.

Rule of thumb: the emulator is necessary but not sufficient. It is authoritative about instruction semantics — branch targets, register use, stack discipline, whether a loop terminates. It is worthless about firmware readiness and ordering. If a change touches anything that calls through ctx[], an emulator pass means only that you have not made an obvious mistake.

5. Bisecting instead of guessing

The one thing that actually moved the _init work forward was a probe that did nothing: prologue and epilogue with zero calls between them. It booted, which proved the frame was sound and put the fault squarely in the handler calls — after three “candidate fixes” had each cost a freeze and a recovery.

If you are about to flash a candidate fix, consider flashing the question instead. ZDL_MATERIALIZE_INIT_MAX_CALLS exists for this (see ../build/README.md).

Pair it with a probe that fails safe. MatProb defaults its Gain knob to 0, so “params did not materialize” is silence rather than an ambiguous noise — see ../src/hardware_probes/README.md.