Skip to content

forge f2: inject(single) accumulates into the pending batch#6405

Merged
shipyard-local[bot] merged 5 commits into
mainfrom
feature/forge-f2a-bakelayer-param-injection
Jul 22, 2026
Merged

forge f2: inject(single) accumulates into the pending batch#6405
shipyard-local[bot] merged 5 commits into
mainfrom
feature/forge-f2a-bakelayer-param-injection

Conversation

@shipyard-local

@shipyard-local shipyard-local Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Fix the F2-review footgun: ParamInjector::inject(single) published a fresh
one-event snapshot per call, so the TripleBuffer's latest-snapshot-wins
collapsed two single injects to the SAME node with no intervening process()
into only the last one — a multi-param node silently dropped a param.

inject(single) now MERGES into the still-unconsumed pending batch (the new
event supersedes only that param's pending entries; other params' pending
events are preserved), so N single injects to different params between
blocks all land. The audio-side drain records the applied sequence in the
mailbox (consumed_sequence) so a post-consumption single inject starts a
fresh batch instead of re-publishing already-applied events (a re-applied
ramp would re-glide). Accumulation stays control-thread-side and single-
writer; the audio side still reads one published snapshot per block, and
the path remains allocation-free. If the pending batch is FULL of other
params' events (a full-capacity queue publish, unconsumed) the single
inject is refused loudly with PartialOverflow rather than silently
evicting a pending event.

Regression test: two single injects (delay time_ms=50ms then feedback=0.8)
between blocks — both must land (echo position AND recirculation ratio),
verified to fail against the pre-fix code. Also pins the full-pending-batch
PartialOverflow branch. Hosting SKILL.md documents the bake-layer param-
injection primitive and its batch-vs-accumulate publication contract.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV


🔎 Provenance

Agent claude
Machine m5
Tab Analyze Pulp for AI plugin builder capabilities
Directory ~/Code/pulp
Session 14ef2f39-9113-4e61-bd8b-8faab97cab00

Resume

claude --resume 14ef2f39-9113-4e61-bd8b-8faab97cab00

Jump to this tab

cmux surface focus BD0A704A-23E0-4F9B-88CC-25BD1ACDAFEA

Relaunch (any agent)

cmux surface resume get --surface BD0A704A-23E0-4F9B-88CC-25BD1ACDAFEA

Restore URLhttps://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV

stamped 2026-07-20 16:23 UTC

@shipyard-local
shipyard-local Bot enabled auto-merge (squash) July 20, 2026 16:24
auto-merge was automatically disabled July 20, 2026 20:46

Pull request was closed

@danielraffel
danielraffel force-pushed the feature/forge-f2a-bakelayer-param-injection branch from dad355a to d769efd Compare July 20, 2026 20:46
danielraffel and others added 4 commits July 20, 2026 14:11
Bake-layer parameter injection: a control thread turns a knob on a BAKED
custom node and the change lands sample-accurately in process() without
re-baking and without touching the live graph's parameter ingress.

- BakedParamView + CustomNodeBakedParam + process_instance_baked_param on
  CustomNodeType: a lowerable node declares injectable params and reads
  their ramped, sample-accurate values per block.
- ParamInjector / InjectStatus + BakedGraphProcessor::claim_param_injection:
  per-node single-writer mailbox, latest-wins, exclusive-claim discipline,
  reusing pulp::state::ParameterEvent / ParameterEventQueue verbatim.
- ParamCursor ramp introspection (ramp_target / ramp_end_offset) so a ramp
  longer than one block continues to completion across subsequent blocks
  (the C1 mid-ramp-freeze fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
Wrap the rest of the lo-fi kit as lowerable custom nodes whose one macro knob
is injectable via the F2-a bake-layer path, plus a new bitcrush/decimator (the
one block with no core/signal equivalent). Header-only catalog
(forge_lofi_catalog.hpp): the wrapped signal blocks are header-only templates,
so this adds no new link dependency to pulp-host.

Nodes (each: declares baked_params, reads its BakedParamView, lowerable=true,
transport-independent — the DelayLine node's F2-a pattern):
  * Filter (Svf lowpass)  → "Tone"  cutoff_hz            (sample-accurate)
  * Waveshaper (tanh)     → "Drive" drive                (sample-accurate)
  * Dry/Wet (DryWetMixer) → "Mix"   mix                  (block-rate, see note)
  * Noise (splitmix64)    → "Hiss"  level                (sample-accurate)
  * Bitcrush/decimator    → "Crush" bit_depth + sample_rate_reduction (new DSP)

Bitcrush is implemented inline: mid-tread quantization to 2^bit_depth levels
(fractional via exp2) + sample-and-hold decimation (phase accumulator), both
injectable.

Tests (test_forge_lofi_catalog.cpp): for each node, bake a graph containing it
and prove injecting its knob changes the baked output — cutoff attenuates a
high tone, drive grows RMS/peak, mix crossfades dry/wet, level raises the noise
floor, bit_depth grows quantization error, rate reduction flattens sample-to-
sample steps. One RT-allocation probe over every node's inject+process path.

Finding: DryWetMixerT's public API is block-rate (scalar set_mix + block
mix_wet over an internal dry buffer, no per-sample gain hook), so its mix is
applied at block granularity — the honest tradeoff for a mix macro; the other
knobs stay per-sample. Deferred (follow-up): LFO/wow modulation of delay time
is graph-internal modulation, not a macro knob.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
Add make_delay_node() to the forge lo-fi catalog: a self-contained
interpolated feedback echo (pulp::signal::DelayLine) whose time_ms and
feedback are injectable baked params, live on the baked graph with no
re-bake. Fills the gap where delay_line previously had no lowerable
catalog realization (Forge's loader lowered it to a frozen Gain-feedback
block). Covered by the F2 catalog test (feedback grows the tail;
distinct tap times leave distinct near-field energy) and the RT sweep.

Header-only (wraps the header-only DelayLine template) — no new host
link dependency. Held for the F2 architectural/adversarial review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
Fix the F2-review footgun: ParamInjector::inject(single) published a fresh
one-event snapshot per call, so the TripleBuffer's latest-snapshot-wins
collapsed two single injects to the SAME node with no intervening process()
into only the last one — a multi-param node silently dropped a param.

inject(single) now MERGES into the still-unconsumed pending batch (the new
event supersedes only that param's pending entries; other params' pending
events are preserved), so N single injects to different params between
blocks all land. The audio-side drain records the applied sequence in the
mailbox (consumed_sequence) so a post-consumption single inject starts a
fresh batch instead of re-publishing already-applied events (a re-applied
ramp would re-glide). Accumulation stays control-thread-side and single-
writer; the audio side still reads one published snapshot per block, and
the path remains allocation-free. If the pending batch is FULL of other
params' events (a full-capacity queue publish, unconsumed) the single
inject is refused loudly with PartialOverflow rather than silently
evicting a pending event.

Regression test: two single injects (delay time_ms=50ms then feedback=0.8)
between blocks — both must land (echo position AND recirculation ratio),
verified to fail against the pre-fix code. Also pins the full-pending-batch
PartialOverflow branch. Hosting SKILL.md documents the bake-layer param-
injection primitive and its batch-vs-accumulate publication contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
@shipyard-local shipyard-local Bot reopened this Jul 20, 2026
@shipyard-local
shipyard-local Bot enabled auto-merge (squash) July 20, 2026 21:14
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jul 21, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 22, 2026
@shipyard-local
shipyard-local Bot added this pull request to the merge queue Jul 22, 2026
Merged via the queue into main with commit 25be0ae Jul 22, 2026
25 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant