The ring buffer — one contiguous view of a stream¶
native/inc/doppler/buffer/buffer.h is a lock-free, single-producer /
single-consumer ring of complex samples. It makes four promises, and this
page says what each one means and where it stops.
| promise | what it means | where it stops |
|---|---|---|
| zero-copy | a reader gets a pointer into the ring, never a copy | the pointer is good until those samples are consume()d |
| free wrapping | a batch that crosses the end of the ring is still one contiguous pointer | n cannot exceed the capacity |
| lock-free | producer and consumer never block each other | exactly one of each; everything here is single-sided |
| simple | six verbs, no modes | the ring does not own a thread, a clock or a policy |
Not to be confused with Ending a Wait, which is the contract for when a blocking wait gives up, across network, memory and disk. This page is the memory transport itself.
Who uses it, and how¶
| caller | threads | how it reads |
|---|---|---|
acq |
one | writes into free space it computes, reads fixed frames off data, consume()s |
detector, detector2d |
one | chunks in through write_some(), fixed frames out through peek() |
burst_capture |
one | a history: samples addressed by absolute stream position; may be file-backed |
Python F32Buffer / F64Buffer / I16Buffer |
two | a producer thread write()s, a consumer thread wait()s |
So the ring has two audiences with opposite needs: a single-threaded DSP object that must never block, and a threaded pipeline stage that wants to. §2 and §3 are the two surfaces.
What is not known yet¶
- Whether
burst_capture's position-addressed history belongs in the ring (at(pos),restore(head, tail)) or stays that object's own (#1425).
1. Why a batch never splits¶
The pages are mapped twice, back to back. Address base + i and address
base + capacity + i are the same physical sample, so a batch that starts
near the end of the ring simply runs on into the second mapping: the MMU does
the wrap, not the code. There is no "two halves" case to write, to test, or
to get wrong in a SIMD kernel.
That is a measured claim, not a hoped-for one. bench_buffer_core reads a
batch size that never straddles the seam against one that usually does:
write_wait_consume[f32,chunk=1024] 0.328 ns/sample (never straddles)
write_wait_consume[f32,chunk=1000] 0.329 ns/sample (usually does) 1.00x
The capacity is whatever was asked for; the mapping is what gets
rounded. Indexing is a mask, not a modulo, so ->mask + 1 samples are
mapped — a power of two — and the mirror is built from whole pages, so at
least one. ->capacity stays the caller's number and every guard uses it:
the slack between the two is address space, never room. It costs nothing per
call, because the two were already separate fields, and it makes the capacity
the same on every machine — it used to be the capacity that was rounded, so
512 asked was 512 on Linux x86, 2048 on macOS arm64 and 8192 on Windows
(measurements §7).
2. Two ways to read, one way to release¶
| blocks? | for | |
|---|---|---|
dp_<t>_wait(ab, n) |
yes — spins until n samples are there, the ring is closed, or the process is interrupted |
a consumer on its own thread |
dp_<t>_peek(ab, n) |
never — n samples or NULL, at once |
a single-threaded user, where wait() would deadlock: the thread that would produce the samples is the one waiting for them |
Both return the same contiguous pointer and neither consumes. Releasing
is always dp_<t>_consume(ab, k), and k need not equal n — releasing
fewer is how overlapped frames are read.
consume(k) refuses k > available() — DP_ERR_INVALID, nothing released —
because past that the read position overtakes the write position and the two
indices stop describing a ring. A caller that releases only what it was lent
never meets it. The bound is free: the read that preceded it already loaded
the producer's index
(measurements §6).
NULL has more than one meaning, and they call for different responses, so
one function owns the precedence — dp_<t>_wait_status(ab, n):
| status | meaning | the right response |
|---|---|---|
DP_WAIT_OK |
n samples are readable |
read them |
DP_WAIT_PENDING |
fewer than n so far |
nothing is wrong; come back |
DP_WAIT_TOO_LARGE |
n exceeds the capacity |
a caller bug — it can never be satisfied |
DP_WAIT_CLOSED |
closed, with fewer than n left |
the normal end of a stream |
DP_WAIT_INTERRUPTED |
the process was asked to stop | stop |
Too-large is checked first; readable samples win over closed (a closed ring still drains); closed wins over interrupted.
3. Two ways to write¶
| when the ring lacks room | for | |
|---|---|---|
dp_<t>_write(ab, src, n) |
refuses the whole call, returns false, adds n to dropped |
a frame that is meaningless in part |
dp_<t>_write_some(ab, src, n) |
writes what fits, returns how many | a stream — including a chunk larger than the ring, which write() can never accept |
dp_<t>_space(ab) is the room write() is guaranteed to accept. dropped
counts samples in refused calls, not samples lost: a producer that retries
keeps its data and still moves the counter. write_some() never touches it.
Both policies, space(), and the answers wait_status() gives, in one
program:
/**
* ring_write_policy_demo.c — two ways to write, and what each promises.
*
* dp_f32_write() is ALL-OR-NOTHING: a block that does not fit is refused
* whole. Nothing is copied, the caller still holds every sample, and the
* refusal is counted in ->dropped -- which is therefore a count of refused
* samples, not of lost ones. Samples are lost only if the caller then
* throws them away.
*
* dp_f32_write_some() takes what fits and says how much. It never refuses,
* so it never counts a drop, and it is the only way to feed a block larger
* than the ring.
*
* dp_f32_space() is the room a write is guaranteed to find, so a producer
* that sizes its block from it is never refused at all. dp_f32_consume()
* is bounded the same way from the other side: it will not release more
* than is readable. And
* dp_f32_wait_status() answers "why can I not have n samples?" without
* blocking, in one call, for a caller that wants to say so.
*
* Build:
* make build
* ./build/native/examples/ring_write_policy_demo
*/
#include "doppler/buffer/buffer.h"
#include <stdio.h>
#include <stdlib.h>
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
fprintf (stderr, "FAIL %s:%d %s\\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} \
while (0)
int
main (void)
{
dp_f32_t *ring = dp_f32_create (1024);
CHECK (ring != NULL);
size_t cap = ring->capacity;
CHECK (cap == 1024); /* exactly what was asked, on every machine */
float *block = calloc (2 * (cap + 1), sizeof *block); /* I/Q interleaved */
CHECK (block != NULL);
/* An empty ring has room for exactly its capacity. */
CHECK (dp_f32_space (ring) == cap);
CHECK (dp_f32_available (ring) == 0);
/* One sample too many: refused WHOLE, and counted. */
CHECK (!dp_f32_write (ring, block, cap + 1));
CHECK (dp_f32_available (ring) == 0); /* nothing was copied */
CHECK (ring->dropped == cap + 1); /* refused, not lost */
/* The same block through write_some: the ring takes what fits. */
CHECK (dp_f32_write_some (ring, block, cap + 1) == cap);
CHECK (dp_f32_space (ring) == 0);
CHECK (dp_f32_write_some (ring, block, 8) == 0); /* full: 0, not an error */
CHECK (ring->dropped == cap + 1); /* write_some never drops */
/* Why can I not have n samples? Asked without blocking. */
CHECK (dp_f32_wait_status (ring, cap) == DP_WAIT_OK);
CHECK (dp_f32_wait_status (ring, cap + 1) == DP_WAIT_TOO_LARGE);
/* The release is bounded the same way: more than is there is refused,
and releases nothing. A caller that gives back only what wait() or
peek() lent never meets this, and may ignore the return. */
CHECK (dp_f32_consume (ring, cap + 1) == DP_ERR_INVALID);
CHECK (dp_f32_available (ring) == cap);
CHECK (dp_f32_consume (ring, cap) == DP_OK);
CHECK (dp_f32_wait_status (ring, 1) == DP_WAIT_PENDING); /* just not yet */
/* A producer that sizes from space() is never refused. */
size_t before = ring->dropped;
for (int k = 0; k < 100; k++)
{
size_t n = dp_f32_space (ring) < 300 ? dp_f32_space (ring) : 300;
CHECK (dp_f32_write (ring, block, n));
if (dp_f32_space (ring) == 0)
dp_f32_consume (ring, dp_f32_available (ring));
}
CHECK (ring->dropped == before);
/* close() says no more is coming; what is there can still be read. */
dp_f32_close (ring);
CHECK (dp_f32_closed (ring));
size_t left = dp_f32_available (ring);
CHECK (dp_f32_wait_status (ring, left + 1) == DP_WAIT_CLOSED);
CHECK (dp_f32_peek (ring, left) != NULL || left == 0);
/* reset() empties AND reopens, so the mapping carries a second stream.
->dropped is a lifetime count and is kept. */
dp_f32_reset (ring);
CHECK (!dp_f32_closed (ring));
CHECK (dp_f32_available (ring) == 0 && dp_f32_space (ring) == cap);
CHECK (ring->dropped == before);
printf ("write policy: capacity %zu, %zu samples refused by write(), "
"0 by write_some(), 0 when sized from space()\n",
cap, (size_t)ring->dropped);
free (block);
dp_f32_destroy (ring);
return 0;
}
4. The two patterns¶
Drip-feed until a frame is there, then take it¶
Samples arrive in pieces smaller than a frame. peek() answers "is there a
frame yet?" without ever blocking.
/**
* ring_drip_feed_demo.c — accumulate small arrivals until a frame is there.
*
* Samples arrive in pieces smaller than a frame, on ONE thread. The blocking
* dp_f32_wait() would deadlock here -- the thread that would produce the
* samples is the one waiting for them -- so the question "is there a frame
* yet?" is asked with dp_f32_peek(), which never blocks: it returns the
* frame, contiguous and zero-copy, or NULL.
*
* Self-validating: exits non-zero if a frame is missing, late, or starts at
* the wrong sample. (Checks are explicit rather than assert(): a Release
* build defines NDEBUG, and an example that validates nothing is a listing.)
*
* Build:
* make build
* ./build/native/examples/ring_drip_feed_demo
*/
#include "doppler/buffer/buffer.h"
#include <stdio.h>
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
fprintf (stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} \
while (0)
enum
{
PIECE = 100, /* complex samples per arrival */
FRAME = 1024, /* complex samples the consumer wants */
ARRIVALS = 25 /* 2500 samples in all: two full frames */
};
int
main (void)
{
dp_f32_t *ring = dp_f32_create (4096);
CHECK (ring != NULL);
/* A ramp, so a frame's first sample says where in the stream it starts. */
float piece[2 * PIECE];
size_t sent = 0, frames = 0;
for (int arrival = 0; arrival < ARRIVALS; arrival++)
{
for (size_t k = 0; k < PIECE; k++)
{
piece[2 * k] = (float)(sent + k); /* I = stream position */
piece[2 * k + 1] = 0.0f;
}
CHECK (dp_f32_write_some (ring, piece, PIECE) == PIECE);
sent += PIECE;
/* NULL until FRAME samples have accumulated; then the frame itself. */
float *frame = dp_f32_peek (ring, FRAME);
if (frame)
{
CHECK (frame[0] == (float)(frames * FRAME)); /* starts on time */
CHECK (frame[2 * (FRAME - 1)]
== (float)(frames * FRAME + FRAME - 1));
dp_f32_consume (ring, FRAME);
frames++;
}
}
CHECK (frames == (size_t)(ARRIVALS * PIECE) / FRAME);
CHECK (dp_f32_available (ring) == (size_t)(ARRIVALS * PIECE) % FRAME);
printf ("drip-feed: %d arrivals of %d -> %zu frames of %d, %zu left over\n",
ARRIVALS, PIECE, frames, FRAME, dp_f32_available (ring));
dp_f32_destroy (ring);
return 0;
}
Stream chunking: any chunk in, fixed frames out¶
The shape every FFT front end needs: input arrives in large or irregular
chunks, the transform wants exactly nfft. Feed with write_some(), drain
with peek(), and alternate — which is also how a chunk larger than the
ring goes through it.
/**
* ring_chunking_demo.c — any chunk in, fixed frames out.
*
* The shape every FFT front end needs: input arrives in large or irregular
* chunks, the transform wants exactly NFFT samples, possibly overlapped.
* Feed with dp_f32_write_some(), drain with dp_f32_peek(), and alternate the
* two -- which is also how a chunk LARGER THAN THE RING goes through it, and
* why all-or-nothing dp_f32_write() cannot do this job at all.
*
* - NFFT does not divide the ring's capacity, so frames straddle the end
* of the ring; the double mapping hands each one back contiguous anyway.
* - HOP < NFFT reads overlapped frames: consume() releases HOP, not NFFT.
*
* Self-validating: every sample of every frame is checked against the input
* stream, so a dropped, repeated or torn sample anywhere exits non-zero.
*
* Build:
* make build
* ./build/native/examples/ring_chunking_demo
*/
#include "doppler/buffer/buffer.h"
#include <stdio.h>
#include <stdlib.h>
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
fprintf (stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} \
while (0)
enum
{
NFFT = 1000, /* frame length; does not divide the capacity */
HOP = 250 /* 75% overlap */
};
int
main (void)
{
dp_f32_t *ring = dp_f32_create (1024);
CHECK (ring != NULL);
/* One input chunk, more than three times what the ring can hold. */
const size_t total = 3 * ring->capacity + 777;
float *in = (float *)malloc (2 * total * sizeof *in);
CHECK (in != NULL);
for (size_t i = 0; i < total; i++)
{
in[2 * i] = (float)i; /* I = stream position */
in[2 * i + 1] = -(float)i;
}
size_t off = 0, frames = 0, torn = 0, across_the_end = 0;
while (off < total)
{
/* Takes what fits and says how much; 0 only when the ring is full. */
off += dp_f32_write_some (ring, in + 2 * off, total - off);
float *frame;
while ((frame = dp_f32_peek (ring, NFFT)) != NULL)
{
/* NFFT contiguous samples -- even when they wrap the ring. */
if ((ring->tail & ring->mask) + NFFT > ring->capacity)
across_the_end++;
for (size_t k = 0; k < NFFT; k++)
if (frame[2 * k] != (float)(frames * HOP + k)
|| frame[2 * k + 1] != -(float)(frames * HOP + k))
torn++;
dp_f32_consume (ring, HOP); /* overlap: release HOP, keep the rest */
frames++;
}
}
CHECK (torn == 0);
CHECK (frames == (total - NFFT) / HOP + 1);
CHECK (across_the_end > 0); /* the wrap really was exercised */
printf ("chunking: one %zu-sample chunk through a %zu-sample ring -> %zu "
"frames of %d (hop %d), %zu read across the end of the ring\n",
total, ring->capacity, frames, NFFT, HOP, across_the_end);
free (in);
dp_f32_destroy (ring);
return 0;
}
The loop costs about 3% over write() + wait() at a 1024-sample frame,
and chunks much larger than the frame cost a further ~5% in cache geometry
that no API can remove
(measurements §4).
5. Threading, and what "single-sided" buys¶
Every function belongs to one side, and reads the other side's index in the direction that can only go stale safely:
| producer side | consumer side | both sides quiescent |
|---|---|---|
write, write_some, space, close |
wait, peek, available, consume, wait_status |
reset, destroy |
space() may under-report (the consumer frees room concurrently) and
available() may under-report (the producer adds samples concurrently);
neither can over-report, so neither can license an overrun. reset()
empties the ring and reopens it (closed is cleared; dropped is a
lifetime count and is kept). It writes both indices, so it is for the
single-threaded user and for between runs.
The two-thread shape whole — backpressure from space(), and an end said
with close(), after which wait() returns NULL and wait_status()
says why:
/**
* ring_threaded_demo.c — one producer thread, one consumer thread, an end.
*
* The shape the ring was built for. The producer hands over whatever block
* sizes it has; the consumer asks for exactly FRAME samples with the
* blocking dp_f32_wait(), which spins until they are there and returns them
* contiguous even when they straddle the end of the ring.
*
* Two things make it a complete program rather than a loop:
*
* - BACKPRESSURE is the producer's. dp_f32_write() never blocks; it
* refuses. So the producer waits for dp_f32_space() before it writes,
* and nothing is ever refused.
* - THE END is said out loud. An empty ring cannot tell a slow producer
* from a finished one, so the producer calls dp_f32_close(). After that
* dp_f32_wait() returns NULL instead of spinning forever, and
* dp_f32_wait_status() says the NULL means "closed", not "interrupted".
* What was written before the close is still delivered first.
*
* Self-validating: the stream is a ramp, and every sample of every frame is
* checked against its position.
*
* Build:
* make build
* ./build/native/examples/ring_threaded_demo
*/
#include "doppler/buffer/buffer.h"
#include <pthread.h>
#include <stdio.h>
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
fprintf (stderr, "FAIL %s:%d %s\\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} \
while (0)
enum
{
FRAME = 1024,
TOTAL = 50 * FRAME + 300 /* not a whole number of frames, on purpose */
};
static void *
producer (void *arg)
{
dp_f32_t *ring = arg;
static const size_t sizes[] = { 3000, 700, 4096, 129, 2048 };
float block[2 * 4096];
size_t sent = 0, turn = 0;
while (sent < TOTAL)
{
size_t n = sizes[turn++ % 5];
if (n > TOTAL - sent)
n = TOTAL - sent;
for (size_t k = 0; k < n; k++)
{
block[2 * k] = (float)(sent + k); /* I = stream position */
block[2 * k + 1] = 0.0f;
}
/* Wait for ROOM rather than retrying a refused write: the consumer
can only free space, so once it is there it stays there. */
while (dp_f32_space (ring) < n)
;
dp_f32_write (ring, block, n);
sent += n;
}
dp_f32_close (ring); /* the consumer's only way to tell slow from done */
return NULL;
}
int
main (void)
{
dp_f32_t *ring = dp_f32_create (8192);
CHECK (ring != NULL);
pthread_t tid;
CHECK (pthread_create (&tid, NULL, producer, ring) == 0);
size_t got = 0;
float *frame;
while ((frame = dp_f32_wait (ring, FRAME)) != NULL)
{
for (size_t k = 0; k < FRAME; k++)
CHECK (frame[2 * k] == (float)(got + k));
dp_f32_consume (ring, FRAME);
got += FRAME;
}
/* NULL has more than one meaning; ask which. */
CHECK (dp_f32_wait_status (ring, FRAME) == DP_WAIT_CLOSED);
/* The tail is under a frame but it is still there, and still in order. */
size_t tail = dp_f32_available (ring);
CHECK (tail == TOTAL % FRAME);
float *rest = dp_f32_peek (ring, tail);
CHECK (rest != NULL);
for (size_t k = 0; k < tail; k++)
CHECK (rest[2 * k] == (float)(got + k));
dp_f32_consume (ring, tail);
pthread_join (tid, NULL);
CHECK (ring->dropped == 0);
printf ("threaded: %d samples -> %zu frames of %d + a tail of %zu, "
"0 refused, ended by close()\n",
TOTAL, got / FRAME, FRAME, tail);
dp_f32_destroy (ring);
return 0;
}
6. What the ring deliberately does not do¶
- No thread, no timeout.
wait()spins; it is for a consumer that has a core to spend. A single-threaded user callspeek(). - No ownership of the pointer's lifetime. A pointer from
wait()orpeek()is valid until those samples are consumed; after that the producer may overwrite them. Zero-copy means exactly that.
7. The element-typed face¶
The ring stores scalars, two per complex sample. DECLARE_DP_BUFFER_VIEW
stamps the same four calls typed as one element per sample —
float _Complex, double _Complex, and for 16-bit I/Q the record
dp_iq16_t {i, q}, since C has no complex integer. Each is a cast: every
count in the header is already in samples, so the two faces cannot disagree
about a length. It is the face the Python binding is generated over.
/**
* ring_element_view_demo.c — one element per sample, on every width.
*
* The ring stores SCALARS, two per complex sample, and dp_f32_wait()
* returns a `float *`. A caller that thinks in samples would rather have
* one ELEMENT per sample, and the `_view` functions are that: the same
* four calls, the same memory, the same counts -- typed as the element.
*
* f32 float _Complex
* f64 double _Complex
* i16 dp_iq16_t { int16_t i, q; }
*
* C has no complex integer, so the 16-bit element is a two-field record.
* It is the same 4 bytes an ADC delivers (I, Q, I, Q, ...), so a capture
* buffer can be handed over with a cast and no copy.
*
* Each is a cast and nothing more: every count in buffer.h is already in
* samples, so the two faces cannot disagree about a length.
*
* Build:
* make build
* ./build/native/examples/ring_element_view_demo
*/
#include "doppler/f32_buffer/f32_buffer_core.h"
#include "doppler/i16_buffer/i16_buffer_core.h"
#include <stdio.h>
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
fprintf (stderr, "FAIL %s:%d %s\\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} \
while (0)
int
main (void)
{
/* ── complex float: write elements, read elements ─────────────────── */
dp_f32_t *f = dp_f32_create (1024);
CHECK (f != NULL);
float _Complex tone[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
CHECK (dp_f32_write_view (f, tone, 4)); /* 4 SAMPLES, not 8 floats */
CHECK (dp_f32_available (f) == 4);
float _Complex *v = dp_f32_peek_view (f, 4);
float *raw = dp_f32_peek (f, 4);
CHECK (v != NULL && (void *)v == (void *)raw); /* one memory, two types */
CHECK (v[2] == 3.0f && raw[2 * 2] == 3.0f && raw[2 * 2 + 1] == 0.0f);
dp_f32_consume (f, 4);
dp_f32_destroy (f);
/* ── 16-bit I/Q: an ADC buffer goes in with a cast ────────────────── */
dp_i16_t *q = dp_i16_create (1024);
CHECK (q != NULL);
int16_t adc[6] = { 10, 11, 20, 21, 30, 31 }; /* I, Q, I, Q, I, Q */
CHECK (dp_i16_write_some_view (q, (const dp_iq16_t *)adc, 3) == 3);
dp_iq16_t *s = dp_i16_wait_view (q, 3); /* already there: returns at once */
CHECK (s != NULL);
CHECK (s[0].i == 10 && s[0].q == 11);
CHECK (s[2].i == 30 && s[2].q == 31);
dp_i16_consume (q, 3);
dp_i16_destroy (q);
printf ("element view: f32 as float _Complex, i16 as {i, q} -- "
"same memory as the scalar face, counts in samples\n");
return 0;
}
8. A ring whose samples are a file¶
dp_<t>_create_backed() is create() over a path. The mapping is shared, so
the ring's samples are the file's contents — no write-to-disk step, no
copy, nothing to disagree. existed says whether the file already held a
ring of this size; dp_<t>_sync() is the checkpoint. What persists is the
samples: the positions live in the struct, so resuming from them is the
caller's bookkeeping.
/**
* ring_backed_demo.c — a ring whose samples are a file.
*
* dp_f32_create_backed() is dp_f32_create() over a path instead of
* anonymous memory. The mapping is shared, so the ring's samples ARE the
* file's contents: there is no separate write-to-disk step and no copy,
* and the two cannot disagree. That is what lets a history outlive the
* process that recorded it.
*
* Two calls matter beyond the ordinary ones:
*
* - `existed` says whether the file already held a ring of this size.
* 1 means its samples are now this ring's; 0 means it was created (or
* resized) and is zeroed.
* - dp_f32_sync() flushes to disk. Until then the samples are in the
* page cache, where a crash can still lose them -- so call it where a
* checkpoint is taken, not after every write.
*
* What is persisted is the SAMPLES. The read and write positions live in
* the ring's struct, not in the file, so a re-attached ring starts empty
* over its old contents: a caller that wants to resume records where it
* was, beside the file, and that bookkeeping is its own.
*
* Build:
* make build
* ./build/native/examples/ring_backed_demo
*/
#include "doppler/buffer/buffer.h"
#include <stdio.h>
#include <stdlib.h>
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
fprintf (stderr, "FAIL %s:%d %s\\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} \
while (0)
enum
{
N = 4096
};
int
main (void)
{
const char *dir = getenv ("TMPDIR");
char path[512];
snprintf (path, sizeof path, "%s/dp_ring_backed_demo.bin",
dir && *dir ? dir : "/tmp");
remove (path); /* a clean start, so `existed` below means something */
/* ── first process: record, checkpoint, go away ───────────────────── */
int existed = -1;
dp_f32_t *ring = dp_f32_create_backed (N, path, &existed);
CHECK (ring != NULL);
CHECK (existed == 0); /* created, and zeroed */
size_t cap = ring->capacity;
static float block[2 * N];
for (size_t k = 0; k < N; k++)
{
block[2 * k] = (float)k;
block[2 * k + 1] = -(float)k;
}
CHECK (dp_f32_write (ring, block, N));
dp_f32_sync (ring); /* the checkpoint: now it is on disk */
dp_f32_destroy (ring); /* unmaps; the FILE stays */
/* ── second process: the history is simply there ──────────────────── */
ring = dp_f32_create_backed (N, path, &existed);
CHECK (ring != NULL);
CHECK (existed == 1); /* same size: mapped as it stands */
CHECK (ring->capacity == cap);
CHECK (dp_f32_available (ring) == 0); /* positions are NOT in the file */
for (size_t k = 0; k < N; k++)
{
CHECK (ring->data[2 * k] == (float)k);
CHECK (ring->data[2 * k + 1] == -(float)k);
}
printf ("backed: %d samples written, synced, unmapped -- and read back "
"from %s by a fresh mapping\n",
N, path);
dp_f32_destroy (ring);
remove (path);
return 0;
}