diff --git a/changelog/2026-10-27-engines-play-after-boot.md b/changelog/2026-10-27-engines-play-after-boot.md new file mode 100644 index 000000000..0271be9c2 --- /dev/null +++ b/changelog/2026-10-27-engines-play-after-boot.md @@ -0,0 +1 @@ +Octatrack (USB, beta): a module machine such as Analog BD now plays right after a power cycle. Its sound engine loads while the sequencer runs when it fits in free DSP memory, instead of waiting for you to stop, and it loads several times faster. MODULES LOADED now appears once those engines are running, so pressing PLAY on it sounds every track. If your effects fill the DSP so that one of them has to move first, the engine still waits until the Octatrack has been stopped for two seconds. A halt of the DSPs that can happen in the first minute after a cold boot (DSP STOPPED, then no module engine loads until the next power cycle) is older than this change and not fixed by it. diff --git a/changelog/2026-10-27-flex-machine-switch-live.md b/changelog/2026-10-27-flex-machine-switch-live.md new file mode 100644 index 000000000..a7ea92f8a --- /dev/null +++ b/changelog/2026-10-27-flex-machine-switch-live.md @@ -0,0 +1 @@ +Octatrack base (beta): switching a track from FLEX to Analog BD or FM Synth, or from any module machine back to FLEX, now reloads the track's live parameters from its Part, as stock does when a switch changes the machine type. Before, the switch left them on the old machine's values until SOURCE CLEAR: a machine chosen from FLEX could stay silent, and FLEX chosen back played the machine's settings. STATIC and the other stock machines are unchanged. Checked in the host test and on a MKII for Analog BD (development base: the live parameters before and after each switch, and by ear); FM Synth, POLY8 and VECTOR not yet tried on a unit. diff --git a/changelog/2026-10-27-usb-bridge-elektron-device.md b/changelog/2026-10-27-usb-bridge-elektron-device.md new file mode 100644 index 000000000..bf1de287a --- /dev/null +++ b/changelog/2026-10-27-usb-bridge-elektron-device.md @@ -0,0 +1 @@ +Octatrack USB bridge (`scripts/octatrack-usb-bridge.py`): with another Elektron device on the computer's USB, such as an Analog Rytm on Overbridge, the bridge took the first Elektron device it found and every request to the Octatrack failed. It now opens the device that offers the Modwerk interface. Checked in `sdk/tests/test_usb_bridge.py` and on a MKII with a Rytm MKII attached. The Octatrack driving guide adds the trap. diff --git a/changelog/2026-10-28-boot-src-label.md b/changelog/2026-10-28-boot-src-label.md new file mode 100644 index 000000000..0d198a189 --- /dev/null +++ b/changelog/2026-10-28-boot-src-label.md @@ -0,0 +1 @@ +Octatrack base (beta): after a boot, a track on a module machine (Analog BD, FM Synth and the others) now shows its machine's name at SRC straight away. Before, the screen kept "SRC > FLEX" until the track was tapped, because the saved modules come back a few seconds after the screen is first drawn and the base redrew the track screen but not its header. Checked in the host test and on a MKII: the name shows on every boot. diff --git a/docs/OCTATRACK_BETA_TEST_PLAN.md b/docs/OCTATRACK_BETA_TEST_PLAN.md index 7e74a72a1..7e4e5377b 100644 --- a/docs/OCTATRACK_BETA_TEST_PLAN.md +++ b/docs/OCTATRACK_BETA_TEST_PLAN.md @@ -344,7 +344,7 @@ image RAM-booted for the S cases, then the owner flashes it once. Part 1 end to Pick up here. In order; **O** needs the owner at the unit. -1. **Finish the engines-on-demand pull request** (branch `claude/octatrack-engines-integration`; steps in `docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md`, "Where this stands"): changelog notes for the five rebuilt base-bound packages, docs, every CI check run serially on the WSL test PC, a development twin of the committed beta base `acf91e0a…` (on demand, merged with main after #619) checked on the unit, then the pull request. +1. **Finish the engines-on-demand pull request** (branch `claude/octatrack-engines-integration`; steps in `verification/2026-10-15-resident-engines-integration-build.md`): changelog notes for the five rebuilt base-bound packages, docs, every CI check run serially on the WSL test PC, a development twin of the committed beta base `acf91e0a…` (on demand, merged with main after #619) checked on the unit, then the pull request. 2. **Flash the release base** (O) and check that a kept set survives a power cycle (case 1.18); until now the new bases ran from RAM only. 3. **FUNC held at boot skips the module restore** (O, edge case 15): the testers' escape hatch if a module breaks a unit at startup. Built, never run. Also run absent-module case (c) of finding 24 with it. 4. **Finding 6, a base update versus the testers' saved sets:** every beta3 tester's card holds a set for another base and the site refuses Keep there. This release changes the base again, so every tester hits it: give it a migration path. diff --git a/docs/OCTATRACK_DRIVING.md b/docs/OCTATRACK_DRIVING.md index f0d506338..aa92a9031 100644 --- a/docs/OCTATRACK_DRIVING.md +++ b/docs/OCTATRACK_DRIVING.md @@ -44,6 +44,8 @@ before reporting success. ## Start the link +**One Elektron device on the Mac's USB.** The bridge opens the first device with Elektron's vendor id that offers the Modwerk interface; before 27 October 2026 it opened the first Elektron device of any kind, so an Analog Rytm on Overbridge plugged in beside the Octatrack made every request fail ("unit went away", "no unit running the test base"). Unplug other Elektron devices before a RAM boot or a flash all the same: on that day a RAM boot staged with a Rytm attached failed part-way and hung at the logo, and the retry with the Rytm unplugged came back. + Keep emulator socket paths short, including the host path behind a container bind mount. A long host output path can fail with `usb: cannot listen` even when its container alias is short. ```sh diff --git a/docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md b/docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md index 66224c4f6..79999691c 100644 --- a/docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md +++ b/docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md @@ -16,6 +16,23 @@ Update, 22 October 2026: Analog BD's engine and tables are no longer base data. (docs/OCTATRACK_PACKAGE_DSP_HOOKS.md, "Phase 1 as built"). Every resident build is now on demand: the permanent-engine build (`--resident-stock-hooks` alone) is retired. Below, Analog BD's figures are its engine's, which are unchanged. +Update, 27 October 2026: an engine that fits in free words is placed while the sequencer plays, and MODULES LOADED +waits for the engines the bank uses (record: docs/verification/2026-10-27-engines-play-after-boot.md): + +- **While playing** the claim takes free words only (`dl_engine_claim` commit 1); WRITE, PLACE, RESET and ENABLE then run + as when stopped. The WRITE goes to arena words no slot runs, as a pick's upload does while playing; PLACE sets only the + dormant feature's base word; the track is heard from its ENABLE on. Live FX code is still retired (a trim) or moved (an + evacuation) only while stopped and settled, and an engine is still unplaced only while stopped. A claim told its code + must move (why 7) waits for the stop without planning again each tick: free words only fall while playing. Nothing is + placed while a pattern change is queued: the next pattern's Parts are not the ones the claim checks. +- **A RESET job carries up to eight packets** (`DL_RESET_DATA`, 192 words, one packet a frame: about 3 ms), never past + one owned range, so Analog BD's reset is 20 board ticks instead of 158. Each packet is the one the receiver took + before, at its own offset. A Part switch's upload waits behind one such job at most. +- **MODULES LOADED waits** until every trusted controller the bank's tracks use is ACTIVE or stuck for the boot + (`modwerk_controller_settling`, read from ep0.c's tick): at least half a second, so the board has bound them and seen + them used, at most ten; dropped if the host changes the set meanwhile. SOME MODULES FAILED and MODULES NOT LOADED are + said at once. + ## Goal `--resident-stock-hooks` links the Sidechain Compressor, Output Matrix and @@ -23,7 +40,8 @@ Analog BD engines into the top of each core's program memory for good. The owner's decision is to load an engine's DSP code only while a track uses its feature (22 October 2026; from 15 October until then, while its module was installed). The hard rules are: -- No placement or code move at a Part switch, pattern or bank change, Part reload or project load, or while playing. +- No code move at a Part switch, pattern or bank change, Part reload or project load, or while playing. An engine is + placed while playing only into free words, and never while a pattern change is queued (27 October 2026, above). - No extra flash or card writes. - Negligible runtime overhead. @@ -74,7 +92,7 @@ measured. 2. They are uploaded with an owned `DL_WRITE` at the engine's table offset. The receiver reads every chunk back through P and refuses a chunk whose sum differs. 3. An owned PLACE follows (op 10, word 7 = 1, word 4 the feature, word 5 the table offset). 4. Only once PLACE is acknowledged on every required core does the board send its first RESET chunk, then ENABLE. The receiver also refuses to enable a feature whose base word is 0. - - **When it runs.** From the board's tick only (`controller_board.c`), when a controller generation of the feature is bound, its feature is used ("Use, not install" below) and it is not yet reset. Never at a Part switch, pattern or bank change, Part reload or project load. A placement or an unplacement starts only while the sequencer is stopped and no pattern change is queued (`modwerk_machine_stopped` and the queued bank/pattern bytes, as compaction reads them); otherwise it waits. Nothing new runs in the frame interrupt. + - **When it runs.** From the board's tick only (`controller_board.c`), when a controller generation of the feature is bound, its feature is used ("Use, not install" below) and it is not yet reset. Never at a Part switch, pattern or bank change, Part reload or project load. A placement starts only while no pattern change is queued (the queued bank/pattern bytes, as compaction reads them); while the sequencer plays it takes free words only (27 October 2026, above). An unplacement starts only while the sequencer is stopped (`modwerk_machine_stopped`) and no pattern change is queued; otherwise it waits. Nothing new runs in the frame interrupt. - **Lifetime.** - An engine belongs to its feature, not to one generation. A new generation of the same feature (a replacement, a restore) finds it placed and adopts it without reloading. The code is base data, the same for every generation. - An engine is unplaced (PLACE offset 0) and its span freed only when no slot of that feature wants it (none is held, or its holder is parked, below), the board knows the feature is disabled on that core, and the sequencer is quiet. Quarantine holds it for good. @@ -91,19 +109,19 @@ What uses each engine's feature (`controller_board.c` `use_tick`, once a board t | Feature | Used while | Read from | | --- | --- | --- | -| Analog BD (any packaged feature) | its controller asked for the write lease since the current bank was entered | `controller_write_lease.c` marks every request it resolves to a trusted controller, granted or refused; Analog BD's renderer asks every frame for each track carrying its machine, which only the current Part shows, so the bank keeps the mark until it changes | +| Analog BD (any packaged feature) | a track of the current bank's Parts, working or saved, is signed for its module's machine, or its controller asked for the write lease since the bank was entered | `machine_registry.c` `modwerk_machine_bank_modules` (28 October 2026: the renderer asks only from a track's first trig, so after a boot the engine waited for PLAY); `controller_write_lease.c` marks every request it resolves to a trusted controller, granted or refused. The bank keeps the mark until it changes | | Sidechain | a track runs stock COMPRESSOR (FX id 24), or one of the bank's four Parts, working or saved, names it | `modwerk_dsp_bank_names` (dsp.c) | | Key bus | Sidechain is used, or a reader's effect is named the same way | the reader's handle (`modwerk_dsp_catalogue_handle`) | | Output Matrix | always, while installed | with CUE CFG on MATRIX its routing codes are cue levels to stock, so its engine must already run when MATRIX is chosen; moving it to use needs Matrix to handle MATRIX without its engine first | - **Never placed.** A slot whose feature is not used stays BOUND: nothing written, placed or reset, no room taken. -- **Placed when used**, as at install before: written, PLACEd, reset, enabled, its page applied, under the same guards (stopped, no pattern change queued; a move only when settled). +- **Placed when used**, as at install before: written, PLACEd, reset, enabled, its page applied, under the same guards (no pattern change queued; while playing, free words only; a move or trim only when stopped and settled). - **Parked when no longer used.** Once the unit is stopped and settled (two seconds after the last key or knob, no pick waiting: `mw_engines_settled`) the board reverts its page and disables the feature, the removal's own path, and leaves the slot RETIRING. The engines then collect its words. A page that is not what was written keeps it running instead of quarantining it. Sidechain stays wanted until its DISABLE is complete, so it always goes off before the key bus, as it does at removal. Its regions stay held for buffers while it is installed, so coming back never waits for a buffer. - **All its cores or none.** Before an unplaced engine takes any core's words, the board runs admission's dry run (`mw_engines_admit`) for every core it needs, beside the other wanted engines. With no room on one of them it waits whole and wants no core meanwhile, so the engines give back a partial claim. Room found again waits one tick, so the key bus Sidechain implies is placed first, above it. Until this rule (22 October 2026), Analog BD, used in a bank whose core 1 had no room, was placed on core 0 alone and held 998 words there it could never run (seen on the owner's MKII while chasing beta finding 63, whose cause turned out to be another one, below). - **The self-test charges no new machine voice** (dsp.c, beta finding 63). Its emptied-core attempts would let a machine's voice in (Analog BD's track on T1 took 726 cycles of core 1), after which the Part's own effects on that core no longer fit back and every later set was refused for DSP time. While it runs, a voice not charged when it started waits for its end (the track renders stock meanwhile); one charged before keeps its claim. On the owner's MKII (`a898fd46`, dev twin of `d6bfff70`) bank A's self-test then passed 7/7 with all 19 modules. - **Back when used again.** `mw_controller_restore`, then placed, reset from offset 0 and enabled as at install (Analog BD's package unit is offered again on the way). - **Removal.** A parked module goes at once (RETIRING, its UI clear); one never used is cancelled while BOUND. -- **What this costs.** A track that first uses a feature while the unit plays stays dry (Sidechain: the stock compressor, no KEY page) until the transport stops and the engine is placed, about two seconds later. Within a bank a Part switch never waits: Sidechain and readers read all four Parts, and Analog BD keeps its mark for the bank. A bank change while playing to a bank that first uses Analog BD waits for the stop. +- **What this costs.** A track that first uses a feature while the unit plays stays dry (Sidechain: the stock compressor, no KEY page) until its engine runs: 1.5 to 1.8 s on the unit when it fits in free words (27 October 2026, above; about 1 s of it core 0 waiting behind core 1's jobs), otherwise until the transport stops and live FX code can make room, about two seconds after that. Within a bank a Part switch never waits: Sidechain and readers read all four Parts, and Analog BD keeps its mark for the bank. A bank change while playing to a bank that first uses Analog BD places it while playing when it fits in free words. - **Why.** On the owner's MKII (22 October 2026, development twin of `a408c505`), a tester's 19-module set (#791) installed 17 modules and was refused Analog BD and Sidechain with refusal 2 ("not enough module memory"): the loaded bank played Mini Verb, E-Verb, Tape Echo and BBD, whose code filled core 1, and admission dry-ran the engines' claims against it. With every track on a stock effect the same 19 installed and the self-test passed (19/19 intact, 7 DSP effects together, CPU peak 57 %). - **Checked in software.** `test_resident_engines_board.py` modes 2, 3, 15 and 21 (admitted without room, the engine waiting) and 54-58 (never placed without use; placed on Analog BD's lease; parked in a new bank only when quiet and settled, feature off before unplace; back again; removal of a parked and of a never-used module; Sidechain on and off with stock COMPRESSOR, off before the key bus; a reader's key bus on and off with its effect). `test_dsp.c` covers `modwerk_dsp_bank_names`. - **On the unit** (owner's MKII, 22 October 2026, `df19fdca`, the development twin of `a849d17e`; beta finding 62): the same 19 modules installed in the state that refused two of them before, with Analog BD and Sidechain left BOUND and nothing placed, and Output Matrix placed at install. COMPRESSOR picked on a track brought Sidechain and the key bus in within seconds; FILTER back put both away (unplaced on both cores), twice. A bank without an Analog BD track put Analog BD away; a bank with one brought it back. Not yet: the release image, a power cycle, an Analog BD track first set while playing, audio around a placement. @@ -171,7 +189,7 @@ Method: as above, all 20 USB audio channels as float32, every comparison against ## Checked in software (15 October 2026) -- **Host, ColdFire side** (`sdk/tests/test_resident_engines_board.py`, ASan/UBSan): the actual board, loader, generated manager and generated owned transport against a modelled receiver and made-up engine units. Place, switch on, run, switch off, unplace and free; the order WRITE, PLACE, RESET, ENABLE on each core and disable before unplace; the playing guard (no job while playing, none for an unplace either); a full core refused at admission; live FX code in the region refused until it is gone, and a later pick loading below the engines; the bank's Part union refused when it would no longer fit; a WRITE refused part way (that core's span freed, the other core placed, nothing enabled, then unplaced at removal); an uncertain PLACE (span kept, the feature refused afterwards); twelve Part switches starting no engine job, changing no reserve or capacity and loading no code; a successor adopting the placed engine with no new job; hole reuse; Output Matrix on core A only. The C relocator against `resident_dsp.relocate` at eight bases, edge words included. +- **Host, ColdFire side** (`sdk/tests/test_resident_engines_board.py`, ASan/UBSan): the actual board, loader, generated manager and generated owned transport against a modelled receiver and made-up engine units. Place, switch on, run, switch off, unplace and free; the order WRITE, PLACE, RESET, ENABLE on each core and disable before unplace; the playing guard (no job while playing, none for an unplace either; since 27 October 2026 mode 1 places while playing and waits only for a queued pattern change, and mode 64 brings Analog BD up while playing); a full core refused at admission; live FX code in the region refused until it is gone, and a later pick loading below the engines; the bank's Part union refused when it would no longer fit; a WRITE refused part way (that core's span freed, the other core placed, nothing enabled, then unplaced at removal); an uncertain PLACE (span kept, the feature refused afterwards); twelve Part switches starting no engine job, changing no reserve or capacity and loading no code; a successor adopting the placed engine with no new job; hole reuse; Output Matrix on core A only. The C relocator against `resident_dsp.relocate` at eight bases, edge words included. - **Host, evacuation** (the same test, modes 12-21; its receiver model keeps a dispatch table and checks after every frame that each slot runs its id's whole, correct code from where the manager holds it, never a stub, freed or half-written words): - a 900-word effect at the top of core A's arena: admitted, moved below the region (to 136, clear of its old copy), new copy whole before its one BIND, the engine's first word after it, then placed and ACTIVE; the old copy's freed words overwritten afterwards with no effect on the slot; removal gives the capacity back and the effect stays; - while playing, within 2 s of a key, with a pick waiting, and playing again: nothing moves (core B, which needs no move, places at once); stopped and settled: moved, placed; @@ -232,26 +250,6 @@ Both were found while writing the evacuation and are fixed in the integration br - Host: `test_resident_engines_board.py` modes 26-28: a Part switch during Sidechain's engine WRITE on core A and one during the board's RESET there both complete after the job, with no rollback or failure, and the RESET goes on; two owned jobs in a row holding the core for 800 frames, then the switch completes; a reservation that never ends refuses after 600 refused starts and the switch goes through once it is gone; the same wait in the table step, with an owned job on core B. - **Mutation check** (scratch, not committed): each of 12 mutants is caught: the rollback to the stub (the old code), the BIND back to the new copy, no overlap check, a BIND back counted as stubbed, a BIND back outside a rollback, the journal clearing or not recording the generation, -2 a refusal again in `advance()` or in the table step, an unbounded wait, no owned holder reported, one bound shared by every owned job. -## The integration build on the unit (15 October 2026) - -Twin built from this branch before the base regeneration (on demand, `--dev`, configuration `76e15b41…`), on the owner's MKII: - -- **All three engines:** placed at boot restore and by install, arena 2,224 / 3,125 (Sidechain 423 words). Eight switches between identical Parts while playing: T3 and MAIN bit-exact, no loader packet, no engine job. -- **Sidechain key phase:** T5 (COMPRESSOR, KEY T3, MON ON) lags T3 by 32 samples on 5 of 5 enables (two blocks); the earlier base gave 89 or 121 depending on the enable. On the key-phase twin, 9 of 9 equal. MON: swapping T5's COMPRESSOR for FILTER with MON on gives exact silence. -- **Evacuation:** SPRING REV at the top of core 1 (under Analog BD), sustained as a drone (all six knobs up, HP 0); installing Sidechain moved it and placed the engine. Under the drone (T3 0.34-0.36 RMS): no click (no 20 ms block above the drone's own second-difference texture), no dropout. Counters: moves and evacuations up by one, no failure. -- **Picks during an engine job:** three PLATE REV picks on T5 while Output Matrix installed: all taken, no selection refused or rolled back. Before the fix the same timing gave DSP LOAD FAILED once on the unit. -- **Analog BD on a restored Part (finding 24):** T3 left as ANALOG BD survives a RAM boot with the module in the saved set: FLEX type and marker kept, the drum plays. Before the fix the bank restore was refused: a default bank without trigs, which the unit's autosave then kept ("total silence", first blamed on Sidechain). -- **Development trap:** a RAM boot after a session of resident engine installs and removals sometimes drops the unit off USB until a power cycle (seen twice without a stray session). - -## Where this stands (end of 15 October 2026) - -The beta base is regenerated on demand on top of main after #619 (`acf91e0a…`, which adds the startup without the wave's outline and the two-section MODWERK STATUS page; Analog BD's recipe recaptured again, only its image hash changed). Steps 1 and 2 are done (merged with main, 15 October evening). Next: - -1. Changelogs: a 15 October note on the current version of the five base-bound modules (four rebuilt, CC Map bytes unchanged); the Sidechain key-phase note now ships in the beta base. -2. This note's Status and Open → Beta base, and one line in the beta test plan naming tester base `acf91e0a…` and the finding 17 recapture. -3. The checks `npm run pr:ready` runs, serially and memory-capped (the parallel run OOMs the WSL test PC), including `import-module-build --check-only --verify-existing` for the rebuilt packages. -4. A development twin of exactly this configuration plus `--dev`, then on the unit: boot, the three engines, a Part sweep, an Analog BD reboot. Then the pull request. - ## Open - **Evacuation on the unit.** Written and host-tested (above), not yet run on hardware; the test is above ("Evacuation: the on-unit test"). Still unproven there: that the move is inaudible while an effect rings (no click, dropout or distortion on its track), its frame cost, and that the boot restore of a project whose bank holds big effects now comes up without "SOME MODULES FAILED" (that failure was inferred, never seen). @@ -261,5 +259,5 @@ The beta base is regenerated on demand on top of main after #619 (`acf91e0a…`, - the receiver's op-10 frame cost, and the first-frame behaviour of an engine at a new address; - the key phase change: T5 against T3 (KEY T3, MON ON) must lag by the same number of samples over repeated removals and reinstalls; - Analog BD's X tables stay at `$2840` in the DMA block (an existing trap). -- **Beta base.** `build_core.py` and the resident sources are part of the base's source hash, so any change to them needs the base regenerated (`npm run octatrack:base -- --stock OS.bin --beta --write`: `--beta` is every flag of the beta base, `--cached-modules` and `--usb-audio-variants` among them, and `--write` takes no other) and Analog BD's recipe recaptured when its lease entries moved. The committed beta base is on-demand (since `acf91e0a`). A fresh worktree with a linked `node_modules` first needs `prepare`'s generators (`node scripts/machine-modules.mjs && node scripts/module-changelogs.mjs --write && node scripts/elekloader-vendor.ts --write`). Then relink the five base-bound packages with `npm run octatrack:module-packages` (it rebuilds every package; since #734 the standalone and table recipes match main and the kit's Elekloader checkout) and check that only `requiredBase` moved; on `aba30393` all five came out byte-identical. +- **Beta base.** `build_core.py` and the resident sources are part of the base's source hash, so any change to them needs the base regenerated (`npm run octatrack:base -- --stock OS.bin --beta --write`: `--beta` is every flag of the beta base, `--cached-modules` and `--usb-audio-variants` among them, and `--write` takes no other) and Analog BD's recipe recaptured when its lease entries moved. An edited elekloader file the resident patch also touches (`dsp.c` among them) needs its hash in `module-adapters/resident_base_sources.json` and its `resident_base.patch` context updated too: the host tests build the unpatched file and pass, the base build refuses it. A fresh worktree with a linked `node_modules` first needs `prepare`'s generators (`node scripts/machine-modules.mjs && node scripts/module-changelogs.mjs --write && node scripts/elekloader-vendor.ts --write`). Then relink the five base-bound packages with `npm run octatrack:module-packages -- --base-only --stock OS.bin` (the full run rebuilds every package and stops at a standalone recipe on a computer whose tools differ, after deleting what it published) and check that only `requiredBase` and the controllers' bytes moved. - **Arena numbers.** Done: 4,418 / 4,546 words with nothing placed (above); with all three placed, core A keeps 2,259 and core B 3,160 (measured with Sidechain at 388 words; at 423, 2,224 and 3,125 by arithmetic). diff --git a/docs/module-guides/machines.md b/docs/module-guides/machines.md index 410eaf9d6..a26ea24ea 100644 --- a/docs/module-guides/machines.md +++ b/docs/module-guides/machines.md @@ -27,6 +27,7 @@ A new machine must be a first-class entry wherever the instrument lists machines - [ ] **LFO destinations.** Every continuous machine parameter works as an LFO destination without a crash or a step. **Verify** with `stress_project.py` (three LFOs per track) and record which parameters you modulated. - [ ] **Save and reload.** A project saved with the machine reloads it, with its parameters and model. Say what a build without the module shows for that track. **Verify.** - [ ] **The stock machines still work,** on the same track and on others. +- [ ] **Switching sounds right at once.** A machine that runs on a FLEX track keeps the stored type FLEX, so stock never reloads the track's live parameters on a FLEX ↔ machine switch; the base does (`mw_assign`, through stock's own loader). **Verify** both directions while playing, with no SOURCE CLEAR: the machine sounds, and FLEX plays its sample, not the machine's settings. - [ ] **A bounded render.** A machine that renders on the ColdFire (POLY8, FM Synth, VECTOR) runs in the frame interrupt, and USB, the UI and every task wait while it runs. Time your render with DTCN3 and bound it, as POLY8 0.3.1's voice governor does. On USB bases the base's frame guard gives the frame interrupt 80 % of each frame and renders stock's silent source for a machine track whose render would overdraw that, so an unbounded machine drops out in a busy project instead of freezing the unit; card builds' core logger does the same (`sdk/runtime/logging/frame_guard.c`) and logs the dropouts as `GRD`. **Verify** with `npm run device -- guard` on a development base: in the busiest pattern you support, it reports no skips ([traps](../OCTATRACK_HARDWARE_TRAPS.md#dsp-loader-dynamic-dsp-code)). - [ ] **Muted trigs.** A mute module drops a muted track's trig where stock binds the track's voice (`ev_voice_bind`). A machine that starts its own note from the trig, in its renderer, gets there first: ask the base's raised hook `ev_trig_muted` before the note starts (`raise` in `runtime.json`) and start no note on a track whose bit is set; 0 means play every trig, as stock does. Card builds have no base hooks ([HOOKS.md](../../sdk/machines/octatrack/HOOKS.md), #777). - [ ] **Its own render.** A machine that makes its sound on the ColdFire contributes `render(track, ping, start, end)`. The base calls it twice a frame for each of its tracks, before and after the event's sample offset (the second call has `end` 16). At the packer's cursor (`*(uint32_t *)0x80001c80`) it writes four longs (source count `end - start`, ring write position `start`, rate `0x04000000` (1.0, Q26), read position `start << 26`), then one L and one R long a sample, the 16-bit sample in the high half, and moves the cursor past them; the track's filter, AMP, LFOs and effects follow as for a sample (Analog BD's `control.c`, FM Synth's `poly.s`, SYNTIZ's draft). The played pitch is the parameter record's PTCH halfword (locks, LFOs and scenes applied) on stock's curve. diff --git a/docs/verification/2026-10-15-resident-engines-integration-build.md b/docs/verification/2026-10-15-resident-engines-integration-build.md new file mode 100644 index 000000000..652456cc7 --- /dev/null +++ b/docs/verification/2026-10-15-resident-engines-integration-build.md @@ -0,0 +1,23 @@ +# Resident engines on demand: the integration build on the unit — 15 October 2026 + +Moved from `docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md` on 27 October 2026; history, not current instructions. + +## The integration build on the unit + +Twin built from this branch before the base regeneration (on demand, `--dev`, configuration `76e15b41…`), on the owner's MKII: + +- **All three engines:** placed at boot restore and by install, arena 2,224 / 3,125 (Sidechain 423 words). Eight switches between identical Parts while playing: T3 and MAIN bit-exact, no loader packet, no engine job. +- **Sidechain key phase:** T5 (COMPRESSOR, KEY T3, MON ON) lags T3 by 32 samples on 5 of 5 enables (two blocks); the earlier base gave 89 or 121 depending on the enable. On the key-phase twin, 9 of 9 equal. MON: swapping T5's COMPRESSOR for FILTER with MON on gives exact silence. +- **Evacuation:** SPRING REV at the top of core 1 (under Analog BD), sustained as a drone (all six knobs up, HP 0); installing Sidechain moved it and placed the engine. Under the drone (T3 0.34-0.36 RMS): no click (no 20 ms block above the drone's own second-difference texture), no dropout. Counters: moves and evacuations up by one, no failure. +- **Picks during an engine job:** three PLATE REV picks on T5 while Output Matrix installed: all taken, no selection refused or rolled back. Before the fix the same timing gave DSP LOAD FAILED once on the unit. +- **Analog BD on a restored Part (finding 24):** T3 left as ANALOG BD survives a RAM boot with the module in the saved set: FLEX type and marker kept, the drum plays. Before the fix the bank restore was refused: a default bank without trigs, which the unit's autosave then kept ("total silence", first blamed on Sidechain). +- **Development trap:** a RAM boot after a session of resident engine installs and removals sometimes drops the unit off USB until a power cycle (seen twice without a stray session). + +## Where it stood (end of 15 October 2026) + +The beta base is regenerated on demand on top of main after #619 (`acf91e0a…`, which adds the startup without the wave's outline and the two-section MODWERK STATUS page; Analog BD's recipe recaptured again, only its image hash changed). Steps 1 and 2 are done (merged with main, 15 October evening). Next: + +1. Changelogs: a 15 October note on the current version of the five base-bound modules (four rebuilt, CC Map bytes unchanged); the Sidechain key-phase note now ships in the beta base. +2. This note's Status and Open → Beta base, and one line in the beta test plan naming tester base `acf91e0a…` and the finding 17 recapture. +3. The checks `npm run pr:ready` runs, serially and memory-capped (the parallel run OOMs the WSL test PC), including `import-module-build --check-only --verify-existing` for the rebuilt packages. +4. A development twin of exactly this configuration plus `--dev`, then on the unit: boot, the three engines, a Part sweep, an Analog BD reboot. Then the pull request. diff --git a/docs/verification/2026-10-27-engines-play-after-boot.md b/docs/verification/2026-10-27-engines-play-after-boot.md new file mode 100644 index 000000000..01e1860ff --- /dev/null +++ b/docs/verification/2026-10-27-engines-play-after-boot.md @@ -0,0 +1,83 @@ +# Octatrack base: module machines play right after a boot — 27 October 2026 + +Owner review: this relaxes the stopped-only placement rule in `docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md`. + +**What was wrong.** On a flashed base (`5afa623e`, Analog BD 0.1.6, 14 modules), PLAY pressed on MODULES LOADED left +Analog BD silent until the next stop. Three causes, each in the base: +- An on-demand engine was placed only while the sequencer was stopped and settled. +- A controller RESET went one 24-word packet a 60 Hz board tick: Analog BD's 3,776 words took 158 ticks, about 2.6 s + a core. +- The restore said MODULES LOADED before any engine ran. + +**What changes.** +- **Placed while playing** when the engine fits in free words (`dl_engine_claim` commit 1). Trims, evacuations and + unplacing still wait for a stopped, settled unit; nothing is placed while a pattern change is queued. A play-time claim + told its code must move waits for the stop without planning again each tick (`resident_engines.c` `deferred`). +- **RESET jobs carry up to eight packets** (`DL_RESET_DATA` 192 words, `resident_driver.py`; + `MW_CONTROLLER_RESET_CHUNK`). Each packet is the one the receiver took before, at its own offset. +- **MODULES LOADED waits** for `modwerk_controller_settling()` (`controller_board.c`) to read 0, ticked from ep0.c: + 0.5 s at least, 10 s at most. A slot waits only while the bank uses its feature; a closed core (board transport or + owned channel), a lost core state, a key bus that cannot come up, or a stuck engine ends its wait. The notice is + dropped if the host changes the set during the wait. SOME MODULES FAILED and MODULES NOT LOADED are said at once. +- **Used from the Part.** A packaged feature counts as used once a track of the bank's Parts, working or saved, is + signed for its module's machine (`machine_registry.c` `modwerk_machine_bank_modules`, read by `use_tick`), not only + once its renderer asks for the write lease, which it does only from a track's first trig. +- **`bind_live`** keeps a module's retry when a later module in the same pass binds (`bind_again` was cleared by any + successful bind, so an earlier refused reservation waited for the next owner epoch, and the notice could come early). + +**How it was checked.** +- **Host.** `test_resident_engines_board.py`: mode 1 (placed while playing, nothing while queued), 13 (core B places while + playing, core A waits for the stop to move code), 46 (stall), 54 (settling reads 0 when nothing is used), 64 (Analog BD + placed, reset and enabled while playing within 30 ticks; 158 reset packets a core; settling). `test_resident_transport.c` + (a reset past `DL_RESET_DATA` refused; a 50-word multi-packet job). `test_modset.c` (the notice's minimum, maximum, + once only; dropped on a set change; a partial restore said at once). +- **On the unit** (owner's MKII, development twin `1b913b5c` of this base, RAM-booted, trials only, nothing kept; USB + recordings of T1, T2, T3 and MAIN): + - *Switch while playing.* Bank A T1 FLEX → Analog BD at 18:55:06: placed on both cores within the second (offsets 3294 + and 3420), reset complete and slot ACTIVE by 18:55:07.8 (about 1.8 s). T1 stayed silent until SRC CLEAR: the separate + FLEX-switch bug, not in this build. + - *Bank change while playing.* Bank D → bank B (Analog BD on T1), engine unplaced before: the transport kept playing + (T2 and T3 every 2.000 s across the change). Core 1 placed within 0.3 s, core 0 about 1 s later after waiting behind + core 1's jobs (wait 7, transport busy); reset complete 2.3 s after the change. T1's first Analog BD sound 1.50 s after the change, in + time; the first 12 of 16 steps of that bar were missed. No SRC CLEAR needed. + - *Other tracks.* T2 and T3 bit-exact loop to loop through both placements (largest difference 1.2e-6, the recorder's + floor): no click, gap or drop. + - *Level.* Analog BD on bank B at the same RMS as on bank A (0.033). +- **Code review** of the change: reset offsets, the last partial packet, range ends and ack counts checked against the + receiver; the 10 s cap always ends the wait. Its findings on the settling wait are fixed above. + + - *After a power cycle, before the use-from-Part change* (personal base `d4658748` flashed, 14 modules kept; three + cold boots, PLAY on the notice): Analog BD came in 1.50 s after PLAY (boot 1 recorded: the first 6 of its trigs + missed), about the 13th trig in the others. Its engine started loading only at PLAY: the restored track asks for + the lease only from its first trig, so MODULES LOADED had nothing to wait for. Hence the use-from-Part change. + +- **After a power cycle, with the use-from-Part change** (personal base `1db64a78`, 14 modules kept, blank project; 28 + October): MODULES LOADED, then PLAY: Analog BD sounded on the first step (boot 1 recorded over USB). T1 showed + "SRC → ANALOG BD" without a tap on every boot (the SRC label fix, in this change). +- **Flashed `ccc53692`** (this branch at `9c9ff6bd` plus Analog BD 0.1.6, 14 modules, the owner's own project; 28 + October, Nathaniel): SRC label, first trig after MODULES LOADED and the FLEX ↔ Analog BD switch all pass. On one of + four cold boots the early-boot halt below struck 16 s in, at a bank change while playing (stall record 21, the same + signature); the next boot played Analog BD on its first trig. + +**Open.** +- **Early-boot DSP halt, not fixed here.** Within about 10-40 s of a cold boot, at the project load or a bank change + while playing, the frame chain stopped: 0.5 s later the watchdog showed DSP STOPPED, revived the frames and turned + residency off for the boot (no engine loads or retires until a power cycle). It also happened on a boot with FUNC + held (no modules restored), so it is in the base, not in Analog BD, and `dsp.c` and `status.c` are main's. All five + stall records agree: stock transfer state 7, our transfer idle (`dl_phase` 0, TCD0 and TCD1 DONE, eDMA ES 0), the + frame interrupt unmasked (IMRL bit 1 clear) with nothing pending, EPORT pin 1 high and its edge flag clear, DSP + select 0, one job (a status-meter PEEK in four of five) waiting for its answer. So the chain ended state 7 normally + and the next frame's edge was lost, which fits the 200 µs hold-off trap in `docs/OCTATRACK_HARDWARE_TRAPS.md` + rather than a host-port access of ours. Not reproduced on a RAM-booted development twin; seen again on `ccc53692` (above). A 10 s watchdog + (`37473105`, reverted in `b0d3e493`) made the halt permanent (a drone): keep the 0.5 s recovery. +- **Ringmod SC refused at boot restore** with `RUNTIME_BUSY` on most cold boots of `d4658748` and `1db64a78`, though + it restores on `5afa623e` and installs live. Candidates: the key-bus admission at the restore (`controller_board.c`) + or `rebind_tick`. Parked. +- **First trig after Analog BD → FLEX stutters** (the switch fix); an FM Synth switch and a switch in a full project + are untested. +- **USB drop.** One unprovoked ~2 s USB drop mid-trial (twin, Analog BD placed; possibly the shared hub re-enumerating) + rolled the trial back and left both cores' engine claims at wait 7 and the key bus closed (`0x80`) until a power cycle. + A review points at `journal_fail()` returning before `dl_owned_lease_release` in `mw_manager_journal_release` + (`resident_journal.py`), which is on main: a separate fix. +- **Faster still** (proposals, not here): run both cores' jobs side by side (about 1 s), or place while a bank change is + queued against the queued bank's Parts, free words only, so the first trig sounds. diff --git a/public/octatrack-modules/analog-bassdrum/0.1.6-experimental.mwrm b/public/octatrack-modules/analog-bassdrum/0.1.6-experimental.mwrm index 87d219537..ec872a996 100644 Binary files a/public/octatrack-modules/analog-bassdrum/0.1.6-experimental.mwrm and b/public/octatrack-modules/analog-bassdrum/0.1.6-experimental.mwrm differ diff --git a/public/octatrack-modules/output-matrix/0.1.1-experimental.mwrm b/public/octatrack-modules/output-matrix/0.1.1-experimental.mwrm index 011b89a80..54442b7dc 100644 Binary files a/public/octatrack-modules/output-matrix/0.1.1-experimental.mwrm and b/public/octatrack-modules/output-matrix/0.1.1-experimental.mwrm differ diff --git a/public/octatrack-modules/sidechain-compressor/0.2.0-experimental.mwrm b/public/octatrack-modules/sidechain-compressor/0.2.0-experimental.mwrm index df087c21e..507f94fd6 100644 Binary files a/public/octatrack-modules/sidechain-compressor/0.2.0-experimental.mwrm and b/public/octatrack-modules/sidechain-compressor/0.2.0-experimental.mwrm differ diff --git a/scripts/octatrack-usb-bridge.py b/scripts/octatrack-usb-bridge.py index faba05b65..263a8f5aa 100644 --- a/scripts/octatrack-usb-bridge.py +++ b/scripts/octatrack-usb-bridge.py @@ -67,7 +67,14 @@ def dispose_device(dev): def open_unit(): """(device, vendor interface), claimed; None while no unit runs the test base.""" - dev = usb.core.find(idVendor=0x1935) + # The first Elektron device on the bus may be another instrument (an Analog Rytm on Overbridge): take the one + # that offers the Modwerk vendor interface. + def modwerk(d): + try: + return any((i.bInterfaceClass, i.bInterfaceSubClass, i.bInterfaceProtocol) == (0xff, 0x4d, 1) for c in d for i in c) + except usb.core.USBError: + return False + dev = usb.core.find(idVendor=0x1935, custom_match=modwerk) if dev is None: return None claimed = False diff --git a/sdk/machines/octatrack/base-binding.json b/sdk/machines/octatrack/base-binding.json index e5bbd49a8..d2a7e275c 100644 --- a/sdk/machines/octatrack/base-binding.json +++ b/sdk/machines/octatrack/base-binding.json @@ -1,26 +1,26 @@ { "schema": 1, - "identity": "be678c65f584137216a466a23eb83741fa6ea3f23bf21f2324eab09f56895028", + "identity": "38e47e16c89f70f613f5d79dd1fcbc28ddaced0c188c6056654141d3925185e9", "arena": 1089508832, - "pool": 1087543200, + "pool": 1087545248, "symbols": { - "modwerk_controller_write_begin": 1084871420, - "modwerk_controller_write_end": 1084871448, - "modwerk_cue_cfg_clamp": 1085072680, - "modwerk_raise_ev_mute_keep": 1085075034 + "modwerk_controller_write_begin": 1084872232, + "modwerk_controller_write_end": 1084872260, + "modwerk_cue_cfg_clamp": 1085074216, + "modwerk_raise_ev_mute_keep": 1085076570 }, "controllers": { - "sidechain-compressor": "bc005d3dc5f4a961bc23068b28ab20314f492b12609e087f8d2d4d47caf68d81", - "output-matrix": "8e4c2be24be544ef9abfbb3bef89e017f5a3994c991e5d59ccae7cad0675271b", - "analog-bassdrum": "5ef60dd9313bbe9bdf0b0fc7a013cc47e186225278008ddda0361bde53475b8b" + "sidechain-compressor": "178d200073896dd2416f07bb952f835087f9250778e773fc9426a5165db259ce", + "output-matrix": "17e9afe714d2707147804360bbf4e614240d9d5afc037df5fec0d2a1fc56c3e1", + "analog-bassdrum": "579b9b5e76b000a8b9c95f6131a547fbb5117f497d4bc3ee0f98385edc96e5a3" }, "variants": { - "tracks-main-cue": "be678c65f584137216a466a23eb83741fa6ea3f23bf21f2324eab09f56895028", - "tracks": "94324dac458c7bb6a971213c5ae7e86b3b02033d0752f00b58373a2d2ff8ca4f", - "tracks-post": "f805347c65c097669a6b294d68005b95896502019b2be588b1d4dbeec61e9e17", - "main-cue": "084aac41817ba5d9b402981fd0cf925d7ef027148b1c38bc9d271ea455c1ede3", - "main": "8c0de9fcdc3967881a821f044b45f21bf05826f61c944048ca4bf02d352b36d9", - "master": "278b55963ef71e4350f048e72ce4699bb33f794085fe5ae678c5e5c5eec8c973", - "off": "afc372d7534de9a97d9bada9270c37d77b6f3d538ced3d1c0c71a4c81f192111" + "tracks-main-cue": "38e47e16c89f70f613f5d79dd1fcbc28ddaced0c188c6056654141d3925185e9", + "tracks": "325d5d8b961ecd5c1e3a236dfb062c7cd944fe571fffc549a773b49a7d44feb5", + "tracks-post": "28784e9378141cc92a99537da943246b729f70c88cb79135891d9816a6afc73f", + "main-cue": "e8a43e71c1fe120642e3c0adefbd574ed863ad3100b52e29a105707d93983d87", + "main": "af1c4c77d0a7c2613cf4bf10ba14ccd1cb86ab6799ed6dcfc287b41c8a7768aa", + "master": "762f446ef8ea6b6a57ac0b726a3d5b5dfb43fd41f8244897559eda4feaa2c513", + "off": "0bbff6c54e40f61633ad0152bf6a5cbc264c008c12a207fb34377ee81a5eacc9" } } diff --git a/sdk/machines/octatrack/elekloader/ep0.c b/sdk/machines/octatrack/elekloader/ep0.c index 02f83a1e9..02e796363 100644 --- a/sdk/machines/octatrack/elekloader/ep0.c +++ b/sdk/machines/octatrack/elekloader/ep0.c @@ -503,6 +503,7 @@ void modwerk_ep0_bus_reset(void) * or trial is open is handled as unplugged (mv_tick). */ #define HOST_TIMEOUT_TICKS 600u int modwerk_modset_due(void); /* modset.c: a kept set waits for its save */ +void modwerk_modset_notice_tick(void); /* modset.c: "MODULES LOADED" once the restored engines run */ void modwerk_ep0_tick(void); static void ep0_tick(void); void modwerk_ep0_tick(void) @@ -511,6 +512,7 @@ void modwerk_ep0_tick(void) } static void ep0_tick(void) { + modwerk_modset_notice_tick(); #ifdef MODWERK_RUNTIME_VOLATILE_JOURNAL if(volatile_started && modwerk_volatile_tick(VOLATILE_SERVICE,modwerk_runtime_ticks)) wake(); #endif diff --git a/sdk/machines/octatrack/elekloader/machine_registry.c b/sdk/machines/octatrack/elekloader/machine_registry.c index 564c85dc4..37b0cbf5c 100644 --- a/sdk/machines/octatrack/elekloader/machine_registry.c +++ b/sdk/machines/octatrack/elekloader/machine_registry.c @@ -19,6 +19,7 @@ #define BANK 0x46c82456u #define PART_OFF 0x8ed80u #define PART_STRIDE 6322u +#define SAVED_OFF 0x9504au /* the saved Parts, as dsp.c reads them */ #define SRAM_PART 0x100a4eceu #define SIG 60u #define FLEX_PAGE 0x400d31aeu @@ -141,6 +142,24 @@ static unsigned orphan(const Part *part, unsigned t) if (s[0] == known[k].sig[0] && s[1] == known[k].sig[1] && s[2] >= known[k].lo && s[2] <= known[k].hi) return 1; return 0; } +/* controller_board.c: the modules whose machines a track of the current bank's Parts, working or saved, is signed for, + * up to max. A machine's engine is used from then on, as Sidechain's is once a Part names COMPRESSOR: after a boot its + * renderer first asks for the write lease at the first trig, too late for it (owner's MKII, 28 October 2026). */ +unsigned modwerk_machine_bank_modules(uint32_t *module, unsigned max) +{ + const Machine *m[ROWS - 5u], *who; unsigned n = machines(m), found = 0; + if (!valid_bank()) return 0; + for (unsigned k = 0; k < n && found < max; ++k) { + unsigned hit = 0; + for (unsigned i = 0; i < 8u * 8u && !hit; ++i) { + unsigned p = i >> 3; /* Parts 1-4 working, then saved */ + hit = owner(&m[k], 1, AT(U32(BANK) + (p < 4u ? PART_OFF : SAVED_OFF - 4u * PART_STRIDE) + p * PART_STRIDE), i & 7u, &who) != 0; + } + struct runtime_owner o; + if (hit && modwerk_runtime_owner_at((uintptr_t)m[k], &o) == 1) module[found++] = o.module; + } + return found; +} static unsigned current(const Machine *const *m, unsigned n, const Machine **who) { *who = 0; @@ -290,9 +309,19 @@ static void charge(const Machine *const *m, unsigned n) if (who ? charged[t] != who && !claim(t, who) : charged[t] != 0) release(t); } } +/* Stock loads a track's live parameters from its Part (0x40001f18: bank, Part, track) where a commit changes the + * stored type. A module machine is FLEX underneath, so FLEX <-> machine keeps the type and the live copy would stay on + * the old machine's values (the machine silent, FLEX playing its bytes) until SOURCE CLEAR: load it here instead. */ +static unsigned relive(const Part *part, uint32_t offset, unsigned track, unsigned type) +{ + if (part[0x22 + track] == type) STOCK(void (*)(unsigned, unsigned, unsigned), 0x40001f18u)(U8(0x100b14ceu), offset / PART_STRIDE, track); + return type; +} /* A machine's admission precedes every mutation; -1 refuses. */ +extern volatile uint32_t modwerk_machine_assigned; /* controller_board.c: it reads the bank's machines again after an assignment */ unsigned mw_assign(Part *part, unsigned track, unsigned row) { + modwerk_machine_assigned = modwerk_machine_assigned + 1u; if (!valid_bank() || track >= 8) return row < 5 ? row : 1; uint32_t offset = ADDR(part) - U32(BANK) - PART_OFF; if (offset % PART_STRIDE || offset / PART_STRIDE >= 4) return (unsigned)-1; @@ -318,7 +347,7 @@ unsigned mw_assign(Part *part, unsigned track, unsigned row) } unsigned sig = SIG + 30 * track; part[sig] = mirror[sig] = next->sig[0]; part[sig + 1] = mirror[sig + 1] = next->sig[1]; part[sig + 2] = mirror[sig + 2] = next->sig_lo; - return 1; + return old != next ? relive(part, offset, track, 1) : 1; } /* Browser YES keeps the signed owner; ordinary machine selection clears it. */ if (old && old->assign) { @@ -327,11 +356,13 @@ unsigned mw_assign(Part *part, unsigned track, unsigned row) if (still == old) return pool; hush(part, track, row < 5 ? row : 1); leave(part, mirror, track, old); + return relive(part, offset, track, row < 5 ? row : 1); } else if (old || absent) { hush(part, track, row < 5 ? row : 1); if (old) leave(part, mirror, track, old); else flex_defaults(part, mirror, track); unsigned sig = SIG + 30 * track; for (unsigned k = 0; k < 3; ++k) part[sig + k] = mirror[sig + k] = 0; + return relive(part, offset, track, row < 5 ? row : 1); } return row < 5 ? row : 1; } diff --git a/sdk/machines/octatrack/elekloader/modset.c b/sdk/machines/octatrack/elekloader/modset.c index 671797f85..0bdbf9d5a 100644 --- a/sdk/machines/octatrack/elekloader/modset.c +++ b/sdk/machines/octatrack/elekloader/modset.c @@ -42,7 +42,10 @@ uint32_t modwerk_test_ticks; #define HELD_FUNC (modwerk_test_held) #define GATE() (modwerk_test_gate) #define TICKS (modwerk_test_ticks) -#define SAY(text) ((void)(text)) +const char *modwerk_test_said; /* the last notice */ +int modwerk_test_settling; +#define SAY(text) (modwerk_test_said = (text)) +#define SETTLING() (modwerk_test_settling) #ifdef MODWERK_HOST_CARD extern uint8_t modwerk_test_card_io[512]; /* the fixture's stock file calls under the real store and load below */ #define octamod_log_io modwerk_test_card_io @@ -59,6 +62,8 @@ extern volatile uint32_t modwerk_runtime_ticks; #define GATE() octamod_log_can_flush() #define TICKS modwerk_runtime_ticks #define SAY(text) ((void (*)(const char *, unsigned))0x4005a2b8u)(text, 0xa0) +__attribute__((weak)) int modwerk_controller_settling(void) { return 0; } /* controller_board.c, on a resident base */ +#define SETTLING() modwerk_controller_settling() #endif #if !defined MODWERK_HOST || defined MODWERK_HOST_CARD extern int olog_stock_open(struct file_object *, const char *, const char *, void *, unsigned); @@ -72,6 +77,13 @@ static const uint8_t *candidate; /* the package the host staged, until it is acc static uint32_t candidate_bytes; static unsigned pending, restored, slot; static uint32_t retry_at; +/* "MODULES LOADED" waits until the trusted controllers the bank's tracks use run their engines (Analog BD's, placed, + * reset and enabled), so PLAY on it sounds them (owner's MKII, 27 October 2026: Analog BD stayed silent). Half a second + * at least, for the board to bind them and see them used; ten seconds at most; dropped if the host changes the set. */ +#define NOTICE_FIRST 30u +#define NOTICE_LAST 600u +static const char *volatile notice; +static volatile uint32_t notice_at; volatile uint32_t modwerk_modset_generation, modwerk_modset_count, modwerk_modset_result; /* for the report */ enum { RESTORED = 1, NONE_SAVED, SKIPPED, FOREIGN, DAMAGED, REFUSED, SAVED, SAVE_FAILED, TOO_BIG }; @@ -95,7 +107,7 @@ static void seal(uint32_t count, uint32_t bytes) /* boot.c, at prepare and accept: what the host staged, and that it stays. */ void modwerk_modset_candidate(const uint8_t *package, uint32_t bytes) -{ if (!JOURNAL_BLOCKED()) candidate = package, candidate_bytes = bytes; } +{ notice = 0; /* the set changes: a held notice would describe the old one */ if (!JOURNAL_BLOCKED()) candidate = package, candidate_bytes = bytes; } #ifdef MODWERK_RUNTIME_SETS /* Pure preflight over the old saved set and the complete incoming change set. * No intermediate accepted package reaches set[] or the card. With a null @@ -189,7 +201,7 @@ int modwerk_modset_keep(const uint8_t *const package[], const uint32_t bytes[], { static uint32_t ids[RUNTIME_JOURNAL_ROWS];static uint8_t sha[MU_DIGEST_BYTES * RUNTIME_JOURNAL_ROWS]; uint32_t end = used(), count = 0;uint8_t got[MU_DIGEST_BYTES]; - keep_bytes = 0; + keep_bytes = 0, notice = 0; if (!restored) return 0; for (unsigned i = 0; i < n; ++i) { uint32_t step = 8u + ((bytes[i] + 3u) & ~3u); @@ -307,8 +319,8 @@ static void restore_pass(void) if (installed != count) seal(installed, end); modwerk_modset_generation = be32(set + 8), modwerk_modset_count = installed; modwerk_modset_result = installed == count ? RESTORED : REFUSED; - if (installed) SAY(installed == count ? "MODULES LOADED" : "SOME MODULES FAILED"); - else SAY("MODULES NOT LOADED"); + if (installed && installed == count) notice_at = TICKS, notice = "MODULES LOADED"; + else SAY(installed ? "SOME MODULES FAILED" : "MODULES NOT LOADED"); /* no engine changes what these say */ modwerk_diag_set(); /* diag.c: the set this boot runs, in the unit's log */ } /* After a boot: the newest whole set, each package installed as a USB install would be. */ @@ -339,6 +351,15 @@ static void restore(void) restored = 0, left_out = left_why = 0; restore_pass(); } +/* ep0.c's tick (sys task, as the board's): the restore's notice, once its engines run or its time is up. */ +void modwerk_modset_notice_tick(void) +{ + const char *text = notice; + uint32_t waited = TICKS - notice_at; + if (!text || waited < NOTICE_FIRST || (waited < NOTICE_LAST && SETTLING())) return; + notice = 0; + SAY(text); +} /* ep0.c holds host frames until the restore has run: an install before it would race it, and the restore would undo it. */ int modwerk_modset_restored(void) { return (int)restored; } #ifdef MODWERK_SAMPLE_EDITOR diff --git a/sdk/machines/octatrack/elekloader/runtime.c b/sdk/machines/octatrack/elekloader/runtime.c index 911647b22..e6197b289 100644 --- a/sdk/machines/octatrack/elekloader/runtime.c +++ b/sdk/machines/octatrack/elekloader/runtime.c @@ -241,15 +241,20 @@ static uintptr_t hook(unsigned position, enum runtime_event e) * (controller_board.c): since effects load at the pick (#796), Sidechain's KEY page lands on COMPRESSOR while FX1 * SETUP is already drawn from the stock page (#882). That drawer leaves an open FX SETUP window alone, so once the * shared popup slot (0x460d175c) is that window, open, in SETUP's geometry, its own draw callback runs too, as - * octabam's PLOCKS P2 refreshes it (plk_refresh_setup). */ + * octabam's PLOCKS P2 refreshes it (plk_refresh_setup). + * The header (the bottom-left SRC label and the track's numbers) is stock's 0x4004d780, which the track screen's drawer + * does not call: a track select and SRC SETUP's commit (0x4005a900) do. A restored module machine's track kept stock's + * "SRC > FLEX" (a module machine is FLEX underneath) until the track was tapped (Nathaniel's MKII, 28 October 2026, + * Analog BD on T1 after a cold boot), so the header is drawn again here too. */ volatile uint8_t modwerk_runtime_redraw_wanted; #ifdef MODWERK_HOST -unsigned modwerk_test_redraws; -#define REDRAW() (++modwerk_test_redraws) +unsigned modwerk_test_redraws, modwerk_test_headers; +#define REDRAW() (++modwerk_test_redraws, ++modwerk_test_headers) #else static void REDRAW(void) { ((void (*)(int))0x4004d948u)(-1); + ((void (*)(void))0x4004d780u)(); /* the header: SRC's machine name, as a track select draws it */ uint32_t page = U32(0x460d1684u), setup = page == 3u ? 0x400bc25au : 0x400bc484u, w; /* FX1, FX2 SETUP */ if (U32(0x80000012u) || (page != 3u && page != 4u)) return; /* a MIDI track, or another page */ w = U32(setup + 8u); diff --git a/sdk/machines/octatrack/elekloader/test_machine_registry.c b/sdk/machines/octatrack/elekloader/test_machine_registry.c index 7769a18a8..75faeede3 100644 --- a/sdk/machines/octatrack/elekloader/test_machine_registry.c +++ b/sdk/machines/octatrack/elekloader/test_machine_registry.c @@ -14,7 +14,7 @@ static uint8_t stock_voices[8 * 168]; /* stock's voice records (0x800049d8 + 168 /* Synthetic memory: a few segments at the addresses the registry reads. */ struct segment { uint32_t base, size; uint8_t *bytes; }; -static uint8_t bank[0x8ed80u + 4u * 6322u + 64u], sram[4u * 6322u], misc[0x100], code[0x40], stocktab[0x80], ui[0x40], stockdef[12], +static uint8_t bank[0x9504au + 4u * 6322u + 64u], sram[4u * 6322u], misc[0x100], code[0x40], stocktab[0x80], ui[0x40], stockdef[12], clamps[0x270]; static struct segment segments[] = { {0x46000000u, sizeof bank, bank}, {0x100a4eceu, sizeof sram, sram}, {0x100b14c0u, 0x40, misc}, @@ -44,8 +44,13 @@ static int stock_render(unsigned t, unsigned p, unsigned s, unsigned e) { (void) static void stock_any() { ++stock_calls; } static int silent_renders; static int stock_silent(unsigned t, unsigned p, unsigned s, unsigned e) { (void)t; (void)p; (void)s; (void)e; ++silent_renders; return -9; } +static unsigned relives, relive_args[3]; +static void stock_relive(unsigned bank_index, unsigned part_index, unsigned track) { ++relives, relive_args[0] = bank_index, relive_args[1] = part_index, relive_args[2] = track; } static void *host_stock(uint32_t a) -{ return a == 0x40004008u ? (void *)(uintptr_t)stock_render : a == 0x400047f0u ? (void *)(uintptr_t)stock_silent : (void *)(uintptr_t)stock_any; } +{ + return a == 0x40004008u ? (void *)(uintptr_t)stock_render : a == 0x400047f0u ? (void *)(uintptr_t)stock_silent + : a == 0x40001f18u ? (void *)(uintptr_t)stock_relive : (void *)(uintptr_t)stock_any; +} #define U8(a) (*(volatile uint8_t *)host_at(a)) #define U32(a) host_u32(a) #define AT(a) ((volatile uint8_t *)host_at(a)) @@ -56,7 +61,11 @@ void mg_tick(void); /* midi_generators.c, checked by test_midi_generators.c */ void mg_tick(void) {} /* The loader's side: the table rows and the machine glue. */ #include "loader.h" +volatile uint32_t modwerk_machine_assigned; /* controller_board.c's, not linked here */ #include "modwerk_events.h" +/* The loader's owner of an address: here the module id is the machine's first letter. */ +int modwerk_runtime_owner_at(uintptr_t address, struct runtime_owner *o) +{ const struct modwerk_machine *m = (const void *)address; memset(o, 0, sizeof *o); o->module = m->sig[0]; return 1; } static uintptr_t volatile machine_row[10], sequencer_row[9]; static int masked; uint32_t modwerk_machine_mask(void) { CHECK(!masked); masked = 1; return 7; } @@ -205,7 +214,15 @@ int main(void) CHECK(!strcmp(mw_name(5), "ANALOG BD") && !strcmp(mw_name(6), "VECTOR") && !strcmp(mw_name(7), "FM SYNTH") && (uintptr_t)mw_name(8) == 0x1001u); /* Assign: admission first, defaults and signature in the Part and its SRAM mirror; a refusal changes nothing. */ a_admit = 0; CHECK(mw_assign(part0(), 2, 5) == (unsigned)-1 && part0()[60 + 60] == 0); + uint32_t mods[4]; + CHECK(modwerk_machine_bank_modules(mods, 4) == 0); a_admit = 1; CHECK(mw_assign(part0(), 2, 5) == 1); + /* The bank's Parts sign a track for A: its module's engine is used (controller_board.c), a saved Part's too. */ + CHECK(modwerk_machine_bank_modules(mods, 4) == 1 && mods[0] == 'A'); + memcpy(host_at(0x46000000u + 0x9504au + 2u * 6322u + SIG + 30u * 5u), "FM\1", 3); + bank[0x9504au + 2u * 6322u + 0x22 + 5] = 1; + CHECK(modwerk_machine_bank_modules(mods, 4) == 2 && mods[0] == 'A' && mods[1] == 'F'); + memset(host_at(0x46000000u + 0x9504au + 2u * 6322u + SIG + 30u * 5u), 0, 3); CHECK(part0()[60 + 60] == 'A' && part0()[61 + 60] == 'B' && part0()[62 + 60] == 1 && sram[60 + 60] == 'A'); CHECK(part0()[0x2a + 60 + 6] == 1 && part0()[0x1da + 60 + 11] == 12 && sram[0x1da + 60 + 11] == 12); CHECK(mw_type(1, slot(2)) == 5 && mw_type(3, slot(2)) == 3 && mw_type(1, slot(3)) == 1); @@ -357,6 +374,20 @@ int main(void) CHECK(mw_assign(part0(), 3, 1) == 1 && part0()[60 + 90] == 0); /* The type byte is stock's to change (the commit stores the returned row): the registry never writes it. */ CHECK(part0()[0x22 + 3] == 1); + /* FLEX <-> a machine keeps the stored type, so the registry loads the track's live parameters from its Part + * (stock's 0x40001f18, which a type change runs); a commit that changes the type, from or to STATIC, is stock's. */ + const void *a_only[] = {&A}; + rows(a_only, 1, 0, 0); + misc[0x0e] = 3; relives = 0; /* bank 4 */ + CHECK(mw_assign(part0(), 3, 5) == 1 && relives == 1 && relive_args[0] == 3 && relive_args[1] == 0 && relive_args[2] == 3); + CHECK(mw_assign(part0(), 3, 5) == 1 && relives == 1); /* the same machine again */ + CHECK(mw_assign(part0(), 3, 1) == 1 && relives == 2 && relive_args[2] == 3); /* back to FLEX */ + CHECK(mw_assign(part0(), 3, 1) == 1 && relives == 2); /* FLEX to FLEX */ + part0()[0x22 + 3] = 0; /* STATIC */ + CHECK(mw_assign(part0(), 3, 5) == 1 && relives == 2); + part0()[0x22 + 3] = 1; /* the commit stores FLEX */ + CHECK(mw_assign(part0(), 3, 0) == 0 && relives == 2); /* and STATIC again */ + misc[0x0e] = 0; rows(0, 0, 0, 0); /* A switch that keeps the stored type stops the track's stock voice (byte 0 of its record) before its parameters diff --git a/sdk/machines/octatrack/elekloader/test_modset.c b/sdk/machines/octatrack/elekloader/test_modset.c index 18a1b8068..02136f9b3 100644 --- a/sdk/machines/octatrack/elekloader/test_modset.c +++ b/sdk/machines/octatrack/elekloader/test_modset.c @@ -193,6 +193,29 @@ int main(void) modwerk_test_held = 1; boot(); modwerk_modset_service(); modwerk_test_held = 0; accept(0, 48); modwerk_modset_service(); CHECK(modwerk_modset_result == SAVED && modwerk_modset_generation == newest + 2); boot(); modwerk_modset_service(); CHECK(modwerk_modset_result == RESTORED && installed == 1); + /* "MODULES LOADED" waits for the engines the bank uses: half a second at least, ten seconds at most, once. */ + boot(); modwerk_test_said = 0; modwerk_modset_service(); CHECK(modwerk_modset_result == RESTORED && !modwerk_test_said); + modwerk_test_settling = 1; + for (unsigned t = 0; t < 600u; ++t, ++modwerk_test_ticks) modwerk_modset_notice_tick(); + CHECK(!modwerk_test_said); + modwerk_modset_notice_tick(); CHECK(modwerk_test_said && !strcmp(modwerk_test_said, "MODULES LOADED")); + modwerk_test_said = 0; modwerk_modset_notice_tick(); CHECK(!modwerk_test_said); + boot(); modwerk_modset_service(); modwerk_test_settling = 0; + for (unsigned t = 0; t < 30u; ++t, ++modwerk_test_ticks) modwerk_modset_notice_tick(); + CHECK(!modwerk_test_said); + modwerk_modset_notice_tick(); CHECK(modwerk_test_said && !strcmp(modwerk_test_said, "MODULES LOADED")); + /* The host changing the set drops a held notice; a partial restore says so at once (no engine changes that). */ + modwerk_test_said = 0; boot(); modwerk_modset_service(); modwerk_test_settling = 1; + modwerk_modset_candidate(0, 0); + for (unsigned t = 0; t < 700u; ++t, ++modwerk_test_ticks) modwerk_modset_notice_tick(); + CHECK(!modwerk_test_said); modwerk_test_settling = 0; + accept(1, 48); modwerk_modset_service(); CHECK(modwerk_modset_result == SAVED && modwerk_modset_count == 2); + refuse_id = be32(package[1] + 28); refusal_now = RUNTIME_FULL; /* not BUSY: left out at once, no retries */ + boot(); modwerk_modset_service(); refuse_id = 0; refusal_now = RUNTIME_BUSY; + CHECK(modwerk_modset_result == REFUSED && installed == 1 && modwerk_test_said && !strcmp(modwerk_test_said, "SOME MODULES FAILED")); + modwerk_test_said = 0; + for (unsigned t = 0; t < 700u; ++t, ++modwerk_test_ticks) modwerk_modset_notice_tick(); + CHECK(!modwerk_test_said); if (failures) { fprintf(stderr, "%u module set checks failed\n", failures); return 1; } puts("Octatrack module set: saved in turn, removals dropped, the newest whole set restored, damaged and foreign sets and a held FUNC refused, " "a Keep joined only as the trial's identity and reported unsaved until saved."); diff --git a/sdk/machines/octatrack/elekloader/test_runtime.c b/sdk/machines/octatrack/elekloader/test_runtime.c index 14984ed3e..bcf53eb2e 100644 --- a/sdk/machines/octatrack/elekloader/test_runtime.c +++ b/sdk/machines/octatrack/elekloader/test_runtime.c @@ -66,6 +66,7 @@ int main(void) owner_epoch += 2; modwerk_runtime_tick(); CHECK(modwerk_test_redraws == 4); /* The controller board wrote or put back a stock effect's page (#882): one redraw on the next tick, set unchanged. */ modwerk_runtime_redraw_wanted = 1; modwerk_runtime_tick(); CHECK(modwerk_test_redraws == 5 && !modwerk_runtime_redraw_wanted); + CHECK(modwerk_test_headers == modwerk_test_redraws); /* every redraw draws the header too (SRC's machine name) */ modwerk_runtime_tick(); CHECK(modwerk_test_redraws == 5); /* Activation only while nothing plays or records; stock code only, below the bootloader copy. */ modwerk_test_stopped = 0; diff --git a/sdk/machines/octatrack/module-adapters/controller-policies.json b/sdk/machines/octatrack/module-adapters/controller-policies.json index d3126f485..a93d84c25 100644 --- a/sdk/machines/octatrack/module-adapters/controller-policies.json +++ b/sdk/machines/octatrack/module-adapters/controller-policies.json @@ -2,7 +2,7 @@ { "module": "sidechain-compressor", "moduleId": 2513434262, - "controllerSha256": "bc005d3dc5f4a961bc23068b28ab20314f492b12609e087f8d2d4d47caf68d81", + "controllerSha256": "178d200073896dd2416f07bb952f835087f9250778e773fc9426a5165db259ce", "sourcePolicySha256": "96638989bbfb3b2f695d60eee84e6e3f9a6ada4099a0c9c889cbecf477f06b58", "revisions": [ 1, @@ -21,7 +21,7 @@ { "module": "output-matrix", "moduleId": 3404814517, - "controllerSha256": "8e4c2be24be544ef9abfbb3bef89e017f5a3994c991e5d59ccae7cad0675271b", + "controllerSha256": "17e9afe714d2707147804360bbf4e614240d9d5afc037df5fec0d2a1fc56c3e1", "sourcePolicySha256": "0994f291fc10c4b89c827cf60802e30ba152bf49c6572027e9edc0a22524a232", "revisions": [ 1, @@ -40,7 +40,7 @@ { "module": "output-matrix", "moduleId": 3404814517, - "controllerSha256": "8e4c2be24be544ef9abfbb3bef89e017f5a3994c991e5d59ccae7cad0675271b", + "controllerSha256": "17e9afe714d2707147804360bbf4e614240d9d5afc037df5fec0d2a1fc56c3e1", "sourcePolicySha256": "0994f291fc10c4b89c827cf60802e30ba152bf49c6572027e9edc0a22524a232", "revisions": [ 1, @@ -59,7 +59,7 @@ { "module": "output-matrix", "moduleId": 3404814517, - "controllerSha256": "8e4c2be24be544ef9abfbb3bef89e017f5a3994c991e5d59ccae7cad0675271b", + "controllerSha256": "17e9afe714d2707147804360bbf4e614240d9d5afc037df5fec0d2a1fc56c3e1", "sourcePolicySha256": "0994f291fc10c4b89c827cf60802e30ba152bf49c6572027e9edc0a22524a232", "revisions": [ 1, @@ -78,7 +78,7 @@ { "module": "analog-bassdrum", "moduleId": 2883261509, - "controllerSha256": "5ef60dd9313bbe9bdf0b0fc7a013cc47e186225278008ddda0361bde53475b8b", + "controllerSha256": "579b9b5e76b000a8b9c95f6131a547fbb5117f497d4bc3ee0f98385edc96e5a3", "sourcePolicySha256": "ccb3bb4638004690936ac24e6f96a2c408f7f1130e7c3e9cccaf6cf18f44fd46", "revisions": [ 1, diff --git a/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.c b/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.c index 9f877e256..3a13d4f42 100644 --- a/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.c +++ b/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.c @@ -88,7 +88,7 @@ int mw_controller_start(struct mw_controller_registry *r,const struct mw_control unsigned next=s->state,feature=b->identity.feature,c=j->core; if(j->operation==MW_CTL_RESET) { if((s->state!=MW_CTL_BOUND && s->state!=MW_CTL_INITIALIZING) || j->offset!=s->acknowledged_words[c] || - !j->count || j->count>24u || j->offset>s->reset_words[c] || j->count>s->reset_words[c]-j->offset || + !j->count || j->count>MW_CONTROLLER_RESET_CHUNK || j->offset>s->reset_words[c] || j->count>s->reset_words[c]-j->offset || j->expected || j->desired || !r->backend->resources_idle || r->backend->resources_idle(r->context,b)!=1) return MW_CTL_CLOSED; struct mw_controller_slot *old=previous(r,s); if(old && old->state!=MW_CTL_RETIRING && old->state!=MW_CTL_RELEASED) return MW_CTL_CLOSED; diff --git a/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.h b/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.h index 7464cfddf..c69c8476d 100644 --- a/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.h +++ b/sdk/machines/octatrack/module-adapters/controller-policy/controller_association.h @@ -5,6 +5,8 @@ #include #define MW_CONTROLLER_SLOTS 6u +/* The most words one RESET job writes: the owned transport's DL_RESET_DATA, eight packets. */ +#define MW_CONTROLLER_RESET_CHUNK 192u enum mw_controller_state { MW_CTL_EMPTY, MW_CTL_RESERVED, MW_CTL_BOUND, MW_CTL_INITIALIZING, MW_CTL_READY, MW_CTL_PUBLISHING, MW_CTL_ACTIVE, MW_CTL_DISABLING, MW_CTL_RETIRING, MW_CTL_QUARANTINED, MW_CTL_RELEASED }; diff --git a/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.c b/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.c index 5ee0a3679..025fc870f 100644 --- a/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.c +++ b/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.c @@ -19,6 +19,7 @@ int modwerk_dsp_keybus_charge(int on); /* dsp.c (resident_engines.dsp): th uint32_t modwerk_dsp_keybus_price(unsigned c); /* ... on core c while it is not charged there, 0 once it is */ int modwerk_dsp_bank_names(unsigned id); /* dsp.c: a track runs this FX id, or one of the bank's Parts names it */ extern volatile uint8_t modwerk_controller_lease_seen; /* controller_write_lease.c: features a trusted controller asked to write */ +unsigned modwerk_machine_bank_modules(uint32_t *module, unsigned max); /* machine_registry.c: machines the bank's Parts sign */ #endif unsigned modwerk_controller_policy_entries(void); @@ -39,11 +40,13 @@ uint8_t *mw_controller_board_test_donor(uint32_t); uint32_t mw_controller_board_test_bank(void); int mw_controller_board_test_handle(uint32_t module); #define BANK_NOW() mw_controller_board_test_bank() +#define PART_NOW() 0u #define HANDLE(module) mw_controller_board_test_handle(module) #else #define READ8(a) (*(volatile const uint8_t *)(uintptr_t)(a)) #define DONOR(a) ((uint8_t *)(uintptr_t)(a)) #define BANK_NOW() (*(volatile const uint32_t *)0x46c82456u) /* the current bank (selection.c), 0 before a project */ +#define PART_NOW() READ8(0x80000003u) /* the current Part */ int modwerk_dsp_catalogue_handle(uint32_t module); /* dsp.c: the effect id a module holds this session, <0 none */ #define HANDLE(module) modwerk_dsp_catalogue_handle(module) static int board_read(void *context, uint32_t address, void *out, uint32_t length) @@ -65,9 +68,9 @@ static struct { uint32_t generation, since; uint8_t quiesce; } want[MW_CONTROLLE static uint8_t started, closed, mask[2], mask_known; /* Engines load when a track uses them, not when their module is installed (owner, 22 October 2026). Sidechain is used * while a track runs stock COMPRESSOR or one of the bank's Parts names it. Analog BD (any packaged feature) is used once - * its controller asked for the write lease in this bank: its renderer does, every frame, for each track that carries its - * machine, which only the current Part shows, so the bank keeps it until it changes and a Part switch never waits for - * a placement. Output Matrix counts as used while installed: with CUE CFG on MATRIX its routing codes are cue levels to + * a track of the bank's Parts is signed for its machine, or its controller asked for the write lease in this bank (its + * renderer does, every frame, for each track that carries its machine, but only from the first trig), so the bank keeps + * it until it changes and a Part switch never waits for a placement. Output Matrix counts as used while installed: with CUE CFG on MATRIX its routing codes are cue levels to * stock, so its engine must already run when MATRIX is chosen. A held slot whose feature is unused is parked: never * placed, or, once placed, disabled (its page reverted) and left RETIRING, so the engines collect its words; used again, * it is restored and placed, reset and enabled as at install. */ @@ -97,7 +100,7 @@ static uint8_t kb_readers; /* ... and whether a live package requires it (a #ifdef MODWERK_RESIDENT_ON_DEMAND static uint8_t kb_priced; /* the tap's price is charged on both cores (dsp.c): only for readers */ #endif -static const uint32_t kb_zeros[24]; +static const uint32_t kb_zeros[MW_CONTROLLER_RESET_CHUNK]; /* the key bus's, Sidechain's and Matrix's reset words */ /* The key bus cannot come up this boot: the board closed, or a core's state lost (its mask unknown, a lane stuck or its * owned channel closed: an uncertain job closed it for the boot, the transport's fail-closed rule). Nothing reopens it, * which would act on a DSP whose state nobody knows, so a reader is refused (RUNTIME_FEATURE) rather than admitted to @@ -252,7 +255,8 @@ static int engine_shape(const struct runtime_dsp *d, unsigned feature) } return 1; } -/* Its reset on core c from `offset`: the initial words of the claims there, in order, never past one claim. */ +/* Its reset on core c from `offset`: the initial words of the claims there, in order, never past one claim, at most one + * job's worth (MW_CONTROLLER_RESET_CHUNK). */ static int engine_init(const struct runtime_dsp *d, unsigned c, uint32_t offset, const uint32_t **words, unsigned *count) { uint32_t at = 0, logical = 0; @@ -262,7 +266,7 @@ static int engine_init(const struct runtime_dsp *d, unsigned c, uint32_t offset, if (applies(q, c)) { if (offset < logical + q->words) { uint32_t k = logical + q->words - offset; - *words = d->initial + at + (offset - logical), *count = k > 24u ? 24u : k; + *words = d->initial + at + (offset - logical), *count = k > MW_CONTROLLER_RESET_CHUNK ? MW_CONTROLLER_RESET_CHUNK : k; return 1; } logical += q->words; @@ -412,7 +416,6 @@ static int roots_clear(void *u, const struct mw_controller_binding *b) * job's release. */ static int reset_source(void *u, const struct mw_controller_binding *b, unsigned c, uint32_t offset, const uint32_t **words, unsigned *count) { - static const uint32_t zeros[24]; (void)u; unsigned f = b->identity.feature, n = c < 2u ? total(f, c) : 0u; if (!n || offset >= n || !words || !count) return 0; @@ -420,9 +423,9 @@ static int reset_source(void *u, const struct mw_controller_binding *b, unsigned const struct runtime_module *m = module_of(b); return m && engine_init(&m->dsp, c, offset, words, count) && *count <= n - offset; } - unsigned k = n - offset > 24u ? 24u : n - offset; + unsigned k = n - offset > MW_CONTROLLER_RESET_CHUNK ? MW_CONTROLLER_RESET_CHUNK : n - offset; if (f == 1u && offset < 528u && k > 528u - offset) k = 528u - offset; /* one owned range per chunk */ - *words = zeros; *count = k; return 1; + *words = kb_zeros; *count = k; return 1; } /* Known from the dormant boot upload and every ACKed owned CAS since; any * uncertain job on a core makes it unknown for the rest of the boot. */ @@ -502,12 +505,36 @@ static unsigned slot_features(void) return features; } #ifdef MODWERK_RESIDENT_ON_DEMAND +volatile uint32_t modwerk_machine_assigned; /* machine_registry.c's mw_assign counts every machine assignment here */ /* Once a tick: what is used (above). A new bank starts from nothing. */ static void use_tick(void) { uint32_t bank = BANK_NOW(); + static unsigned seen_part, scan_tick; static uint32_t seen_assigned; + unsigned part = PART_NOW(); + int changed = bank != seen_bank || part != seen_part || modwerk_machine_assigned != seen_assigned; if (bank != seen_bank) seen_bank = bank, seen = 0; + seen_part = part, seen_assigned = modwerk_machine_assigned; seen |= modwerk_controller_lease_seen & MW_DSP_PACKAGED; modwerk_controller_lease_seen = 0; + /* A packaged feature's machine signed on a track of the bank's Parts: used before its first trig asks for the lease, + * so after a boot it runs by MODULES LOADED. Only while one held is not seen yet: a few loads a tick. */ + unsigned unseen = 0; + for (unsigned k = 0; k < MW_CONTROLLER_SLOTS; ++k) + if (held(®ISTRY.slots[k])) unseen |= REGISTRY.slots[k].binding.identity.feature & MW_DSP_PACKAGED & ~seen; + static unsigned last_unseen; + if (unseen & ~last_unseen) changed = 1; /* a feature newly held (the boot's restore): looked for at once */ + last_unseen = unseen; + /* The bank's Parts (8 x 8 tracks, uncached) are read again only when the bank, the Part or a track's machine changed, + * and every 64th tick for an edit that is none of those (a Part copy or reload): a packaged machine installed but + * unused by the bank stays unseen all session, and the scan ran every tick inside the lock (owner, 29 October 2026). */ + if (unseen && (changed || (++scan_tick & 63u) == 0u)) { + uint32_t module[4]; unsigned n = modwerk_machine_bank_modules(module, 4u); + for (unsigned k = 0; k < MW_CONTROLLER_SLOTS; ++k) { + const struct mw_controller_slot *s = ®ISTRY.slots[k]; + for (unsigned i = 0; i < n && held(s); ++i) + if (s->binding.identity.module == module[i]) seen |= s->binding.identity.feature & unseen; + } + } used = (uint8_t)(seen | 2u | (modwerk_dsp_bank_names(COMPRESSOR) ? 1u : 0u)); } /* Slot `at` needs its engine: held and used, or on its way out of use (its DISABLE not yet complete, so Sidechain goes @@ -608,7 +635,9 @@ static int keybus_start(unsigned c, unsigned op) struct dl_owned_job job; int ok; if (op == KB_RESET || op == KB_SCRUB) { - unsigned offset = op == KB_SCRUB ? 0u : kb_reset[c], n = KEYBUS_WORDS - offset > 24u ? 24u : KEYBUS_WORDS - offset; + /* A scrub is one offset-0 packet (the seed and counter); a reset as much as one job carries. */ + unsigned offset = op == KB_SCRUB ? 0u : kb_reset[c], most = op == KB_SCRUB ? DL_DATA : MW_CONTROLLER_RESET_CHUNK; + unsigned n = KEYBUS_WORDS - offset > most ? most : KEYBUS_WORDS - offset; if (offset < KEYBUS_SPLIT && n > KEYBUS_SPLIT - offset) n = KEYBUS_SPLIT - offset; ok = dl_owned_reset_reserve(c, KEYBUS_GENERATION, KEYBUS, offset, n, kb_zeros, &job); } else ok = dl_owned_command_reserve(c, KEYBUS_GENERATION, 10u, mask[c], @@ -688,28 +717,28 @@ static void bind_live(void) { uint32_t epoch = modwerk_runtime_owner_epoch(); if (epoch == bound_epoch && !bind_again) return; - bound_epoch = epoch; bind_again = 0; + unsigned again = 0; /* set once, after the pass: a later module's bind must not clear an earlier one's wait */ + bound_epoch = epoch; for (unsigned i = 0; i < RUNTIME_MODULES; ++i) { const struct runtime_module *m = modwerk_runtime_module(i); struct runtime_owner o; struct mw_controller_binding b; const struct mw_controller_write_policy *p; if (!m) continue; - if (modwerk_runtime_owner_snapshot(m->dsp.image, &o) != 1) { bind_again = 1; continue; } + if (modwerk_runtime_owner_snapshot(m->dsp.image, &o) != 1) { again = 1; continue; } if (!(p = row_for(o.controller_sha256, o.module, o.image_bytes)) || !binding_of(&o, p, &b) || slot_of(&b) < MW_CONTROLLER_SLOTS) continue; const struct mw_controller_slot *previous = 0; unsigned others = 0; for (unsigned k = 0; k < MW_CONTROLLER_SLOTS; ++k) { const struct mw_controller_slot *s = ®ISTRY.slots[k]; if (held(s) && s->binding.identity.feature == p->identity.feature) { ++others; previous = s; } } - bind_again = 1; /* until it is bound, or gone */ - if (others > 1u || (previous && previous->state != MW_CTL_RETIRING)) continue; /* wait for its disable */ + if (others > 1u || (previous && previous->state != MW_CTL_RETIRING)) { again = 1; continue; } /* wait for its disable */ struct mw_controller_declaration d = {p->identity, {total(p->identity.feature, 0), total(p->identity.feature, 1)}}; unsigned at; - if (mw_controller_reserve(®ISTRY, &d, &d.identity, previous ? &previous->binding : 0, &at) != MW_CTL_OK) continue; - if (mw_controller_bind(®ISTRY, at, &b) != MW_CTL_OK) { (void)mw_controller_cancel(®ISTRY, ®ISTRY.slots[at].binding); continue; } + if (mw_controller_reserve(®ISTRY, &d, &d.identity, previous ? &previous->binding : 0, &at) != MW_CTL_OK) { again = 1; continue; } + if (mw_controller_bind(®ISTRY, at, &b) != MW_CTL_OK) { (void)mw_controller_cancel(®ISTRY, ®ISTRY.slots[at].binding); again = 1; continue; } want[at].generation = b.generation; want[at].since = ticks; want[at].quiesce = 0; verified[at] = 0; - bind_again = 0; } + bind_again = again; } static void advance(unsigned at) { @@ -837,6 +866,35 @@ void modwerk_controller_tick(void) modwerk_machine_unlock(irq); } +#ifdef MODWERK_RESIDENT_ON_DEMAND +/* A base without the machine registry (the host fixtures): no machine signs a track. */ +__attribute__((weak)) unsigned modwerk_machine_bank_modules(uint32_t *module, unsigned max) { (void)module; (void)max; return 0; } +#endif +int modwerk_controller_settling(void) +{ + if (!started || closed) return 0; + if (bind_again) return 1; /* a live controller not bound yet, or waiting for its predecessor's disable */ + for (unsigned at = 0; at < MW_CONTROLLER_SLOTS; ++at) { + const struct mw_controller_slot *s = ®ISTRY.slots[at]; + unsigned need = s->binding.identity.required_cores, stuck = 0; + if (!held(s) || s->state == MW_CTL_ACTIVE || s->state >= MW_CTL_DISABLING) continue; + /* Stuck for this boot: its core closed (the board's transport or the owned channel) or its state lost, or the + * key bus it rides on cannot come up. */ + for (unsigned c = 0; c < 2u; ++c) + if (need >> c & 1u) stuck |= transport.closed[c] || dl_owned_closed(c) || !(mask_known >> c & 1u); +#ifdef MODWERK_RESIDENT_ON_DEMAND + /* Only what this bank uses: a slot past BOUND that the bank no longer uses parks only when stopped and settled. */ + if (!(used & s->binding.identity.feature) && !s->touched) continue; + if (roomless[at] && s->state <= MW_CTL_BOUND) continue; /* no room for its engine */ + stuck |= KEYBUS_IMPLIED(s->binding.identity.feature) && keybus_down(); + for (unsigned c = 0, k = MW_DSP_INDEX(s->binding.identity.feature); c < 2u && k < MW_ENGINE_FEATURES; ++c) + if (need >> c & 1u) stuck |= mw_engines_wait[c][k] == MW_WAIT_STUCK; +#endif + if (!stuck) return 1; + } + return 0; +} + int modwerk_controller_admit(const struct runtime_dsp *from, const struct runtime_dsp *to, enum runtime_metadata_operation operation) { diff --git a/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.h b/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.h index 69bea65d5..bfee0596a 100644 --- a/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.h +++ b/sdk/machines/octatrack/module-adapters/controller-policy/controller_board.h @@ -39,4 +39,8 @@ unsigned modwerk_controller_held(void); /* A live reader whose key bus cannot come up this boot: dsp.c fails a self-test with it and, when the loader has no * refusal of its own, reports RUNTIME_FEATURE. */ int modwerk_controller_keybus_lost(void); +/* ev_tick: 1 while a trusted controller the bank's tracks use still brings its engine up (not yet bound, placed, reset + * or enabled), so the boot restore's "MODULES LOADED" waits for it (modset.c). 0 once each is running, or can't be this + * boot: no room for its engine, its engine or a core stuck, the slot quarantined, the board closed. */ +int modwerk_controller_settling(void); #endif diff --git a/sdk/machines/octatrack/module-adapters/controller-policy/controller_transport.c b/sdk/machines/octatrack/module-adapters/controller-policy/controller_transport.c index 093894944..bd87b13df 100644 --- a/sdk/machines/octatrack/module-adapters/controller-policy/controller_transport.c +++ b/sdk/machines/octatrack/module-adapters/controller-policy/controller_transport.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ #include "controller_transport.h" #include "loader.h" +typedef char mw_controller_reset_chunk_is_the_transports[MW_CONTROLLER_RESET_CHUNK == DL_RESET_DATA ? 1 : -1]; static int locked(void) { #ifdef MW_CONTROLLER_TRANSPORT_TEST @@ -90,7 +91,7 @@ int mw_controller_transport_begin(struct mw_controller_transport *t,const struct if(op==MW_CTL_RESET) { uint32_t offset=s->acknowledged_words[c];const uint32_t *words=0;unsigned count=0; if(offset>=s->reset_words[c] || t->policy->reset_source(t->context,b,c,offset,&words,&count)!=1 || - !count || count>DL_DATA || count>s->reset_words[c]-offset || + !count || count>MW_CONTROLLER_RESET_CHUNK || count>s->reset_words[c]-offset || !dl_owned_reset_reserve(c,b->generation,b->identity.feature,offset,count,words,&receipt)) goto out; } else if(op==MW_CTL_ENABLE || op==MW_CTL_DISABLE) { uint32_t mask=0; diff --git a/sdk/machines/octatrack/module-adapters/machine-recipes.json b/sdk/machines/octatrack/module-adapters/machine-recipes.json index 8c9731037..67334b747 100644 --- a/sdk/machines/octatrack/module-adapters/machine-recipes.json +++ b/sdk/machines/octatrack/module-adapters/machine-recipes.json @@ -470,7 +470,7 @@ }, "analog-bassdrum": { "schema": 1, - "imageSha256": "9045e67a5c49da4e188a97e20473dde1eb348ada7c50bc10f4005734384097aa", + "imageSha256": "a684c02dd4420dd95f0e732ca2198eabc7f5818001cbbabb8904f7ce80f97c1d", "sources": { "sdk/machines/octatrack/module-adapters/analog_bd_runtime.py": "b9c0e0f96632a0d0ab5283387edaae1b604e4e82e27e26b3fdc1666867d31daf", "sdk/machines/octatrack/module-adapters/analog_bd_write_lease.py": "3e6a3766f32176e9be69627ef2b71272970f2d02a2088e77673946b640d365c7", diff --git a/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.c b/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.c index a9988f61c..01a678493 100644 --- a/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.c +++ b/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.c @@ -8,14 +8,18 @@ #ifdef MW_ENGINES_TEST int mw_engines_test_quiet(void); +int mw_engines_test_queued(void); int mw_engines_test_settled(void); #define QUIET() mw_engines_test_quiet() +#define QUEUED() mw_engines_test_queued() #define SETTLED() mw_engines_test_settled() #else int modwerk_machine_stopped(void); -/* Stopped (no track or recorder running), and no pattern change queued (dsp.c's compaction reads the same bytes). */ -#define QUIET() (modwerk_machine_stopped() && *(volatile uint8_t *)0x800065bfu == *(volatile uint8_t *)0x800065bdu && \ - *(volatile uint8_t *)0x800065c0u == *(volatile uint8_t *)0x800065beu) +/* A pattern change is queued (dsp.c's compaction reads the same bytes). */ +#define QUEUED() (*(volatile uint8_t *)0x800065bfu != *(volatile uint8_t *)0x800065bdu || \ + *(volatile uint8_t *)0x800065c0u != *(volatile uint8_t *)0x800065beu) +/* Stopped (no track or recorder running), and no pattern change queued. */ +#define QUIET() (modwerk_machine_stopped() && !QUEUED()) extern volatile uint32_t modwerk_runtime_ticks, modwerk_status_key_at; /* runtime.c, status.c: UI ticks (60 Hz), the last key */ int modwerk_dsp_moves_idle(void); /* dsp.c: no self-test, rebind or waiting pick */ /* Live FX code may move for an engine (an evacuation, beside QUIET): two seconds after the last key or knob and no pick @@ -47,6 +51,7 @@ static uint8_t closed[2]; * new reader is refused at admission rather than waiting on the transport for good. */ static int shut(unsigned c) { return closed[c] || dl_owned_closed(c); } static unsigned wanted_now; /* mw_engines_want */ +static unsigned deferred[2] = {~0u, ~0u}; /* a play-time claim had to move or retire code: what it needed, until quiet */ void mw_engines_want(unsigned features) { wanted_now = features; } int mw_engines_settled(void) { return QUIET() && SETTLED(); } @@ -244,9 +249,15 @@ int mw_engines_place(unsigned feature, unsigned cores, uint32_t generation, cons if (j != k && (wanted_now >> j & 1u) && s.at[j] < 0 && unit_of(c, j)->count) (void)add_span(c, &s, j, unit_of(c, j)->count); unsigned need = reserve_of(c, &s); /* A claim the reserve already covers (an evacuation lowered the capacity for it) is bookkeeping only, taken - * at once so the reserve never waits unowned; a new claim, and any code move, only while quiet. */ - if (need > dl_engine_reserved(c) && !QUIET()) { *why = MW_WAIT_PLAYING; continue; } - int claim = dl_engine_claim(c, need, SETTLED() ? 2 : 1); + * at once so the reserve never waits unowned. A new claim waits while a pattern change is queued (the next + * pattern's Parts are not the ones it checks). While playing it takes free words only (commit 1): live FX + * code is retired (a trim) or moved (an evacuation) only while quiet and settled. */ + if (need > dl_engine_reserved(c) && QUEUED()) { *why = MW_WAIT_PLAYING; continue; } + /* Free words only fall while playing (code goes only when quiet), so a claim that had to move or retire + * code waits for the stop instead of planning again on every tick. */ + if (!QUIET() && need >= deferred[c]) { *why = MW_WAIT_EVACUATION; continue; } + int claim = dl_engine_claim(c, need, QUIET() && SETTLED() ? 2 : 1); + deferred[c] = claim == 0 && dl_engine_why[c] == 7u && !QUIET() ? need : ~0u; if (claim <= 0) { if (claim < 0) mw_engines_refused = mw_engines_refused + 1; *why = claim < 0 ? MW_WAIT_CLAIM_REFUSED : dl_engine_why[c] >= 6u ? MW_WAIT_EVACUATION : MW_WAIT_CLAIM_BUSY; @@ -254,7 +265,10 @@ int mw_engines_place(unsigned feature, unsigned cores, uint32_t generation, cons } e->offset = (uint16_t)s.at[k], e->count = (uint16_t)u->count, e->generation = generation, e->state = MW_ENGINE_CLAIMED; } - if (!QUIET()) { *why = MW_WAIT_PLAYING; continue; } + /* Written and placed while playing too (owner's MKII, 27 October 2026: PLAY right after a boot left Analog BD + * silent until the next stop). The words go to free arena no slot runs, as a pick's upload does while playing, + * and PLACE only sets the dormant feature's base word: no track sounds different until its ENABLE. */ + if (QUEUED()) { *why = MW_WAIT_PLAYING; continue; } int r = e->state == MW_ENGINE_CLAIMED ? reserve(c, LANE_WRITE, k, generation) : e->state == MW_ENGINE_WRITTEN ? reserve(c, LANE_PLACE, k, generation) : 0; if (r > 0) e->state = e->state == MW_ENGINE_CLAIMED ? MW_ENGINE_WRITING : MW_ENGINE_PLACING, *why = MW_WAIT_NONE; diff --git a/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.h b/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.h index 1600d83b5..739b30e74 100644 --- a/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.h +++ b/sdk/machines/octatrack/module-adapters/resident-runtime/resident_engines.h @@ -70,9 +70,10 @@ void mw_engines_package(unsigned feature, uint32_t generation, const struct mw_e * the key bus) take their region in one claim and live FX code there moves once, in one evacuation. */ void mw_engines_want(unsigned features); /* Drive placement of `feature` on `cores` for a controller generation: 1 placed everywhere, 0 not yet (a job in - * flight, the sequencer playing, the manager busy, an evacuation running or waiting, no room), -1 never this boot (an - * uncertain PLACE). A core whose `busy` entry is set (the board's own job holds it) is left alone. Live FX code in the - * region is moved below it first (the manager's evacuation), only while QUIET, two seconds after the last key and + * flight, a pattern change queued, the manager busy, an evacuation or trim running or waiting, no room), -1 never this + * boot (an uncertain PLACE). A core whose `busy` entry is set (the board's own job holds it) is left alone. It places + * while the sequencer plays when the engine fits in free words. Live FX code in the region is moved below it first + * (the manager's evacuation), and cached code retired (a trim), only while QUIET, two seconds after the last key and * with no pick waiting. */ int mw_engines_place(unsigned feature, unsigned cores, uint32_t generation, const uint8_t busy[2], const uint8_t mask[2], unsigned known); diff --git a/sdk/machines/octatrack/module-adapters/resident-runtime/test_resident_transport.c b/sdk/machines/octatrack/module-adapters/resident-runtime/test_resident_transport.c index 40ebb9c16..adf5dcd30 100644 --- a/sdk/machines/octatrack/module-adapters/resident-runtime/test_resident_transport.c +++ b/sdk/machines/octatrack/module-adapters/resident-runtime/test_resident_transport.c @@ -21,10 +21,11 @@ static void checksum(unsigned core) { assert(!(sum&65535u)); } int main(void) { - uint32_t words[24]; - for(unsigned i=0;i<24;++i) words[i]=(0x123456u+i*0x731u)&0xffffffu; + enum { RESET_DATA=8*DL_DATA }; /* DL_RESET_DATA, in the owned header this test's transfer.h lacks */ + uint32_t words[RESET_DATA+1]; + for(unsigned i=0;iexpected; t[56]=(uint16_t)(j->expected>>16);', ' t[7]=(uint16_t)(opcode==11u ? j->id:j->expected); t[56]=(uint16_t)(j->expected>>16);') change('int dl_job_status(unsigned c) {', '''/* Fixed feature/logical-offset upload; the DSP maps its owned ranges independently. */ int dl_hook_reset_start(unsigned c,unsigned feature,unsigned offset,unsigned count,const uint32_t *words) { unsigned total=@TOTAL@; unsigned split=@SPLIT@; /* one owned range per chunk */ - if(c>1 || jobs[c].state || !total || (c && feature==2u) || !words || !count || count>DL_DATA || + if(c>1 || jobs[c].state || !total || (c && feature==2u) || !words || !count || count>DL_RESET_DATA || offset>=total || count>total-offset || (split && offsetsplit-offset)) return 0; for(unsigned i=0;i0xffffffu) return 0; jobs[c].upload.words=words; jobs[c].upload.relocations=0; jobs[c].upload.relocation_count=0; @@ -93,6 +96,9 @@ def _ledger_resets(): int dl_owned_upload_reserve(unsigned core,uint32_t generation,const struct dl_upload *,struct dl_owned_job *); int dl_owned_reset_reserve(unsigned core,uint32_t generation,unsigned feature,unsigned offset, unsigned count,const uint32_t *words,struct dl_owned_job *); +/* The words one reset job carries: up to eight packets, one a frame (about 3 ms), never past one owned range. A + * controller's 3,776-word reset is 20 board ticks, not 158 (2.6 s) at one packet a tick. */ +#define DL_RESET_DATA (8u*DL_DATA) /* Reserve consumes ticket/sequence and holds the sole core slot without any * transmission. Submit is the registry backend's exact start callback. * Cancel is valid only before submit; consumed identities are never reused. */ @@ -285,7 +291,7 @@ def validator(signature, mutation, name, arguments): publish(); if(out) { out->sent_sequence=owned_sent[c] ? (uint16_t)(v->first_sequence+owned_sent[c]-1u) : 0; - unsigned n=jobs[c].opcode==DL_WRITE ? (jobs[c].position+DL_DATA-1u)/DL_DATA : 0; + unsigned n=jobs[c].opcode==DL_WRITE || jobs[c].opcode==11u ? (jobs[c].position+DL_DATA-1u)/DL_DATA : 0; out->acknowledged_sequence=state==3 ? v->last_sequence : n ? (uint16_t)(v->first_sequence+n-1u) : 0; } if(state==4 || !state) { owned_close(c,OWNED_WHY_POLL); return -1; } diff --git a/sdk/tests/test_resident_engines.py b/sdk/tests/test_resident_engines.py index 8c7f67a0b..4a14ee042 100644 --- a/sdk/tests/test_resident_engines.py +++ b/sdk/tests/test_resident_engines.py @@ -127,6 +127,7 @@ def c_relocated(units, bases, work): void dl_engine_release(unsigned c,unsigned r) { (void)c;(void)r; } unsigned dl_engine_reserved(unsigned c) { (void)c;return 0; } int mw_engines_test_quiet(void) { return 1; } +int mw_engines_test_queued(void) { return 0; } int mw_engines_test_settled(void) { return 1; } volatile uint8_t dl_engine_why[2]; ''') diff --git a/sdk/tests/test_resident_engines_board.py b/sdk/tests/test_resident_engines_board.py index 183072255..949586b4a 100644 --- a/sdk/tests/test_resident_engines_board.py +++ b/sdk/tests/test_resident_engines_board.py @@ -196,8 +196,11 @@ def expected(units): MODEL = r''' #include "resident_engines.h" #include "expected.h" -static unsigned quiet = 1; -int mw_engines_test_quiet(void) { return (int)quiet; } +/* quiet: stopped with no pattern change queued; queued: a pattern change waits (so not quiet either). Playing is + * quiet = 0 with nothing queued. */ +static unsigned quiet = 1, queued; +int mw_engines_test_quiet(void) { return (int)(quiet && !queued); } +int mw_engines_test_queued(void) { return (int)queued; } /* UI ticks, the last key, a pick waiting: the evacuation's own wait (two seconds after the last key, no pick waiting). */ static unsigned ticks = 1000, key_at, pick_waiting; int mw_engines_test_settled(void) { return ticks - key_at >= 120u && !pick_waiting; } @@ -271,6 +274,11 @@ def expected(units): uint32_t mw_controller_board_test_bank(void) { return board_bank; } int mw_controller_board_test_handle(uint32_t module) { (void)module; return 63; } int modwerk_dsp_bank_names(unsigned id) { return id == 24u ? (int)names_compressor : id == 63u ? (int)names_reader : 0; } +/* The module whose machine a track of the bank's Parts is signed for (machine_registry.c), or none. */ +static uint32_t bank_module; +extern volatile uint32_t modwerk_machine_assigned; /* controller_board.c: a Part signed outside the fixture's bank change */ +unsigned modwerk_machine_bank_modules(uint32_t *module, unsigned max) +{ if (!bank_module || !max) return 0; module[0] = bank_module; return 1; } static unsigned refuse_write[2], refuse_place[2]; /* table offsets at which a WRITE chunk, a PLACE is refused */ static unsigned refuse_bind[2], drop_bind[2], refuse_once[2]; /* a BIND to this table offset refused; applied but never * answered (once); refused once */ @@ -582,15 +590,20 @@ def expected(units): assert(slot_for(SC, MW_CTL_ACTIVE) && dsp_mask[0] == 9); placed_at(0, 1, sc); placed_at(1, 1, sc); } else if (mode == 1) { - /* The guard: nothing is placed or unplaced while the sequencer plays or a pattern change waits. */ - quiet = 0; + /* The guard: nothing is placed while a pattern change waits; while the sequencer plays an engine that fits in + * free words is placed, reset and enabled; nothing is unplaced until it stops. */ + quiet = 0; queued = 1; assert(install(pkg_sc, sizeof pkg_sc) == MU_APPLIED); run(300); assert(slot_for(SC, MW_CTL_BOUND) && !mw_engines_jobs && !trails[0] && !trails[1] && !dl_engine_reserved(0)); - assert(!words[0][0] && !dsp_mask[0]); - quiet = 1; + assert(!words[0][0] && !dsp_mask[0] && mw_engines_wait[0][0] == MW_WAIT_PLAYING); + queued = 0; /* playing, nothing queued */ run(200); - assert(slot_for(SC, MW_CTL_ACTIVE)); placed_at(0, 1, sc); placed_at(1, 1, sc); + assert(slot_for(SC, MW_CTL_ACTIVE) && dsp_mask[0] == 9 && dsp_mask[1] == 9); + for (unsigned c = 0; c < 2; ++c) { placed_at(c, 1, sc); ordered(c, 1, sc, 0); } + assert(!dl_engine_evacuations && !test_trims()); + quiet = 1; + run(20); assert(refused(removal(SC), 32) == RUNTIME_BUSY); run(20); quiet = 0; /* the removal's switch, then play */ @@ -807,13 +820,17 @@ def expected(units): } else if (mode == 13) { /* Nothing moves while playing, within two seconds of a key, or while a pick waits; then it does. */ big_at_top(0); - unsigned from = trails[0], from1 = trails[1]; + unsigned from = trails[0]; quiet = 0; assert(install(pkg_sc, sizeof pkg_sc) == MU_APPLIED); run(300); - assert(trails[0] == from && trails[1] == from1 && !mw_engines_jobs && !dl_engine_evacuations && test_live(0, 3) == (int)(top - 900u)); - assert(mw_engines_wait[0][0] == MW_WAIT_PLAYING && slot_for(SC, MW_CTL_BOUND)); - quiet = 1; key_at = ticks; /* stopped by a key: core B places at once, core A waits for the two seconds */ + /* Playing: core B, which needs no move, places at once; core A's code stays where it is and nothing is written + * there. Sidechain stays off on both until core A has its engine too. */ + assert(trails[0] == from && !dl_engine_evacuations && test_live(0, 3) == (int)(top - 900u)); + /* Core A asked once, was told its code must move (why 7), and waits for the stop without asking again. */ + assert(mw_engines_wait[0][0] == MW_WAIT_EVACUATION && mw_engines[1][0].state == MW_ENGINE_PLACED); + assert(slot_for(SC, MW_CTL_BOUND) && !dsp_mask[0] && !(dsp_mask[1] & 1u)); + quiet = 1; key_at = ticks; /* stopped by a key: core A waits for the two seconds */ run(100); assert(!count_ops(0, from, 3) && !count_ops(0, from, 4) && test_live(0, 3) == (int)(top - 900u) && !dl_engine_evacuations); assert(mw_engines_wait[0][0] == MW_WAIT_EVACUATION && dl_engine_why[0] == 7 && mw_engines[1][0].state == MW_ENGINE_PLACED); @@ -1380,8 +1397,9 @@ def expected(units): } } else if (mode == 46) { /* One core's reset lags (its chunks answered late): the other core waits, its bus not enabled, until both - * resets are complete, so no reader reads that core's half of the window before it is fresh. */ - stall_reset[1] = 40; + * resets are complete, so no reader reads that core's half of the window before it is fresh. The stall stays under + * the transport's 64-frame timeout: core 0's reset packets, one a frame, advance the frames meanwhile. */ + stall_reset[1] = 24; assert(install(pkg_reader, sizeof pkg_reader) == MU_APPLIED); unsigned waited = 0; for (unsigned t = 0; t < 300 && (dsp_mask[0] & dsp_mask[1] & 8u) != 8u; ++t) { @@ -1537,7 +1555,7 @@ def expected(units): assert(install(pkg_ab, sizeof pkg_ab) == MU_APPLIED); run(400); assert(slot_for(AB, MW_CTL_BOUND) && !mw_engines_jobs && !cas_count && !dl_engine_reserved(0) && !dl_engine_reserved(1)); - assert(test_capacity(0) == top && test_capacity(1) == top); + assert(test_capacity(0) == top && test_capacity(1) == top && !modwerk_controller_settling()); /* nothing to wait for */ leasing = 4u; run(600); assert(slot_for(AB, MW_CTL_ACTIVE) && dsp_mask[0] == 4 && dsp_mask[1] == 4 && ab_reset_is(0)); @@ -1748,6 +1766,50 @@ def expected(units): run(20); assert(!mw_engines_jobs && !dl_engine_reserved(1) && mw_engines[1][0].state == MW_ENGINE_FREE && !trails[1]); assert(mw_engines_admit(1u, 0u, 0u) == MW_ENGINE_STUCK_BOOT); + } else if (mode == 65) { + /* PLAY right after a boot (owner's MKII, 27 October 2026, Analog BD silent until the next stop): used while the + * sequencer plays, Analog BD is placed, reset from its package and enabled without a stop, and nothing moves. + * Its 3,776-word reset goes eight packets a job, so the whole thing takes about 30 board ticks (half a second); + * at one 24-word packet a tick the reset alone took 158 (2.6 s). */ + quiet = 0; + unsigned start = ticks; + assert(install(pkg_ab, sizeof pkg_ab) == MU_APPLIED); + /* About as many frames a tick as the unit runs (16 samples a frame, 60 ticks a second: about 46). */ + while (!slot_for(AB, MW_CTL_ACTIVE) && ticks - start < 600u) { + ++ticks; modwerk_controller_lease_seen |= (uint8_t)leasing; modwerk_controller_tick(); test_advance(); + for (unsigned f = 0; f < 23; ++f) dsp(); + /* The boot restore's "MODULES LOADED" waits for this (modset.c). */ + assert(slot_for(AB, MW_CTL_ACTIVE) || modwerk_controller_settling()); + } + assert(!modwerk_controller_settling()); + assert(slot_for(AB, MW_CTL_ACTIVE) && dsp_mask[0] == 4 && dsp_mask[1] == 4 && ab_reset_is(0)); + placed_at(0, 4, top - 998u); placed_at(1, 4, top - 998u); + for (unsigned c = 0; c < 2; ++c) { + unsigned resets = 0; + for (unsigned i = 0; i < trails[c]; ++i) resets += trail[c][i].op == 11u && trail[c][i].w7 == 4u; + assert(resets == 158u); /* every packet still 24 words or fewer, at its own offset (the receiver's check) */ + } + assert(ticks - start <= 30u && !dl_engine_evacuations && !test_trims()); /* half a second */ + } else if (mode == 66) { + /* After a boot (owner's MKII, 28 October 2026): the restored Part's track is signed for Analog BD's machine, but + * its renderer asks for the lease only from the first trig, so the engine waited for PLAY and missed the first + * trigs. The Part's signature counts as use: placed while stopped, before any lease, and the notice waits. */ + leasing = 0; ++board_bank; + assert(install(pkg_ab, sizeof pkg_ab) == MU_APPLIED); + run(400); + assert(slot_for(AB, MW_CTL_BOUND) && !mw_engines_jobs && !modwerk_controller_settling()); + bank_module = AB; ++modwerk_machine_assigned; /* the track signed, as mw_assign counts it */ + unsigned start = ticks; + while (!slot_for(AB, MW_CTL_ACTIVE) && ticks - start < 600u) { + run(1); + assert(slot_for(AB, MW_CTL_ACTIVE) || modwerk_controller_settling()); + } + assert(slot_for(AB, MW_CTL_ACTIVE) && dsp_mask[0] == 4 && dsp_mask[1] == 4 && !modwerk_controller_settling()); + placed_at(0, 4, top - 998u); placed_at(1, 4, top - 998u); + /* A bank whose Parts sign no Analog BD track parks it again once stopped and settled. */ + bank_module = 0; ++board_bank; + run(400); + assert(slot_for(AB, MW_CTL_RETIRING)); unplaced(0, 4); unplaced(1, 4); } else assert(0); return 0; } @@ -1858,7 +1920,7 @@ def build(work): @unittest.skipUnless(shutil.which('cc'), 'host C compiler unavailable') class ResidentEnginesBoard(unittest.TestCase): - MODES = 65 + MODES = 67 def test_place_switch_run_disable_unswitch_free_and_their_refusals(self): with tempfile.TemporaryDirectory() as temp: @@ -1912,6 +1974,7 @@ def test_relocation_in_c_matches_resident_dsp_relocate(self): void dl_engine_release(unsigned c,unsigned r) { (void)c;(void)r; } unsigned dl_engine_reserved(unsigned c) { (void)c;return 0; } int mw_engines_test_quiet(void) { return 1; } +int mw_engines_test_queued(void) { return 0; } int mw_engines_test_settled(void) { return 1; } volatile uint8_t dl_engine_why[2]; ''' diff --git a/sdk/tests/test_usb_bridge.py b/sdk/tests/test_usb_bridge.py index 38e5d659a..12e1d190a 100644 --- a/sdk/tests/test_usb_bridge.py +++ b/sdk/tests/test_usb_bridge.py @@ -36,6 +36,16 @@ def setUp(self): def setup(self, request=21, direction=0xc1, length=32): return 'setup ' + struct.pack('