@camstack/shm-ring 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/binding.gyp +35 -0
- package/dist/frame-ring-reader-cache.d.ts +22 -0
- package/dist/frame-ring-reader-cache.d.ts.map +1 -0
- package/dist/frame-ring.d.ts +379 -0
- package/dist/frame-ring.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +655 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +643 -0
- package/dist/index.mjs.map +1 -0
- package/dist/native.d.ts +34 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +226 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +224 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/testing/looping-clip-frame-source.d.ts +57 -0
- package/dist/testing/looping-clip-frame-source.d.ts.map +1 -0
- package/package.json +59 -0
- package/prebuilds/linux-x64/@camstack+shm-ring.node +0 -0
- package/src/shm.cc +321 -0
package/binding.gyp
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
{
|
|
4
|
+
"target_name": "shm_ring",
|
|
5
|
+
"sources": [ "src/shm.cc" ],
|
|
6
|
+
"include_dirs": [
|
|
7
|
+
"<!@(node -p \"require('node-addon-api').include\")"
|
|
8
|
+
],
|
|
9
|
+
"defines": [
|
|
10
|
+
"NAPI_DISABLE_CPP_EXCEPTIONS",
|
|
11
|
+
"NAPI_VERSION=8"
|
|
12
|
+
],
|
|
13
|
+
"cflags!": [ "-fno-exceptions" ],
|
|
14
|
+
"cflags_cc!": [ "-fno-exceptions" ],
|
|
15
|
+
"conditions": [
|
|
16
|
+
[ "OS=='linux'", {
|
|
17
|
+
"libraries": [ "-lrt" ]
|
|
18
|
+
} ],
|
|
19
|
+
[ "OS=='mac'", {
|
|
20
|
+
"xcode_settings": {
|
|
21
|
+
"CLANG_CXX_LANGUAGE_STANDARD": "c++17",
|
|
22
|
+
"MACOSX_DEPLOYMENT_TARGET": "11.0"
|
|
23
|
+
}
|
|
24
|
+
} ],
|
|
25
|
+
[ "OS=='win'", {
|
|
26
|
+
"msvs_settings": {
|
|
27
|
+
"VCCLCompilerTool": {
|
|
28
|
+
"ExceptionHandling": 1
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} ]
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DecodedFrame, FrameHandle, IScopedLogger } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* Opens (and caches) a `FrameRingReader` per `shmId` and reads the pixels a
|
|
4
|
+
* `FrameHandle` refers to. Single-consumer — one cache per frame subscription.
|
|
5
|
+
*/
|
|
6
|
+
export declare class FrameRingReaderCache {
|
|
7
|
+
private readonly rings;
|
|
8
|
+
private readonly logger;
|
|
9
|
+
private closed;
|
|
10
|
+
constructor(logger?: IScopedLogger);
|
|
11
|
+
/**
|
|
12
|
+
* Read the pixels a `FrameHandle` refers to and return them as a
|
|
13
|
+
* `DecodedFrame`. Returns `null` when the ring slot was recycled before the
|
|
14
|
+
* read (a dropped frame) or the segment could not be opened.
|
|
15
|
+
*/
|
|
16
|
+
read(handle: FrameHandle): DecodedFrame | null;
|
|
17
|
+
/** Close every cached segment. Idempotent. */
|
|
18
|
+
close(): void;
|
|
19
|
+
/** Get the cached reader for a handle's segment, opening it on first use. */
|
|
20
|
+
private ringFor;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=frame-ring-reader-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-ring-reader-cache.d.ts","sourceRoot":"","sources":["../src/frame-ring-reader-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAiB/E;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,MAAM,CAAQ;gBAEV,MAAM,CAAC,EAAE,aAAa;IAIlC;;;;OAIG;IACH,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,GAAG,IAAI;IAyB9C,8CAA8C;IAC9C,KAAK,IAAI,IAAI;IAiBb,6EAA6E;IAC7E,OAAO,CAAC,OAAO;CAkChB"}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { FrameFormat, FrameHandle } from '@camstack/types';
|
|
2
|
+
/** Per-frame metadata published alongside the pixels of a slot. */
|
|
3
|
+
export interface FrameMeta {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
readonly format: FrameFormat;
|
|
7
|
+
readonly pts: number;
|
|
8
|
+
/** Valid pixel bytes — must be `<= slotByteLength`. */
|
|
9
|
+
readonly byteLength: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A scatter-write slot reservation returned by {@link FrameRingWriter.beginFrame}.
|
|
13
|
+
*
|
|
14
|
+
* ## Scatter-write seqlock contract (Phase 5 / D9 Task 7c — READ THIS)
|
|
15
|
+
*
|
|
16
|
+
* `beginFrame()` has already bumped this slot's `seq` to **odd** — a write is in
|
|
17
|
+
* progress, so any reader hitting this slot skips it (returns `null` / a failed
|
|
18
|
+
* `validate()`). The caller now owns an exclusive write window over `buffer`:
|
|
19
|
+
*
|
|
20
|
+
* 1. `buffer` is a writable `subarray` **directly over the slot's pixel
|
|
21
|
+
* region in the mapped segment** — there is no intermediate copy. A
|
|
22
|
+
* producer (e.g. the node-av scaler) fills it in place.
|
|
23
|
+
* 2. The slot stays odd for the **entire fill duration** — not just a memcpy.
|
|
24
|
+
* A scatter producer that takes longer to fill the slot (a whole scale
|
|
25
|
+
* pass) holds the slot odd for that whole pass; this is correct, readers
|
|
26
|
+
* simply latest-wins-drop that slot until {@link FrameRingWriter.commitFrame}.
|
|
27
|
+
* 3. The caller MUST call {@link FrameRingWriter.commitFrame} with this exact
|
|
28
|
+
* `slot` once the fill is done — that writes the metadata, bumps `seq` back
|
|
29
|
+
* to **even** (committed) and advances `writeIndex` so readers can see it.
|
|
30
|
+
*
|
|
31
|
+
* Exactly one `beginFrame` may be open at a time per writer (there is a single
|
|
32
|
+
* writer per ring). `buffer.byteLength` is the slot's full capacity
|
|
33
|
+
* (`slotByteLength`); the caller writes at most `commitFrame`'s `meta.byteLength`
|
|
34
|
+
* valid bytes into it.
|
|
35
|
+
*/
|
|
36
|
+
export interface FrameSlotWrite {
|
|
37
|
+
/** The ring slot index this write targets — pass it back to `commitFrame`. */
|
|
38
|
+
readonly slot: number;
|
|
39
|
+
/**
|
|
40
|
+
* A writable view **directly over the slot's pixel region** in the shared
|
|
41
|
+
* mapping. Fill it in place, then `commitFrame(slot, meta)`. Valid only
|
|
42
|
+
* between the `beginFrame` that produced it and its matching `commitFrame`.
|
|
43
|
+
*/
|
|
44
|
+
readonly buffer: Buffer;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The result of a successful seqlock read.
|
|
48
|
+
*
|
|
49
|
+
* ## Buffer-lifetime contract (Phase 5 / D9 Task 7b — READ THIS)
|
|
50
|
+
*
|
|
51
|
+
* `pixels` is a view onto the **reader's reusable scratch buffer**, NOT a
|
|
52
|
+
* freshly allocated copy. The seqlock-validated pixel bytes are memcpy'd into
|
|
53
|
+
* that scratch buffer (the copy is the seqlock safety copy — the bytes survive
|
|
54
|
+
* a post-read slot recycle), but the buffer itself is reused on this reader's
|
|
55
|
+
* **next** `readHandle` / `readLatest` call.
|
|
56
|
+
*
|
|
57
|
+
* Therefore `pixels` is **valid only until this same reader's next read**:
|
|
58
|
+
*
|
|
59
|
+
* - A consumer that processes the frame **synchronously**, fully, before it
|
|
60
|
+
* issues another read on this reader may use `pixels` directly (borrow).
|
|
61
|
+
* - A consumer that **retains** the frame past its next read — queues it for
|
|
62
|
+
* async work, stores it, hands it to another tick — MUST `Buffer.from(...)`
|
|
63
|
+
* it into its own storage at the point of retention. Holding the borrowed
|
|
64
|
+
* `pixels` past the next read is silent corruption: the next read overwrites
|
|
65
|
+
* the bytes in place.
|
|
66
|
+
*
|
|
67
|
+
* `meta` and `handle` are plain immutable values — safe to retain freely.
|
|
68
|
+
*/
|
|
69
|
+
export interface FrameRead {
|
|
70
|
+
/**
|
|
71
|
+
* The slot's pixel bytes copied into the reader's reusable scratch buffer.
|
|
72
|
+
*
|
|
73
|
+
* **Borrowed, not owned** — valid only until this reader's next
|
|
74
|
+
* `readHandle` / `readLatest`. Retain past that point ⇒ copy first
|
|
75
|
+
* (`Buffer.from(pixels)`). See the `FrameRead` doc comment.
|
|
76
|
+
*/
|
|
77
|
+
readonly pixels: Buffer;
|
|
78
|
+
readonly meta: FrameMeta;
|
|
79
|
+
/** A serialisable handle for this exact frame. */
|
|
80
|
+
readonly handle: FrameHandle;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A **zero-copy** read: `pixels` is a `subarray` directly over the shared
|
|
84
|
+
* mapping — no memcpy at all (Phase 5 / D9 Task 7b Step 3).
|
|
85
|
+
*
|
|
86
|
+
* ## Validation contract (READ THIS — concurrency-sensitive)
|
|
87
|
+
*
|
|
88
|
+
* Because `pixels` aliases the live ring slot, a concurrent writer can recycle
|
|
89
|
+
* that slot at any moment, tearing the bytes a consumer is reading. A
|
|
90
|
+
* `FrameView` is therefore an **optimistic** read: the seqlock's *opening*
|
|
91
|
+
* check already passed (the slot was committed and, for a handle read, the seq
|
|
92
|
+
* matched), but the closing re-check is deferred to the consumer.
|
|
93
|
+
*
|
|
94
|
+
* A consumer MUST:
|
|
95
|
+
* 1. read the slot **synchronously and fast** — no `await`, no yielding;
|
|
96
|
+
* 2. call `validate()` **immediately after** finishing with `pixels`;
|
|
97
|
+
* 3. if `validate()` returns `false`, **discard** whatever it computed from
|
|
98
|
+
* `pixels` — the bytes were (or may have been) overwritten mid-read, i.e.
|
|
99
|
+
* a torn frame, the latest-wins drop.
|
|
100
|
+
*
|
|
101
|
+
* `validate()` returning `true` means the slot's `seq` is unchanged since the
|
|
102
|
+
* read opened — the bytes the consumer just processed were a single committed
|
|
103
|
+
* frame, not torn. This is the same seqlock guarantee `readHandle` gives, but
|
|
104
|
+
* the closing half is the consumer's responsibility instead of being baked
|
|
105
|
+
* into a memcpy + recheck. Use this ONLY for tight synchronous consumers
|
|
106
|
+
* (e.g. motion's frame scan, the perf bench's compute stage); any consumer
|
|
107
|
+
* that retains or processes asynchronously MUST use the copying `readHandle` /
|
|
108
|
+
* `readLatest` instead.
|
|
109
|
+
*/
|
|
110
|
+
export interface FrameView {
|
|
111
|
+
/**
|
|
112
|
+
* A `subarray` view directly over the shared-memory slot — **zero-copy**.
|
|
113
|
+
* Valid for a synchronous read only; `validate()` confirms it was not torn.
|
|
114
|
+
*/
|
|
115
|
+
readonly pixels: Buffer;
|
|
116
|
+
readonly meta: FrameMeta;
|
|
117
|
+
readonly handle: FrameHandle;
|
|
118
|
+
/**
|
|
119
|
+
* Re-check the slot's seqlock. `true` ⇒ the slot was not recycled while the
|
|
120
|
+
* consumer read `pixels` — the frame is intact. `false` ⇒ a torn read,
|
|
121
|
+
* discard everything derived from `pixels`. Call once, right after the
|
|
122
|
+
* synchronous read of `pixels` completes.
|
|
123
|
+
*/
|
|
124
|
+
validate(): boolean;
|
|
125
|
+
}
|
|
126
|
+
/** Internal geometry of a sized segment. */
|
|
127
|
+
interface RingGeometry {
|
|
128
|
+
readonly slotCount: number;
|
|
129
|
+
readonly slotByteLength: number;
|
|
130
|
+
/** Byte length of the `Int32Array` header. */
|
|
131
|
+
readonly headerBytes: number;
|
|
132
|
+
/** Byte offset of the metadata region. */
|
|
133
|
+
readonly metaOffset: number;
|
|
134
|
+
/** Byte offset of the pixel region. */
|
|
135
|
+
readonly pixelsOffset: number;
|
|
136
|
+
/** Total segment byte length. */
|
|
137
|
+
readonly totalBytes: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Total byte length a segment must have to hold a `slotCount`-slot ring with
|
|
141
|
+
* `slotByteLength`-byte pixel slots. The decoder uses this to size the
|
|
142
|
+
* shared-memory segment before constructing a `FrameRingWriter` over it.
|
|
143
|
+
*/
|
|
144
|
+
export declare function computeSegmentSize(slotCount: number, slotByteLength: number): number;
|
|
145
|
+
/**
|
|
146
|
+
* Slot-count bounds for a per-resolution ring. {@link deriveSlotCount} clamps
|
|
147
|
+
* the budget-derived count into `[MIN_RING_SLOTS, MAX_RING_SLOTS]`:
|
|
148
|
+
* - `MIN` keeps a tiny floor even when one frame nearly exhausts the budget
|
|
149
|
+
* (a 4K RGB frame is ~24.9 MB — a 128 MB budget yields only ~5 raw slots);
|
|
150
|
+
* - `MAX` caps the ring for small frames so a 360p stream does not allocate
|
|
151
|
+
* hundreds of slots' worth of shared memory it will never use latest-wins.
|
|
152
|
+
*/
|
|
153
|
+
export declare const MIN_RING_SLOTS = 6;
|
|
154
|
+
export declare const MAX_RING_SLOTS = 64;
|
|
155
|
+
/**
|
|
156
|
+
* Slot count for a ring, derived from a per-ring shared-memory budget.
|
|
157
|
+
*
|
|
158
|
+
* `budgetBytes` is the shared memory a single stream's ring may consume; the
|
|
159
|
+
* raw count is `floor(budgetBytes / slotByteLength)`, clamped into
|
|
160
|
+
* `[MIN_RING_SLOTS, MAX_RING_SLOTS]`. Live video is latest-wins, so the slot
|
|
161
|
+
* count only needs to absorb the writer-commit / reader-read window — the
|
|
162
|
+
* budget trades shared-memory footprint against that window per resolution.
|
|
163
|
+
*/
|
|
164
|
+
export declare function deriveSlotCount(budgetBytes: number, slotByteLength: number): number;
|
|
165
|
+
/**
|
|
166
|
+
* Bytes per pixel for each packed decode format a frame ring slot can hold.
|
|
167
|
+
*
|
|
168
|
+
* This is the single source of truth shared by the decoder write side
|
|
169
|
+
* (`DecoderFrameRingSink`) and every consumer read side
|
|
170
|
+
* (`FrameRingReaderCache`): if the two computed different values, a reader
|
|
171
|
+
* would mis-map the shared-memory segment and read corrupt pixels.
|
|
172
|
+
*
|
|
173
|
+
* `jpeg` has no fixed bytes-per-pixel — `computeSlotByteLength` short-circuits
|
|
174
|
+
* it to the equivalent RGB24 raster before this is ever consulted, so the `3`
|
|
175
|
+
* here is only a safe placeholder for exhaustiveness over `FrameFormat`.
|
|
176
|
+
*/
|
|
177
|
+
export declare function bytesPerPixel(format: FrameFormat): number;
|
|
178
|
+
/**
|
|
179
|
+
* Per-slot byte capacity for a frame of the given geometry — the single source
|
|
180
|
+
* of truth for both the decoder write side and the consumer read side.
|
|
181
|
+
*
|
|
182
|
+
* For raw formats this is exactly `width × height × bpp`. For `jpeg` (variable
|
|
183
|
+
* length) the slot is sized to the equivalent RGB24 raster — a JPEG is always
|
|
184
|
+
* far smaller than its source raster, so an RGB24-sized slot is a safe upper
|
|
185
|
+
* bound. Note: this upper bound holds only while the JPEG encoder is pinned to
|
|
186
|
+
* its current quality (~80). A near-lossless quality setting can produce a
|
|
187
|
+
* JPEG that exceeds the RGB24 byte count; if quality is raised significantly,
|
|
188
|
+
* this slot size must be revisited.
|
|
189
|
+
*/
|
|
190
|
+
export declare function computeSlotByteLength(width: number, height: number, format: FrameFormat): number;
|
|
191
|
+
/**
|
|
192
|
+
* Shared geometry + typed views over a segment buffer. The header is an
|
|
193
|
+
* `Int32Array` so `Atomics` ops apply directly; metadata and pixels are read
|
|
194
|
+
* and written through `DataView` / `Buffer` slices.
|
|
195
|
+
*/
|
|
196
|
+
declare abstract class FrameRingBase {
|
|
197
|
+
protected readonly segment: Buffer;
|
|
198
|
+
protected readonly shmId: string;
|
|
199
|
+
protected readonly geometry: RingGeometry;
|
|
200
|
+
/** `Int32Array` over the header — slotCount, slotByteLength, writeIndex, seq[]. */
|
|
201
|
+
protected readonly header: Int32Array;
|
|
202
|
+
protected readonly view: DataView;
|
|
203
|
+
protected constructor(segment: Buffer, shmId: string, slotCount: number, slotByteLength: number);
|
|
204
|
+
/** Header index of `seq[slot]`. */
|
|
205
|
+
protected seqIndex(slot: number): number;
|
|
206
|
+
/** Byte offset of slot `slot`'s metadata struct. */
|
|
207
|
+
protected metaOffsetOf(slot: number): number;
|
|
208
|
+
/** Byte offset of slot `slot`'s pixel region. */
|
|
209
|
+
protected pixelOffsetOf(slot: number): number;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The single writer for a ring. Constructed by whoever owns the segment (the
|
|
213
|
+
* decoder). On construction it stamps `slotCount` / `slotByteLength` into the
|
|
214
|
+
* header so a reader opening the same segment can self-describe.
|
|
215
|
+
*/
|
|
216
|
+
export declare class FrameRingWriter extends FrameRingBase {
|
|
217
|
+
/** Slot index of an in-flight `beginWrite()`, or `null` when idle. */
|
|
218
|
+
private pendingSlot;
|
|
219
|
+
/** Cluster node id stamped into every `FrameHandle` this writer produces. */
|
|
220
|
+
private readonly nodeId;
|
|
221
|
+
constructor(segment: Buffer, shmId: string, slotCount: number, slotByteLength: number, nodeId: string);
|
|
222
|
+
/** The number of slots in this ring — derived per-resolution from the budget. */
|
|
223
|
+
get slotCount(): number;
|
|
224
|
+
/** The slot the next `writeFrame` / `beginFrame` will target. */
|
|
225
|
+
private targetSlot;
|
|
226
|
+
/**
|
|
227
|
+
* Open the seqlock on the next slot and hand the caller a writable view
|
|
228
|
+
* directly over that slot's pixel region — the **scatter-write** entry point.
|
|
229
|
+
*
|
|
230
|
+
* The seqlock contract (see {@link FrameSlotWrite}): this bumps the slot's
|
|
231
|
+
* `seq` to **odd** (write in progress), so a concurrent reader skips the slot
|
|
232
|
+
* for the entire fill window. The caller fills `buffer` in place — there is
|
|
233
|
+
* NO intermediate copy: a producer (the node-av scaler) writes its packed
|
|
234
|
+
* output straight into the mapped segment — then MUST call
|
|
235
|
+
* {@link commitFrame} with the returned `slot` to publish the frame.
|
|
236
|
+
*
|
|
237
|
+
* Exactly one `beginFrame` may be open per writer at a time.
|
|
238
|
+
*/
|
|
239
|
+
beginFrame(): FrameSlotWrite;
|
|
240
|
+
/**
|
|
241
|
+
* Close the seqlock opened by {@link beginFrame}: write the metadata, bump
|
|
242
|
+
* `seq` back to **even** (committed) and advance `writeIndex` so readers see
|
|
243
|
+
* this slot as the latest. Returns the published frame's `FrameHandle`.
|
|
244
|
+
*
|
|
245
|
+
* `slot` must be the value from the matching `beginFrame`'s
|
|
246
|
+
* {@link FrameSlotWrite}. The pixels are assumed already filled in place by
|
|
247
|
+
* the caller (the scatter-write contract) — `commitFrame` copies nothing.
|
|
248
|
+
*/
|
|
249
|
+
commitFrame(slot: number, meta: FrameMeta): FrameHandle;
|
|
250
|
+
/**
|
|
251
|
+
* Abandon the seqlock opened by {@link beginFrame} **without publishing the
|
|
252
|
+
* slot** — the degenerate-path counterpart of {@link commitFrame}.
|
|
253
|
+
*
|
|
254
|
+
* `beginFrame` has bumped the slot's `seq` to **odd**. A caller that opened a
|
|
255
|
+
* slot but then could not fill it with valid pixels (the producer failed, or
|
|
256
|
+
* there were no source planes) MUST NOT `commitFrame` — that would close the
|
|
257
|
+
* seqlock over uninitialised / stale shared-memory bytes and advance
|
|
258
|
+
* `writeIndex`, so a reader would see those garbage bytes as a real, latest
|
|
259
|
+
* frame. `abortFrame` instead:
|
|
260
|
+
*
|
|
261
|
+
* 1. bumps `seq` from odd back to **even** — the slot is not left stuck
|
|
262
|
+
* odd, so the writer can reuse it on a later cycle;
|
|
263
|
+
* 2. does **NOT** advance `writeIndex` — `readLatest` therefore never
|
|
264
|
+
* surfaces this slot as the latest (the prior latest stays latest);
|
|
265
|
+
* 3. returns no `FrameHandle` — nothing is handed downstream.
|
|
266
|
+
*
|
|
267
|
+
* The slot's pixel bytes after `abortFrame` are **undefined** (whatever was
|
|
268
|
+
* there before, or a half-written producer output) — but that is harmless
|
|
269
|
+
* because no consumer ever sees the slot as committed: `readLatest` skips it
|
|
270
|
+
* (writeIndex did not move) and any handle that happened to point at this
|
|
271
|
+
* slot from an earlier commit sees the changed `seq` and is correctly
|
|
272
|
+
* rejected by `readHandle`'s seq check.
|
|
273
|
+
*
|
|
274
|
+
* `slot` must be the value from the matching `beginFrame` — validated
|
|
275
|
+
* exactly as {@link commitFrame} does.
|
|
276
|
+
*/
|
|
277
|
+
abortFrame(slot: number): void;
|
|
278
|
+
/**
|
|
279
|
+
* Publish one frame from a caller-provided pixel `Buffer` — the convenience
|
|
280
|
+
* wrapper over the scatter-write {@link beginFrame} / {@link commitFrame}
|
|
281
|
+
* pair: it opens the slot, copies `pixels` into it, then commits.
|
|
282
|
+
*
|
|
283
|
+
* Prefer the `beginFrame` / `commitFrame` pair when the pixels can be
|
|
284
|
+
* produced directly into the slot (the node-av scaler scattering its packed
|
|
285
|
+
* output into the mapped segment) — that path has zero write-side copy.
|
|
286
|
+
* `writeFrame` keeps the single-call form for callers that already hold a
|
|
287
|
+
* detached pixel `Buffer`.
|
|
288
|
+
*/
|
|
289
|
+
writeFrame(pixels: Buffer, meta: FrameMeta): FrameHandle;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* A reader for a ring. Many readers may share a segment concurrently with the
|
|
293
|
+
* single writer. Every read is a seqlock read — a torn or recycled slot is
|
|
294
|
+
* dropped (returns `null`), never returned as garbage pixels.
|
|
295
|
+
*/
|
|
296
|
+
export declare class FrameRingReader extends FrameRingBase {
|
|
297
|
+
/**
|
|
298
|
+
* The reusable per-reader scratch buffer the seqlock-validated pixels are
|
|
299
|
+
* copied into — see the `FrameRead` doc comment for the borrow contract.
|
|
300
|
+
*
|
|
301
|
+
* Sized to `slotByteLength` on construction (the segment's stamped slot
|
|
302
|
+
* capacity) and lazily grown if a slot ever reports a larger `byteLength`.
|
|
303
|
+
* Reusing one buffer per reader replaces the per-read `Buffer.allocUnsafe` —
|
|
304
|
+
* the D9 perf gate's regression (Task 7b): a fresh ~5.93 MB allocation per
|
|
305
|
+
* 1080p frame churned ~1.39 GB/s and stalled on GC. The single memcpy stays
|
|
306
|
+
* (it IS the seqlock safety copy); only the allocation is removed.
|
|
307
|
+
*/
|
|
308
|
+
private scratch;
|
|
309
|
+
/**
|
|
310
|
+
* Cluster node id of the node that OWNS this segment (i.e. the writer's
|
|
311
|
+
* node — the node whose shared memory physically holds the ring), stamped
|
|
312
|
+
* into every `FrameHandle` this reader produces. `FrameHandle.nodeId` always
|
|
313
|
+
* identifies the segment-owning node, never the reading node, so callers
|
|
314
|
+
* pass the writer's node id here (e.g. `FrameRingReaderCache` passes
|
|
315
|
+
* `handle.nodeId` straight through).
|
|
316
|
+
*/
|
|
317
|
+
private readonly nodeId;
|
|
318
|
+
constructor(segment: Buffer, shmId: string, slotCount: number, slotByteLength: number,
|
|
319
|
+
/**
|
|
320
|
+
* Node id of the segment-owning (writer's) node — see the `nodeId` field
|
|
321
|
+
* doc. NOT this reader's own node.
|
|
322
|
+
*/
|
|
323
|
+
nodeId: string);
|
|
324
|
+
/**
|
|
325
|
+
* Return the reader's scratch buffer as a view of exactly `byteLength` bytes,
|
|
326
|
+
* growing the backing buffer first if a slot ever exceeds the stamped slot
|
|
327
|
+
* capacity (defensive — `slotByteLength` should always bound `byteLength`).
|
|
328
|
+
*/
|
|
329
|
+
private scratchFor;
|
|
330
|
+
/**
|
|
331
|
+
* Read the latest committed frame, latest-wins. Returns `null` when the ring
|
|
332
|
+
* is empty (no frame ever published) or the latest slot is mid-write /
|
|
333
|
+
* recycled (a slow reader simply drops that frame and retries next tick).
|
|
334
|
+
*
|
|
335
|
+
* The returned `FrameRead.pixels` is **borrowed** from this reader's reusable
|
|
336
|
+
* scratch buffer — valid only until this reader's next read. A consumer that
|
|
337
|
+
* retains it past the next read MUST copy. See the `FrameRead` doc comment.
|
|
338
|
+
*/
|
|
339
|
+
readLatest(): FrameRead | null;
|
|
340
|
+
/**
|
|
341
|
+
* Read the exact frame a `FrameHandle` refers to. Returns `null` when the
|
|
342
|
+
* slot has been recycled (its committed `seq` no longer matches `handle.seq`)
|
|
343
|
+
* or is mid-write — i.e. the frame is gone, not torn.
|
|
344
|
+
*
|
|
345
|
+
* The returned `FrameRead.pixels` is **borrowed** from this reader's reusable
|
|
346
|
+
* scratch buffer — valid only until this reader's next read. A consumer that
|
|
347
|
+
* retains it past the next read MUST copy. See the `FrameRead` doc comment.
|
|
348
|
+
*/
|
|
349
|
+
readHandle(handle: FrameHandle): FrameRead | null;
|
|
350
|
+
/**
|
|
351
|
+
* The seqlock read of a single slot.
|
|
352
|
+
*
|
|
353
|
+
* `expectedSeq` is the committed seq a `readHandle` caller demands; pass
|
|
354
|
+
* `null` for `readLatest`, which accepts whatever even seq it finds.
|
|
355
|
+
*/
|
|
356
|
+
private seqlockRead;
|
|
357
|
+
/**
|
|
358
|
+
* Read the latest committed frame **zero-copy** — `FrameView.pixels` is a
|
|
359
|
+
* `subarray` over the shared mapping, no memcpy. The caller MUST process it
|
|
360
|
+
* synchronously and then call `validate()`; see the `FrameView` contract.
|
|
361
|
+
* Returns `null` when the ring is empty or the latest slot is mid-write.
|
|
362
|
+
*/
|
|
363
|
+
readLatestView(): FrameView | null;
|
|
364
|
+
/**
|
|
365
|
+
* Read the frame a `FrameHandle` refers to **zero-copy** — `FrameView.pixels`
|
|
366
|
+
* is a `subarray` over the shared mapping, no memcpy. The caller MUST process
|
|
367
|
+
* it synchronously and then call `validate()`; see the `FrameView` contract.
|
|
368
|
+
* Returns `null` when the slot is recycled (seq mismatch) or mid-write.
|
|
369
|
+
*/
|
|
370
|
+
readHandleView(handle: FrameHandle): FrameView | null;
|
|
371
|
+
/**
|
|
372
|
+
* The zero-copy half of the seqlock read: validate the opening seqlock, then
|
|
373
|
+
* return a `subarray` over the slot plus a `validate()` closure that re-reads
|
|
374
|
+
* the seqlock. No pixel copy — the consumer owns the closing recheck.
|
|
375
|
+
*/
|
|
376
|
+
private seqlockReadView;
|
|
377
|
+
}
|
|
378
|
+
export {};
|
|
379
|
+
//# sourceMappingURL=frame-ring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-ring.d.ts","sourceRoot":"","sources":["../src/frame-ring.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@camstack/shm-ring` — cross-platform shared-memory frame plane.
|
|
3
|
+
*
|
|
4
|
+
* Public surface:
|
|
5
|
+
* - the native segment wrapper (`createSegment` / `openSegment` / `unlinkSegment`);
|
|
6
|
+
* - `FrameRing` (the seqlock ring) — added in Task 2.
|
|
7
|
+
*/
|
|
8
|
+
export { createSegment, openSegment, unlinkSegment, } from './native.js';
|
|
9
|
+
export type { ShmSegment, NativeSegmentHandle } from './native.js';
|
|
10
|
+
export { FrameRingWriter, FrameRingReader, computeSegmentSize, bytesPerPixel, computeSlotByteLength, deriveSlotCount, MIN_RING_SLOTS, MAX_RING_SLOTS, } from './frame-ring.js';
|
|
11
|
+
export type { FrameMeta, FrameRead, FrameView, FrameSlotWrite } from './frame-ring.js';
|
|
12
|
+
export { FrameRingReaderCache } from './frame-ring-reader-cache.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAClE,OAAO,EACL,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,qBAAqB,EACrB,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA"}
|