Loading...
Searching...
No Matches
CaptureSyncGroup.hpp
Go to the documentation of this file.
1#pragma once
2
41#include <atomic>
42#include <cstddef>
43#include <cstdint>
44
45namespace score::gfx::interop
46{
47
50{
51 static constexpr std::size_t kMaxMembers = 8;
52
53 std::uint64_t generation{0};
54 std::size_t memberCount{0};
56 int slot[kMaxMembers]{};
60 std::uint64_t stampNs[kMaxMembers]{};
61
62 bool complete() const noexcept
63 {
64 for(std::size_t i = 0; i < memberCount; ++i)
65 if(slot[i] < 0)
66 return false;
67 return memberCount > 0;
68 }
69
72 std::uint64_t skewNs() const noexcept
73 {
74 std::uint64_t lo = ~std::uint64_t(0), hi = 0;
75 for(std::size_t i = 0; i < memberCount; ++i)
76 {
77 const auto s = stampNs[i];
78 if(s == 0)
79 continue;
80 if(s < lo)
81 lo = s;
82 if(s > hi)
83 hi = s;
84 }
85 return hi >= lo ? hi - lo : 0;
86 }
87};
88
94{
95 std::atomic<std::uint64_t> seq{0};
96 CaptureFrameSet set{};
97};
98
100{
101public:
111 static constexpr std::size_t kDepth = 32;
112
113 explicit CaptureSyncGroup(std::size_t memberCount) noexcept
114 : m_members{
115 memberCount < CaptureFrameSet::kMaxMembers
116 ? memberCount
117 : CaptureFrameSet::kMaxMembers}
118 {
119 }
120
121 std::size_t memberCount() const noexcept { return m_members; }
122
125 void publish(const int* slots, const std::uint64_t* stampNs) noexcept
126 {
127 const auto gen = m_generation.load(std::memory_order_relaxed) + 1;
128 auto& e = m_ring[gen % kDepth];
129
130 // Retire the version before touching the payload: a reader that catches the
131 // entry mid-write then sees a version it cannot match and retries, instead
132 // of assembling one member from this capture and another from the last.
133 e.seq.store(0, std::memory_order_relaxed);
134 std::atomic_thread_fence(std::memory_order_release);
135
136 e.set.generation = gen;
137 e.set.memberCount = m_members;
138 for(std::size_t i = 0; i < m_members; ++i)
139 {
140 // takeReturned() hands slots back in a 32-bit mask, so a slot it cannot
141 // name is a slot the producer would never get back. Drop the capture
142 // rather than lend a buffer that can only leak.
143 e.set.slot[i] = slots[i] < int(kMaxReturnableSlot) ? slots[i] : -1;
144 e.set.stampNs[i] = stampNs ? stampNs[i] : 0;
145 }
146
147 e.seq.store(gen, std::memory_order_release);
148
149 const bool complete = e.set.complete();
150 if(complete)
151 {
152 const auto sk = e.set.skewNs();
153 if(sk > m_maxSkewNs.load(std::memory_order_relaxed))
154 m_maxSkewNs.store(sk, std::memory_order_relaxed);
155 }
156 else
157 {
158 m_incomplete.fetch_add(1, std::memory_order_relaxed);
159 }
160
161 // m_generation first: take() derives the lap distance as
162 // m_generation - m_newestComplete, so publishing the complete marker ahead
163 // of the generation lets that subtraction wrap and condemn a fresh set.
164 m_generation.store(gen, std::memory_order_release);
165 if(complete)
166 m_newestComplete.store(gen, std::memory_order_release);
167 }
168
169 struct Latched
170 {
171 int slot{-1};
172 std::uint64_t generation{0};
173 std::uint64_t stampNs{0};
178 bool fresh{false};
179 };
180
183 Latched take(std::size_t member, std::int64_t passId) noexcept
184 {
185 if(member >= m_members)
186 return {};
187
188 if(passId != m_pinnedPass)
189 {
190 m_pinnedPass = passId;
191 const auto gen = m_newestComplete.load(std::memory_order_acquire);
192 // A set older than the ring depth has been overwritten by the producer
193 // while the render thread was behind; there is nothing coherent left to
194 // bind, so hold rather than read a torn set.
195 const auto newest = m_generation.load(std::memory_order_acquire);
196 if(gen == 0 || newest - gen >= kDepth)
197 {
198 if(gen != 0)
199 m_lapped.fetch_add(1, std::memory_order_relaxed);
200 m_pinnedGen = 0;
201 }
202 else
203 {
204 m_pinnedGen = gen;
205 }
206 // Copy the whole capture out of the ring here, once. Re-reading it per
207 // member would let a lap between two members answer one from the new
208 // capture and the other from the old -- the split this class exists to
209 // make unrepresentable.
210 if(m_pinnedGen != 0 && !snapshotPinned())
211 {
212 m_lapped.fetch_add(1, std::memory_order_relaxed);
213 m_pinnedGen = 0;
214 }
215
216 m_pinnedIsNew = m_pinnedGen != 0 && m_pinnedGen != m_lastHandedOut;
217 if(m_pinnedIsNew)
218 {
219 releaseSkipped();
220 queueRetire();
221 m_lastHandedOut = m_pinnedGen;
222 for(std::size_t i = 0; i < m_members; ++i)
223 m_handedOutSlots[i] = m_pinnedSet.slot[i];
224 }
225 // Age the queue on every pass. Tying this to a fresh capture deadlocks:
226 // ageing needs a new capture, a new capture needs a free slot, and slots
227 // only come free by ageing. A producer whose ring is no deeper than the
228 // retire depth never escapes that loop.
229 ageRetired();
230 }
231
232 if(m_pinnedGen == 0)
233 return {};
234
235 Latched out;
236 out.slot = m_pinnedSet.slot[member];
237 out.generation = m_pinnedSet.generation;
238 out.stampNs = m_pinnedSet.stampNs[member];
239 out.fresh = m_pinnedIsNew;
240 return out;
241 }
242
245 void setRetireDepth(std::size_t d) noexcept { m_retireDepth = d ? d : 1; }
246
254 std::uint32_t takeReturned(std::size_t member) noexcept
255 {
256 if(member >= m_members)
257 return 0;
258 return m_returns[member].exchange(0, std::memory_order_acquire);
259 }
260
262 std::uint64_t incompleteCount() const noexcept
263 {
264 return m_incomplete.load(std::memory_order_relaxed);
265 }
269 std::uint64_t lappedCount() const noexcept
270 {
271 return m_lapped.load(std::memory_order_relaxed);
272 }
275 std::uint64_t maxSkewNs() const noexcept
276 {
277 return m_maxSkewNs.load(std::memory_order_relaxed);
278 }
283 std::uint64_t strandedCount() const noexcept
284 {
285 return m_stranded.load(std::memory_order_relaxed);
286 }
287
288private:
291 static constexpr std::size_t kMaxReturnableSlot = 32;
292
293 static constexpr std::size_t kRetireMax = 8;
294 struct Retired
295 {
296 std::uint64_t gen{};
297 std::uint64_t at{};
298 int slot[CaptureFrameSet::kMaxMembers]{};
299 };
300
303 bool snapshotPinned() noexcept
304 {
305 const auto& e = m_ring[m_pinnedGen % kDepth];
306 if(e.seq.load(std::memory_order_acquire) != m_pinnedGen)
307 return false;
308 m_pinnedSet = e.set;
309 std::atomic_thread_fence(std::memory_order_acquire);
310 return e.seq.load(std::memory_order_acquire) == m_pinnedGen;
311 }
312
321 void releaseSkipped() noexcept
322 {
323 for(std::uint64_t gen = m_lastHandedOut + 1; gen < m_pinnedGen; ++gen)
324 {
325 // The producer has overwritten anything older than the ring; kDepth is
326 // sized so that cannot happen to a capture whose slots are still out, but
327 // count it rather than release slots read out of an entry that now
328 // describes a different capture.
329 if(m_pinnedGen - gen >= kDepth)
330 {
331 m_stranded.fetch_add(1, std::memory_order_relaxed);
332 continue;
333 }
334
335 const auto& e = m_ring[gen % kDepth];
336 if(e.seq.load(std::memory_order_acquire) != gen)
337 {
338 m_stranded.fetch_add(1, std::memory_order_relaxed);
339 continue;
340 }
341 const CaptureFrameSet s = e.set;
342 std::atomic_thread_fence(std::memory_order_acquire);
343 if(e.seq.load(std::memory_order_acquire) != gen)
344 {
345 m_stranded.fetch_add(1, std::memory_order_relaxed);
346 continue;
347 }
348
349 for(std::size_t m = 0; m < m_members; ++m)
350 {
351 const int slot = s.slot[m];
352 if(slot < 0 || slot >= int(kMaxReturnableSlot))
353 continue;
354 // The capture being bound may name the same slot -- a producer that
355 // recycles indices rather than lending the device's own buffers does
356 // exactly that -- and releasing it would hand back the frame about to
357 // be drawn.
358 if(slot == m_pinnedSet.slot[m])
359 continue;
360 m_returns[m].fetch_or(1u << unsigned(slot), std::memory_order_release);
361 }
362 }
363 }
364
369 void queueRetire() noexcept
370 {
371 if(m_lastHandedOut == 0)
372 return;
373 if(m_retireN == kRetireMax)
374 {
375 // Full: release the oldest rather than drop it. Dropping strands its
376 // slots with the renderer forever; the oldest is also the one most
377 // likely to be past the GPU already.
378 releaseSlotsOf(m_retire[0]);
379 for(std::size_t i = 1; i < m_retireN; ++i)
380 m_retire[i - 1] = m_retire[i];
381 --m_retireN;
382 }
383 auto& r = m_retire[m_retireN++];
384 r.gen = m_lastHandedOut;
385 r.at = m_acquisitions + 1;
386 for(std::size_t m = 0; m < m_members; ++m)
387 r.slot[m] = m_handedOutSlots[m];
388 }
389
390 void ageRetired() noexcept
391 {
392 ++m_acquisitions;
393 std::size_t keep = 0;
394 for(std::size_t i = 0; i < m_retireN; ++i)
395 {
396 if(m_acquisitions - m_retire[i].at < m_retireDepth)
397 {
398 m_retire[keep++] = m_retire[i];
399 continue;
400 }
401 releaseSlotsOf(m_retire[i]);
402 }
403 m_retireN = keep;
404 }
405
406 void releaseSlotsOf(const Retired& r) noexcept
407 {
408 // The slots come from the record, not from the ring: a lapped entry no
409 // longer describes the capture we handed out, and its slots are still on
410 // loan to the renderer until we return them here.
411 for(std::size_t m = 0; m < m_members; ++m)
412 if(r.slot[m] >= 0 && r.slot[m] < int(kMaxReturnableSlot))
413 m_returns[m].fetch_or(1u << unsigned(r.slot[m]), std::memory_order_release);
414 }
415
416
417 const std::size_t m_members;
418
419 CaptureRingEntry m_ring[kDepth]{};
420 std::atomic<std::uint64_t> m_generation{0};
421 std::atomic<std::uint64_t> m_newestComplete{0};
422
423 std::atomic<std::uint64_t> m_incomplete{0};
424 std::atomic<std::uint64_t> m_lapped{0};
425 std::atomic<std::uint64_t> m_stranded{0};
426 std::atomic<std::uint64_t> m_maxSkewNs{0};
427 std::atomic<std::uint32_t> m_returns[CaptureFrameSet::kMaxMembers]{};
428
429 std::size_t m_retireDepth{1};
430 std::uint64_t m_acquisitions{0};
431 Retired m_retire[kRetireMax]{};
432 std::size_t m_retireN{0};
433
434 // Render-thread only.
435 std::int64_t m_pinnedPass{-1};
436 std::uint64_t m_pinnedGen{0};
437 std::uint64_t m_lastHandedOut{0};
438 bool m_pinnedIsNew{false};
439 CaptureFrameSet m_pinnedSet{};
440 int m_handedOutSlots[CaptureFrameSet::kMaxMembers]{};
441};
442
443}
Definition CaptureSyncGroup.hpp:100
std::uint64_t incompleteCount() const noexcept
Captures dropped because at least one member had no frame.
Definition CaptureSyncGroup.hpp:262
Latched take(std::size_t member, std::int64_t passId) noexcept
Definition CaptureSyncGroup.hpp:183
std::uint32_t takeReturned(std::size_t member) noexcept
Definition CaptureSyncGroup.hpp:254
std::uint64_t lappedCount() const noexcept
Definition CaptureSyncGroup.hpp:269
std::uint64_t maxSkewNs() const noexcept
Definition CaptureSyncGroup.hpp:275
void publish(const int *slots, const std::uint64_t *stampNs) noexcept
Definition CaptureSyncGroup.hpp:125
std::uint64_t strandedCount() const noexcept
Definition CaptureSyncGroup.hpp:283
static constexpr std::size_t kDepth
Definition CaptureSyncGroup.hpp:111
void setRetireDepth(std::size_t d) noexcept
Definition CaptureSyncGroup.hpp:245
STL namespace.
One capture: the slot each member filled, and when the frame was exposed.
Definition CaptureSyncGroup.hpp:50
std::uint64_t skewNs() const noexcept
Definition CaptureSyncGroup.hpp:72
int slot[kMaxMembers]
Slot index per member; < 0 means that member had no frame for this capture.
Definition CaptureSyncGroup.hpp:56
std::uint64_t stampNs[kMaxMembers]
Definition CaptureSyncGroup.hpp:60
Definition CaptureSyncGroup.hpp:94
Definition CaptureSyncGroup.hpp:170
bool fresh
Definition CaptureSyncGroup.hpp:178