Haiku BBufferGroup: Media Buffer Pools and Ownership
Design Haiku Media Kit buffer pools with BBufferGroup, bounded requests, explicit recycling, negotiated sizes, and teardown that cannot strand buffers.
BBufferGroup manages reusable BBuffer objects for Haiku Media Kit nodes. A group is not just a faster allocator: it participates in a producer-consumer protocol where a buffer may be temporarily held by a downstream node. Designing the pool therefore means choosing a capacity, matching buffer size to the negotiated format, bounding waits, and ensuring every acquired buffer is either transferred according to the protocol or recycled.
The Media Kit aims to move media data efficiently between nodes, often by sharing the underlying buffer storage rather than copying every payload. That efficiency makes lifetime rules more important, not less. If a consumer holds every buffer while a producer waits indefinitely for another, the graph can stall even though the machine has memory and CPU available. Buffer starvation is a queueing and ownership problem.
Choose the group constructor deliberately
The public header provides constructors for a group that allocates buffers using a size, count, placement, and lock policy; a default constructor; and a constructor based on existing media buffer IDs. Always check InitCheck() before relying on the group. Construction can fail because of allocation or system constraints. A successful object allocation in C++ does not prove that its internal pool is ready.
The count should reflect the pipeline’s actual concurrency and latency, not an arbitrary large number. A producer may need one buffer being filled, another in transit, and one or more held by a consumer. More buffers can absorb scheduling jitter, but increase memory use and can hide a slow consumer for longer before the system reaches backpressure. Fewer buffers reduce memory but make transient delay easier to expose as starvation.
Size the pool for the negotiated media format. For uncompressed audio, calculate bytes from sample representation, channel count, and frame count with overflow checks. For video, account for row stride, height, and the actual color-space layout rather than assuming tightly packed width times bytes-per-pixel. Encoded formats can be variable-size, so choose a documented maximum or a safe upper bound and reject payloads that exceed it. Never allow a producer to write beyond SizeAvailable().
The lock and placement parameters of the allocation constructor affect memory behavior and must be selected according to the node’s constraints. Locked memory can improve predictability for certain real-time paths but consumes a finite system resource; it is not a general performance switch. Use defaults unless the API documentation and measurements justify a different policy.
Bound requests and observe failures
RequestBuffer(size, timeout) can return a buffer or NULL; the overload accepting a specific buffer reports a status. The default timeout in the header is infinite, which is convenient in simple examples but dangerous in a production path that must remain responsive. Use a finite timeout or a non-blocking strategy where the node’s contract allows it. Treat failure as an expected control path: record the miss, apply an explicit drop or backpressure policy, and preserve the graph’s timing requirements.
Do not respond to a timeout by allocating unbounded replacement buffers. That defeats the pool limit and creates a memory-pressure failure under the same load that caused the timeout. If dropping is acceptable, drop with accounting and report discontinuity when required by the media contract. If it is not acceptable, propagate a bounded backpressure signal or stop cleanly. The right response differs for live capture, offline rendering, and file playback; it must be an explicit product decision.
Useful instrumentation includes pool capacity, current requests, timeouts, outstanding buffers, average and maximum hold time, consumer identity, and format. Measure under worst-case downstream work and device changes. A log message per frame can itself perturb a real-time path, so use counters or sampled diagnostics and report them outside the deadline-sensitive callback.
Transfer, consume, recycle
A producer uses SendBuffer() to pass a buffer to a consumer after filling it and setting valid payload size and media metadata. A consumer implements BufferReceived() and must follow the buffer-recycling/forwarding rules of its role. BBuffer::Recycle() returns a buffer for reuse through its owning group when that is the correct end of the ownership path. Do not keep the buffer pointer after returning it or after handing it to code that may recycle it.
The data pointer is valid only while the buffer remains owned by the current stage. A consumer that needs to retain media beyond the callback must copy the required bytes into storage it owns or use the Media Kit’s explicit forwarding/retention protocol. Retaining a raw pointer and later reading it after recycle risks observing a different frame written into the same memory.
Every branch needs a release policy. If data validation fails before transfer, recycle the acquired buffer. If a downstream send fails, consult the API contract for whether ownership was transferred; do not blindly recycle twice. If a consumer filters a buffer out, recycle it. If it forwards to another output, preserve the required media header and follow the producer/consumer handshake. Build a small ownership table for each node rather than relying on an informal “the framework owns it” assumption.
An illustrative producer-side outline is:
BBuffer* buffer = group.RequestBuffer(requiredBytes, timeout);
if (buffer == NULL)
return B_WOULD_BLOCK;
if (requiredBytes > buffer->SizeAvailable()) {
buffer->Recycle();
return B_BUFFER_OVERFLOW;
}
FillPayload(buffer->Data(), requiredBytes);
buffer->SetSizeUsed(requiredBytes);
// Populate the negotiated media header before sending.
status_t status = SendBuffer(buffer, source, destination);
if (status != B_OK)
buffer->Recycle();
This is an ownership-oriented outline, not a complete Media Kit node: FillPayload, endpoints, timestamp/header values, and error policy belong to the node. Verify the precise ownership result for failed sends against the target Haiku API before applying the recycle branch. A production implementation must avoid double recycling if an API can transfer ownership before returning an error.
Pool exhaustion and group teardown
WaitForBuffers() and ReclaimAllBuffers() provide explicit coordination operations. They do not eliminate the need to understand which downstream nodes may still own buffers. Reclaiming too early can disrupt active graph processing; waiting forever during shutdown can hang application exit. Stop or disconnect the graph in a defined order, prevent new requests, wait for outstanding work within a bounded shutdown budget, then reclaim and destroy the group as appropriate.
Handle reconfiguration carefully. A format change may require a different buffer size or count. Do not replace a group while callbacks still reference the old group. Coordinate the producer’s format-change hooks, downstream acceptance, in-flight buffers, and storage teardown. If the format cannot be supported within configured memory limits, reject it explicitly instead of truncating or silently reinterpreting payloads.
If a group is built from existing buffer IDs, validate the clone information and lifetime of the underlying areas. A buffer identifier is not a file-like permanent reference. Never persist it across reboot or assume that an ID from one media graph can be reused in a later session. Keep group and buffer lifecycle tied to the graph instance.
Sizing with an explicit budget
Before allocating, compute a memory budget: buffer bytes multiplied by count, plus node state, temporary conversions, and any parallel streams. Check multiplication overflow before allocation. For example, if frameBytes derives from a stride and height, ensure stride <= SIZE_MAX / height before multiplying. Reject unreasonable format dimensions before asking the system for memory. This makes allocation failure deterministic and gives the user a useful explanation.
Latency budgeting matters too. If buffers represent 20 ms each, a pool of six could permit roughly 120 ms of data to exist in the pipeline, depending on how the graph uses them. That arithmetic is an upper-bound intuition, not a guaranteed end-to-end latency formula. Measure the actual graph and account for queueing, conversion, scheduling, and hardware latency separately.
Validation and acceptance criteria
Test group initialization failure, one buffer, the intended maximum count, a timeout, downstream slowdown, a format change, consumer rejection, and shutdown with buffers in flight. Use a deterministic test producer and consumer that deliberately delay recycling. Confirm the pool never exceeds its memory budget, the producer follows its drop/backpressure policy, and teardown completes without leaked or double-recycled buffers.
Run long-duration tests with real audio/video endpoints and inspect counters for starvation and late data. Validate that timestamps and SizeUsed() match the bytes actually written. Test corrupt or unusually large format proposals and ensure they are rejected before allocation. Repeatedly start, stop, disconnect, and reconnect the graph to expose stale group pointers.
The acceptance criterion is not simply “no allocation error.” The pool must be sized for the negotiated workload, every ownership transition must be accounted for, requests must have a bounded failure policy, and shutdown must safely resolve outstanding work. That discipline turns BBufferGroup from an opaque queue of memory into a predictable part of a real-time media graph.
Related:
- The Media Kit: Real-Time Audio and Video in Haiku
- Haiku BMediaFile and BMediaTrack: Container and Track Workflows
Sources: