This document describes the full reproducible workflow for turning a piece
of DSP (eventually: an Airwindows plugin) into a .ZDL file that the Zoom
MS-70CDR will load as a stock-feeling effect.
The pipeline is intentionally split into two halves:
.out); the container layer wraps it.hello/ is the proof that the container layer works end-to-end. Real
ports replace the ELF; everything else stays.
ZoomMultistompZDL/
├── README.md # top-level overview for new contributors
├── build/
│ ├── ABI.md # authoritative runtime ABI write-up
│ ├── linker.py # static linker (.obj → .ZDL)
│ ├── zdl.py # ZDL container parser/writer + label patcher
│ ├── analyze_stock_init_handlers.py # stock init/edit callback scanner
│ ├── find_firmware_state_offsets.py # firmware-side state-field scanner
│ ├── linesel_handlers.bin # onf + knob1 + knob2 + RTS helpers (LineSel)
│ ├── air_knob3_edit.bin # knob3 edit handler (AIR mix_edit)
│ ├── divf_rts.bin # __c6xabi_divf RTS code
│ ├── init_materialize.asm # candidate _init that materializes params at load
│ ├── gen_init_materialize.py # assemble that -> byte templates in linker.py
│ ├── disassemble_zdl.py # any ZDL (ours or stock) -> dis6x listing
│ ├── make_gallery_png.py # dist/*.ZDL covers -> graphics/*.png
│ └── README.md # this file
├── src/
│ └── airwindows/
│ ├── hello/ # minimal pass-through, validates flash/boot
│ ├── gain/ # 1-knob volume trim
│ ├── purestdrive/ # Airwindows PurestDrive (1 knob)
│ ├── tapehack/ # Airwindows TapeHack (3 knobs)
│ └── stereochorus/ # Airwindows StereoChorus with ctx[3] state
├── src/hardware_probes/ # diagnostic ZDLs for runtime ABI experiments
├── docs/CONTRIBUTING.md # open questions + workflow rules
├── stock_zdls/ # ~830 stock ZDLs (templates / references)
├── firmware/ # MS-70CDR firmware blobs (boot/Main/FS/Preset)
├── airwindows-ref/ # Airwindows source (reference; future ports)
├── zoom-fx-modding-ref/ # third-party RE notes, picture en/decoder
├── ZoomPedalFun-main/ # MS-70CDR-specific reverse-engineering
└── Zoom-Firmware-Editor-master/ # Java tool: bundles ZDLs into firmware
offset bytes field
------ ----- -----
0 4 NULL prefix (00 00 00 00)
4 4 "SIZE"
8 4 SIZE payload size (always 8)
12 4 header size after this field (56 typical)
16 4 ELF size in bytes
20 4 "INFO"
24 4 INFO payload size (always 48)
28 32 version string ("ZOOM EFFECT DLL SYSTEM VER 1.00\0")
60 8 type/sort byte fields (real_type, unknown1, knob_type,
unknown2, sort_index, sort_sub, bass_flags, sort_fx_type)
68 8 fx_version (e.g. "1.01\0\0\0\0")
76 ... ELF (TI C6740, EABI, little-endian, shared object)
All values little-endian. Source: zoom-fx-modding-ref/library/CH_4.md.
real_type controls the FX category icon and routing. sort_index ≤240
collides with stock effects; use 240+ for new effects so they show up
as additional menu entries instead of replacing originals.
A factory ZDL’s ELF exports a fixed set of symbols, addressed by name in
.dynsym. The Exciter we have in ofd_zdl.txt is canonical:
| Symbol | Role |
|---|---|
Dll_<Name> |
DLL entry point (the e_entry of the ELF). |
Fx_FLT_<Name> |
Audio-loop function (called per buffer). |
Fx_FLT_<Name>_init |
One-shot init. |
Fx_FLT_<Name>_onf |
On/Off handler (effect bypass logic). |
Fx_FLT_<Name>_<param>_edit |
One per knob — runs when the user turns it. |
picEffectType_<Name> |
RLE-compressed 128×64 pixel art (.const). |
effectTypeImageInfo |
Layout struct: image size + knob positions. |
_infoEffectTypeKnob_A_2 |
Knob bitmap descriptor (.fardata). |
_Fx_FLT_<Name>_Coe |
Effect coefficients table. |
SonicStomp |
Container struct that ties the above together. |
This naming convention is a contract with the firmware’s effect loader.
A new plugin must export a matching set; you can rename <Name> freely.
The pedal’s runtime calls the per-knob _edit handlers with a knob ID
(integer) and reads back a float. The conventional return is the knob’s
0..1.0 normalized value, sometimes pre-scaled. See
zoom-fx-modding-ref/library/CH_2.md § “Reading state, parameters” for a
disassembled walkthrough of LineSel’s three handlers.
Fx_FLT_<Name> is the audio loop. It receives a struct holding pointers
to four interleaved buffers — Effect L/R (the wet path) and Guitar L/R
(the dry path) — plus a sample count. The compiler unrolls the inner
loop 8× by default (8-sample stride per channel).
Critically: output is added, not assigned. Each effect modifies a
shared output buffer; downstream effects still see input. This is what
lets reverb/delay tails survive a switch-off later in the chain. See
CH_2.md § “Adding Effect signal to the final output”.
Compiler-emitted read-only tables may appear as either .const or named
subsections such as .const:table_name. The custom linker packs both forms
into the final read-only .const segment and resolves references to them.
The “UI” is two things:
.const, RLE-compressed. Editable as ASCII
art via zoom-fx-modding-ref/diy/decode_picture.py /
encode_picture.py. Knob shapes are not baked into the picture —
the firmware paints them on top using the _infoEffectTypeKnob_A_2
bitmap. That’s why most effect pictures look like they have round
holes where the knobs are.effectTypeImageInfo struct that lists (knob_id, x, y, knob_ptr)
tuples. Edit the (x, y) to move the knob; bump knob_id to add or
remove knobs.Layout is documented in zoom-fx-modding-ref/library/CH_1.md
§ “Optional: knob positions”.
| Component | Purpose | Notes |
|---|---|---|
| Python ≥3.10 | Container packaging (this repo). | No external deps. |
| TI Code Composer Studio (CCS) | Builds the ELF for C674x. | Use CCS 8.x with the C6000 8.3.x compiler. The pedals use ABI v2, ISA C6740, EABI, 32-bit long. |
TI dis6x |
Disassembles .obj / .out. |
Ships with CCS. Used for analysis and verifying compiled output. |
| Zoom Firmware Editor | Bundles .ZDLs into a flashable firmware blob (.bin). |
https://github.com/Barsik-Barbosik/Zoom-Firmware-Editor. Java; included in Zoom-Firmware-Editor-master/. |
| Optional: LunarIPS | Distribute patches without redistributing Zoom IP. |
Verified against ofd_zdl.txt (Exciter):
-mv6740 ISA tag.Tag_ABI = 2).Tag_Long_Precision_Bits = 2 → 32-bit long..out). The file extension .zdl.code Zoom
uses is just convention.The CCS project type is “Empty Assembly-only project” if you start
from disassembled .asm, or a normal C/C++ project if you write in C.
See zoom-fx-modding-ref/library/CH_3.md for click-by-click setup.
The pedal accepts a single firmware image, not loose ZDLs. Workflow:
python3 build_all.py hello → dist/HELLO.ZDL.Zoom-Firmware-Editor-master/ZoomFirmwareEditor.jar..bin Zoom distributes).
Backup blobs are in firmware/ (boot.bin, Main.bin,
FS.bin, Preset.bin).HELLO.ZDL to the FX list (do not replace LINESEL.ZDL —
sort_index = 250 lets HELLO sit alongside the originals)..bin..bin.HELLO should
appear as a new entry.Safety: keep the unmodified Zoom firmware nearby. If a flash bricks the unit, you re-enter UP+DOWN mode and re-flash the original. The bootloader (
firmware/boot.bin) is separate fromMain.binand is what makes recovery possible — do not touch it unless you know what you’re doing.
See the recipe in the top-level README. In short:
<name>.c, manifest.json, and build.py.python3 build_all.py <name> → ZDL drops into dist/.audio_nop: true to confirm the build is
structurally OK before exercising the DSP.build/zdl.py is the only piece of the container layer; it is small on
purpose. As we hit edge cases (BCAB/CABI sections in cab effects, larger
ELFs, multi-page pictures), they get added there.
INFO.unknown1 / INFO.unknown2 semantics are not pinned down. We
copy them from the LineSel template and they work; varying them is
unexplored.Stock init/edit parameter materialization still has a missing ABI piece.
A suspected packed record type 13 lead turned out to be the loader’s
general DT_FINI dynamic-tag path, and normal stock ZDLs do not use it.
The better anchor is now the firmware handler dispatch path: stock handlers
are called with A4 = 0x11f03000 + slot * 0xD4, a 212-byte per-slot runtime
state block. The template writer at c00c8ac0 initializes the critical
callback fields (state[7], state[21], state[31], +136, +140) for
all six slots. It also seeds per-slot data fields, including state[1] from
c00ee8e8 + 4*slot, so the remaining parameter bug is probably in those
materialization/state sources rather than the DSP body.
Update 2026-09-10. Hardware bisect has narrowed this considerably. Our
_init frame executes fine at load (proved by a probe with zero calls in it);
what freezes is calling an edit handler from _init, because our generated
handlers tail-branch into ctx[7] and that pointer is not populated that
early. Stock handlers compute inline and return, which is why stock _init
can call them. Note this is consistent with the template-writer finding above:
state[7] is one of the fields it initializes, so the open question is when
it runs relative to _init. Full evidence in
../docs/INIT-MATERIALIZATION.md §11.
ctx[34] and ctx[35] do. Every stock _init calls
ctx[34](ctx[1], table, N) and ctx[35](ptr, 0, N) before its handlers;
ctx[34] writes into the param block. Neither is modelled by the emulator, so
they cannot be studied there — only by reading firmware.SonicStomp struct layout is empirical — Exciter’s
ofd_zdl.txt shows it sits in .const at +0x250 but the field
definitions aren’t documented yet. For now we don’t touch it.zoom-fx-modding-ref/howto/RTFM.md.