You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The engine currently has no general-purpose mechanism for sharing data between the RT audio thread and NRT worker context that can be allocated/freed at runtime via OSC. AudioBus is preallocated at engine init; synth instances are RT-only. There's no story for, e.g., a sample buffer that:
is allocated/freed via OSC,
is written by a record synth in RT,
is read by play synths in RT,
can be analyzed (onset detection) or processed (heavy DSP) in NRT.
This issue specifies a generic resource protocol — plugins can register new resource types — with AudioBuffer as the first concrete class. Resources are identified by integer id; consumers obtain a type-checked pointer via URI match (similar to a dynamic cast).
Scope
In scope:
Generic resource definition registration via plugin API.
OSC verbs for resource lifecycle.
Synth integration: new kMethcla_ResourcePort port kind.
NRT-side use via a dispatch primitive that brackets the use with refcount.
One concrete class: AudioBuffer (multi-channel sample buffer; replaces and renames the existing MultiChannelBuffer).
Prerequisite (separate issue): dead-code cleanup in src/Methcla/Audio/Resource.hpp plus ResourceIdAllocator → IdAllocator rename — tracked in #148. The names freed by that cleanup are reused in this design.
Design decisions
#
Decision
1
Payload-mutability is per-class; descriptor is always immutable. ResourceDef declares Mutable or Immutable.
2
RT-side acquire is per-synth-lifetime, not per-pass. Refcount mutated only at synth construct / destroy / re-map. See ADR draft 0003.
3
Resource refs are a new port kind kMethcla_ResourcePort; runtime re-map via /node/set i:port-index i:resource-id. Options may also carry resource ids for shape-hint cases consumed by port_descriptor.
4
Engine owns a fixed slot pool (Options::maxNumResources). Each slot stores {state, uri, body: Methcla_Resource*, destroy_fn, refcount, free_pending}. Plugin owns the body heap allocation.
5
Interface contract: a resource URI names a C struct layout declared in a plugin-supplied header. The engine treats body as opaque (Methcla_Resource*, mirroring the existing Methcla_Synth* opacity for synth instance bodies). Consumers cast the pointer after URI match. The struct may be POD, function-pointer-heavy, or both — the engine doesn't care.
6
All slot-state mutation, refcount mutation, and lifecycle dispatch run on RT. Worker is a pure callback path for construct / destroy / perform_with_resources. No atomics needed in the slot. See ADR draft 0002.
7
NRT-side use is bracketed by methcla_world_perform_with_resources (RT-side refcount++ on dispatch, auto-release after worker returns). The Methcla_Resource* body is valid only for the duration of one perform call.
8
Synth-side bookkeeping is the plugin's responsibility (explicit acquire/release primitives). C++ wrapper provides RAII ResourceRef<T> plus a try_acquire flavor returning std::optional.
9
Id encoding: plain int32 slot index; client picks ids (NodeId / AudioBusId precedent). No generation tag.
10
Lifecycle notifications (/resource/ready, /resource/error, /resource/destroyed) originate from RT post-state-transition, dispatched via worker — mirrors /node/done. Required to avoid races between worker direct-notify and follow-up /synth/new.
11
NRT-write race protection: layered — recommend "process-into-fresh-slot" as canonical pattern (β); exclusive=true flag on the dispatch primitive as fail-fast check (γ); per-class Immutable mutability enforced by engine (δ). Strict RW enforcement on Mutable classes is rejected (would break record+play). See ADR draft 0004.
12
kMethcla_ResourcePort supports direction (Input / Output) as informational metadata; the engine does not enforce mutability against direction. Methcla_PortDescriptor stays unchanged — URI checking remains in the synth's connect().
Plugin C API
typedefvoidMethcla_Resource; // opaque handle to a plugin-allocated resource body// (mirrors `typedef void Methcla_Synth;`)typedefenum {
kMethcla_ResourceMutable,
kMethcla_ResourceImmutable
} Methcla_ResourceMutability;
typedefstructMethcla_ResourceDef {
constchar*uri;
Methcla_ResourceMutabilitymutability;
// Worker context. Parse args, allocate body, return it.// Return NULL on failure; engine notifies /resource/error.Methcla_Resource* (*construct)(Methcla_Host*host,
constvoid*tag_buffer, size_ttag_size,
constvoid*arg_buffer, size_targ_size);
// Worker context. Free body. Must be infallible.void (*destroy)(Methcla_Host*host, Methcla_Resource*resource);
} Methcla_ResourceDef;
// Registered via Methcla_Host at plugin load time (parallels register_synthdef).void (*register_resource_def)(Methcla_Host*, constMethcla_ResourceDef*);
RT-side primitives
typedefint32_tMethcla_ResourceId;
// Returns typed body pointer after URI check; increments refcount.// Returns NULL on URI mismatch, slot not Live, free_pending, or out of range.Methcla_Resource*methcla_world_resource_acquire(
Methcla_World*, Methcla_ResourceId, constchar*expected_uri);
// Decrements refcount; if hits 0 and free_pending, posts destroy-cmd to worker.voidmethcla_world_resource_release(Methcla_World*, Methcla_ResourceId);
Consumers cast the returned pointer to the typed interface struct, e.g. (AudioBuffer*)methcla_world_resource_acquire(world, id, AudioBuffer_URI).
NRT-side primitive
// RT acquires each resource (refcount++); on worker return, RT auto-releases all.// If exclusive=true, dispatch fails unless every listed resource has refcount==1.// Worker callback receives body pointers via `data` (engine packs them).voidmethcla_world_perform_with_resources(
Methcla_World*,
constMethcla_ResourceId*resources, size_tnum_resources,
boolexclusive,
Methcla_HostPerformFunctionperform, void*data);
C++ wrapper
RAII handle with throwing and optional variants:
// Throws methcla::ResourceAcquireError on failure.
methcla::ResourceRef<AudioBuffer> buf(world, opts.bufferId);
// std::optional, empty on failure.auto buf = methcla::try_acquire<AudioBuffer>(world, opts.bufferId);
// In connect(): replace contents, return false on failure (no-op on failure).bool ok = m_buf.try_replace(world, new_id);
The ResourceRef<T> name reuses the symbol freed by the dead-code cleanup in #148 (the previous unused ResourceRef<T> = intrusive_ptr<T> alias).
free_pending is set on a Live slot when /resource/free arrives but refcount > 0; the last release to drain refcount then initiates Destroying. The same flag is set on a Constructing slot if free arrives mid-construct; transition to Destroying happens immediately after Constructing → Live, so the slot never sits Live-and-doomed beyond one transition.
Threading
Event
Thread
Touches
/resource/new arrives
RT
slot Free → Constructing; post worker construct
Worker construct returns
NRT → posts back
(worker) sends publish-cmd to RT
RT drains publish-cmd
RT
slot → Live; post ResourceReadyNotification to worker
Worker dispatches /resource/ready
NRT
calls packet handler
Synth bind in /synth/new or /node/set
RT
refcount++
Synth destruction (Node::free)
RT
refcount-- for each held resource
/resource/free arrives
RT
if refcount==0 → Destroying + post worker destroy; else free_pending=true
Worker destroy returns
NRT → posts back
RT publishes Free; notifies via worker
perform_with_resources dispatch
RT
per-resource refcount++; post worker perform
Worker perform returns
NRT → posts back
RT auto-releases all (refcount--)
Critical ordering: EnvironmentImpl::process drains the request queue beforem_worker->perform(). Lifecycle notifications must therefore originate from RT after the state transition, dispatched via worker. Sending notifications directly from the worker would let a client's follow-up /synth/new race the publish-cmd.
Failure modes
Where
Surfaces as
/resource/new validate (id range / slot occupied / definition unknown)
replyError, RT-synchronous
Plugin construct returns NULL
/resource/error i:id s:reason notification; slot returns to Free
/resource/free validate (id invalid / state wrong)
replyError
Plugin destroy
Contractually infallible
Resource port acquire fails in synth construct
C++ wrapper throws → processMessagereplyError on the /synth/new
Resource port acquire fails in synth connect (re-map)
Retain prior binding, log warning, silent to client
perform_with_resources dispatch with exclusive=true and refcount>1
Synchronous fail; no acquires performed; client-visible error
Mutation contract published to plugin authors
A ResourceDef is Mutable (default; suits buffers) or Immutable (engine rejects write-acquires; modifications = new resource id; suits wavetables, lookup tables).
For Mutable classes, NRT writes are safe only when nothing else holds the resource. Recommended pattern: process-into-fresh-slot — allocate destination Y, dispatch perform_with_resources({X, Y}, ...) reading X and writing Y, optionally re-bind synths from X to Y via /node/set, free X.
For in-place writes, use exclusive=true. The engine rejects dispatch if any other holder exists, catching lifecycle bugs at dispatch time.
RT-side mutation (record synths) on Mutable classes is the user's responsibility, controlled by node ordering. The engine does not detect this.
First resource class: AudioBuffer
URI: http://methc.la/resources/AudioBuffer/v1
Mutability: kMethcla_ResourceMutable.
Renamed from the existing MultiChannelBuffer (src/Methcla/Audio/MultiChannelBuffer.hpp) — the rename happens as part of this issue's implementation.
Interface header (include/methcla/plugins/audio_buffer.h) publishes the C struct layout (channels, frames, sample rate, float** data — see src/Methcla/Audio/MultiChannelBuffer.hpp:25-133 for the current C++ form).
construct(host, tags, args): parses i:numChannels i:numFrames f:sampleRate, allocates via methcla_host_alloc_aligned. Returns Methcla_Resource* pointing to the constructed AudioBuffer body.
destroy(host, resource): frees the channel arrays and the descriptor.
Engine implementation notes
Slot pool: std::array<ResourceSlot, maxNumResources> (or fixed-size vector at engine init).
All slot fields are plain types (no atomics); the state, refcount, free_pending, and body are RT-only.
Worker→RT command FIFO is the existing Worker infrastructure (m_worker->sendFromWorker).
Notifications follow the existing Notification / NodeNotification pattern in EngineImpl.hpp — define ResourceReadyNotification, ResourceErrorNotification, ResourceDestroyedNotification.
Synth allocation layout is unchanged at the engine level (no engine-side per-synth resource binding table; the plugin owns its refs).
Engine shutdown ordering
Stop accepting new OSC.
Free all nodes (synth destructors release their resource refs).
For each remaining Live slot, force-destroy via worker.
Wait for in-flight Constructing slots' workers to finish; immediately destroy.
Drain worker FIFOs.
Tear down slot pool.
Plugin classes remain valid for the lifetime of the engine that registered them. Per-plugin unload is not supported in this iteration.
Slot pool + state machine + lifecycle protocol. No plugin classes registered yet; engine compiles with the new API surface. Internal test that registers a dummy class and exercises all state transitions.
Disksampler / sampler / future record synth migration. Use resource ports for buffer references instead of plugin-internal sample storage. (Separate issue.)
Steps 2–6 are mechanical and orthogonal to plugin work. Step 7 produces the first observable feature.
CONTEXT.md updates (apply when implementing)
Add Resource entry: "A plugin-registered shared data object managed by the Engine, identified by an integer id and a type URI." _Avoid_: instance (a Synth is an instance; a Resource has a body).
Add ResourceDef entry mirroring SynthDef: "A Resource type registered by a Plugin. Describes the constructor, destructor, and interface URI for one kind of shared Resource." _Avoid_: resource class, resource type.
Add AudioBuffer entry: "A multi-channel sample-data Resource. Used for recording and playback by Synths." _Acceptable shorthand: buffer, in code.
Narrow AudioBus_Avoid_ from channel, buffer, bus (acceptable shorthand in code) to channel, bus (acceptable shorthand in code) — buffer drops out (it now names a distinct concept).
ADR drafts
To file as docs/adr/0002-...md, 0003-...md, 0004-...md when the implementation PR lands.
0002 — RT-only mutation of resource slot state
Methcla's RT thread owns OSC dispatch, runs every state transition through EnvironmentImpl::process, and is responsible for synth destruction in Node::free (src/Methcla/Audio/Node.cpp:57-64). Every event that mutates a resource slot — creation, refcount changes from synth bind/unbind, free request, completion of worker construct/destroy — originates from RT. The slot's state, refcount, and free_pending fields are therefore single-writer (RT) and need no atomic synchronization. The worker thread is a pure async-callback for the plugin's construct/destroy and never reads or writes the slot directly; it returns results to RT via the existing worker→RT command FIFO.
This deliberately departs from the protocol sketched in docs/rt-resource-lifecycle.md, which assumes RT-acquires + NRT-frees and prescribes an atomic refcount plus a dying flag with a seq_cst Dekker handshake. Methcla's threading topology makes that machinery unnecessary. A future maintainer reading the lifecycle doc may expect to see atomics in the slot and try to "fix" the apparent omission — they should not. NRT use of a resource is always bracketed by methcla_world_perform_with_resources, whose RT-side acquire/release brackets the worker call, so the only mutator of refcount remains RT.
0003 — Per-synth-lifetime resource acquire
A Synth binds to a Resource once — at construct time (from a synth option) or at runtime via /node/set re-map (in connect()) — and holds the binding for its entire lifetime, releasing in the synth destructor. The RT-side process() callback dereferences the cached Methcla_Resource* body pointer directly, doing no refcount work per pass.
This rejects the per-pass acquire/release pattern from docs/rt-resource-lifecycle.md, which had every RT call-site fetch_add/fetch_sub the refcount per pass. Per-pass gives a tight reclamation bound (one pass to drain in-flight references); per-synth-lifetime gives a looser bound — the resource is freed when the last synth using it is destroyed — in exchange for zero RT process()-time overhead. Methcla's typical use case is buffers held by long-lived record/play synths over seconds-to-minutes spans, where the per-pass cost would be all overhead and no benefit. NRT-side "use during processing" is handled separately by methcla_world_perform_with_resources, which brackets each NRT operation with its own RT-mediated acquire/release.
0004 — Mutable resources have discipline-only concurrency
A Methcla_ResourceDef declares its kind as kMethcla_ResourceMutable or kMethcla_ResourceImmutable. Mutable resources (like AudioBuffer) allow concurrent RT and NRT writes; the engine does not enforce reader/writer exclusion. The contract published to clients: (i) process-into-fresh-slot is the canonical safe pattern for NRT writes (allocate a destination, process into it, optionally re-bind synths via /node/set); (ii) methcla_world_perform_with_resources(..., exclusive=true) is a fail-fast check that rejects dispatch unless every listed resource has refcount == 1; (iii) RT-side mutation (a record synth writing while play synths read) is the user's responsibility, controlled by node ordering.
Strict reader/writer enforcement was considered and rejected because it would break the use case that motivated the whole design: a record synth (writer) and one or more play synths (readers) sharing the same buffer within a single audio pass, with node ordering ensuring the record synth runs before its readers. Enforcing exclusion would force clients to either kill and recreate synths around every write (high-friction) or pass an override flag that makes enforcement advisory anyway. Immutable resources exist as a stricter alternative for classes where mutation is meaningless — wavetables, lookup tables, FFT plans; the engine rejects writes on those.
Motivation
The engine currently has no general-purpose mechanism for sharing data between the RT audio thread and NRT worker context that can be allocated/freed at runtime via OSC.
AudioBusis preallocated at engine init; synth instances are RT-only. There's no story for, e.g., a sample buffer that:This issue specifies a generic resource protocol — plugins can register new resource types — with
AudioBufferas the first concrete class. Resources are identified by integer id; consumers obtain a type-checked pointer via URI match (similar to a dynamic cast).Scope
In scope:
kMethcla_ResourcePortport kind.AudioBuffer(multi-channel sample buffer; replaces and renames the existingMultiChannelBuffer).Prerequisite (separate issue): dead-code cleanup in
src/Methcla/Audio/Resource.hppplusResourceIdAllocator→IdAllocatorrename — tracked in #148. The names freed by that cleanup are reused in this design.Design decisions
MutableorImmutable.kMethcla_ResourcePort; runtime re-map via/node/set i:port-index i:resource-id. Options may also carry resource ids for shape-hint cases consumed byport_descriptor.Options::maxNumResources). Each slot stores{state, uri, body: Methcla_Resource*, destroy_fn, refcount, free_pending}. Plugin owns the body heap allocation.bodyas opaque (Methcla_Resource*, mirroring the existingMethcla_Synth*opacity for synth instance bodies). Consumers cast the pointer after URI match. The struct may be POD, function-pointer-heavy, or both — the engine doesn't care.construct/destroy/perform_with_resources. No atomics needed in the slot. See ADR draft 0002.methcla_world_perform_with_resources(RT-side refcount++ on dispatch, auto-release after worker returns). TheMethcla_Resource*body is valid only for the duration of oneperformcall.acquire/releaseprimitives). C++ wrapper provides RAIIResourceRef<T>plus atry_acquireflavor returningstd::optional./resource/ready,/resource/error,/resource/destroyed) originate from RT post-state-transition, dispatched via worker — mirrors/node/done. Required to avoid races between worker direct-notify and follow-up/synth/new.exclusive=trueflag on the dispatch primitive as fail-fast check (γ); per-classImmutablemutability enforced by engine (δ). Strict RW enforcement on Mutable classes is rejected (would break record+play). See ADR draft 0004.kMethcla_ResourcePortsupports direction (Input / Output) as informational metadata; the engine does not enforce mutability against direction.Methcla_PortDescriptorstays unchanged — URI checking remains in the synth'sconnect().Plugin C API
RT-side primitives
Consumers cast the returned pointer to the typed interface struct, e.g.
(AudioBuffer*)methcla_world_resource_acquire(world, id, AudioBuffer_URI).NRT-side primitive
C++ wrapper
RAII handle with throwing and optional variants:
The
ResourceRef<T>name reuses the symbol freed by the dead-code cleanup in #148 (the previous unusedResourceRef<T> = intrusive_ptr<T>alias).OSC API
/resource/news:definition-name i:resource-id [class-specific args]/synth/new s:definition-name./resource/freei:resource-idrefcount==0; else markfree_pending./resource/readyi:resource-id/resource/errori:resource-id s:reason/resource/destroyedi:resource-idSynth integration via
/node/set:kMethcla_ControlPortf:value(canonical);i:valueaccepted (converted to float)kMethcla_ResourcePorti:valueonlyf:against a resource port =replyErrorSlot state machine
free_pendingis set on a Live slot when/resource/freearrives but refcount > 0; the lastreleaseto drain refcount then initiates Destroying. The same flag is set on a Constructing slot if free arrives mid-construct; transition to Destroying happens immediately after Constructing → Live, so the slot never sits Live-and-doomed beyond one transition.Threading
/resource/newarrivesResourceReadyNotificationto worker/resource/ready/synth/newor/node/setrefcount++Node::free)refcount--for each held resource/resource/freearrivesrefcount==0→ Destroying + post worker destroy; elsefree_pending=trueperform_with_resourcesdispatchrefcount++; post worker performrefcount--)Critical ordering:
EnvironmentImpl::processdrains the request queue beforem_worker->perform(). Lifecycle notifications must therefore originate from RT after the state transition, dispatched via worker. Sending notifications directly from the worker would let a client's follow-up/synth/newrace the publish-cmd.Failure modes
/resource/newvalidate (id range / slot occupied / definition unknown)replyError, RT-synchronousconstructreturns NULL/resource/error i:id s:reasonnotification; slot returns to Free/resource/freevalidate (id invalid / state wrong)replyErrordestroyconstructprocessMessagereplyErroron the/synth/newconnect(re-map)perform_with_resourcesdispatch withexclusive=trueand refcount>1Mutation contract published to plugin authors
A ResourceDef is Mutable (default; suits buffers) or Immutable (engine rejects write-acquires; modifications = new resource id; suits wavetables, lookup tables).
For Mutable classes, NRT writes are safe only when nothing else holds the resource. Recommended pattern: process-into-fresh-slot — allocate destination Y, dispatch
perform_with_resources({X, Y}, ...)reading X and writing Y, optionally re-bind synths from X to Y via/node/set, free X.For in-place writes, use
exclusive=true. The engine rejects dispatch if any other holder exists, catching lifecycle bugs at dispatch time.RT-side mutation (record synths) on Mutable classes is the user's responsibility, controlled by node ordering. The engine does not detect this.
First resource class:
AudioBufferURI:
http://methc.la/resources/AudioBuffer/v1Mutability:
kMethcla_ResourceMutable.Renamed from the existing
MultiChannelBuffer(src/Methcla/Audio/MultiChannelBuffer.hpp) — the rename happens as part of this issue's implementation.Interface header (
include/methcla/plugins/audio_buffer.h) publishes the C struct layout (channels, frames, sample rate,float**data — seesrc/Methcla/Audio/MultiChannelBuffer.hpp:25-133for the current C++ form).construct(host, tags, args): parsesi:numChannels i:numFrames f:sampleRate, allocates viamethcla_host_alloc_aligned. ReturnsMethcla_Resource*pointing to the constructedAudioBufferbody.destroy(host, resource): frees the channel arrays and the descriptor.Engine implementation notes
std::array<ResourceSlot, maxNumResources>(or fixed-size vector at engine init).state,refcount,free_pending, andbodyare RT-only.Workerinfrastructure (m_worker->sendFromWorker).Notification/NodeNotificationpattern inEngineImpl.hpp— defineResourceReadyNotification,ResourceErrorNotification,ResourceDestroyedNotification.Engine shutdown ordering
Plugin classes remain valid for the lifetime of the engine that registered them. Per-plugin unload is not supported in this iteration.
Implementation plan
Suggested PR slicing:
src/Methcla/Audio/Resource.hppandResourceIdAllocator→IdAllocatorrename (Delete unused refcount / Resource scaffolding in src/Methcla/Audio/Resource.hpp #148)./resource/new,/resource/free, notifications.kMethcla_ResourcePort,/node/set i:port-idx i:resource-iddispatch, C API foracquire/release.methcla_world_perform_with_resources(single + array, exclusive flag).ResourceRef<T>+try_acquire.AudioBufferresource class. RenameMultiChannelBuffer→AudioBuffer; built-in registration; interface header ininclude/methcla/plugins/audio_buffer.h.Steps 2–6 are mechanical and orthogonal to plugin work. Step 7 produces the first observable feature.
CONTEXT.md updates (apply when implementing)
_Avoid_: instance (a Synth is an instance; a Resource has a body)._Avoid_: resource class, resource type._Acceptable shorthand: buffer, in code._Avoid_fromchannel, buffer, bus (acceptable shorthand in code)tochannel, bus (acceptable shorthand in code)—bufferdrops out (it now names a distinct concept).ADR drafts
To file as
docs/adr/0002-...md,0003-...md,0004-...mdwhen the implementation PR lands.0002 — RT-only mutation of resource slot state
Methcla's RT thread owns OSC dispatch, runs every state transition through
EnvironmentImpl::process, and is responsible for synth destruction inNode::free(src/Methcla/Audio/Node.cpp:57-64). Every event that mutates a resource slot — creation, refcount changes from synth bind/unbind, free request, completion of worker construct/destroy — originates from RT. The slot'sstate,refcount, andfree_pendingfields are therefore single-writer (RT) and need no atomic synchronization. The worker thread is a pure async-callback for the plugin'sconstruct/destroyand never reads or writes the slot directly; it returns results to RT via the existing worker→RT command FIFO.This deliberately departs from the protocol sketched in
docs/rt-resource-lifecycle.md, which assumes RT-acquires + NRT-frees and prescribes an atomic refcount plus adyingflag with a seq_cst Dekker handshake. Methcla's threading topology makes that machinery unnecessary. A future maintainer reading the lifecycle doc may expect to see atomics in the slot and try to "fix" the apparent omission — they should not. NRT use of a resource is always bracketed bymethcla_world_perform_with_resources, whose RT-side acquire/release brackets the worker call, so the only mutator of refcount remains RT.0003 — Per-synth-lifetime resource acquire
A Synth binds to a Resource once — at construct time (from a synth option) or at runtime via
/node/setre-map (inconnect()) — and holds the binding for its entire lifetime, releasing in the synth destructor. The RT-sideprocess()callback dereferences the cachedMethcla_Resource*body pointer directly, doing no refcount work per pass.This rejects the per-pass acquire/release pattern from
docs/rt-resource-lifecycle.md, which had every RT call-sitefetch_add/fetch_subthe refcount per pass. Per-pass gives a tight reclamation bound (one pass to drain in-flight references); per-synth-lifetime gives a looser bound — the resource is freed when the last synth using it is destroyed — in exchange for zero RTprocess()-time overhead. Methcla's typical use case is buffers held by long-lived record/play synths over seconds-to-minutes spans, where the per-pass cost would be all overhead and no benefit. NRT-side "use during processing" is handled separately bymethcla_world_perform_with_resources, which brackets each NRT operation with its own RT-mediated acquire/release.0004 — Mutable resources have discipline-only concurrency
A
Methcla_ResourceDefdeclares its kind askMethcla_ResourceMutableorkMethcla_ResourceImmutable. Mutable resources (likeAudioBuffer) allow concurrent RT and NRT writes; the engine does not enforce reader/writer exclusion. The contract published to clients: (i)process-into-fresh-slotis the canonical safe pattern for NRT writes (allocate a destination, process into it, optionally re-bind synths via/node/set); (ii)methcla_world_perform_with_resources(..., exclusive=true)is a fail-fast check that rejects dispatch unless every listed resource has refcount == 1; (iii) RT-side mutation (a record synth writing while play synths read) is the user's responsibility, controlled by node ordering.Strict reader/writer enforcement was considered and rejected because it would break the use case that motivated the whole design: a record synth (writer) and one or more play synths (readers) sharing the same buffer within a single audio pass, with node ordering ensuring the record synth runs before its readers. Enforcing exclusion would force clients to either kill and recreate synths around every write (high-friction) or pass an override flag that makes enforcement advisory anyway. Immutable resources exist as a stricter alternative for classes where mutation is meaningless — wavetables, lookup tables, FFT plans; the engine rejects writes on those.
Related issues
IdAllocatorrename (prerequisite).