1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//! The FIFO ticket lock [`backend::GpuBackend`](super::backend::GpuBackend) drives for
//! chunk-level fairness among concurrent GPU requests -- see the parent module's doc
//! comment's "Concurrency: chunk-level fairness" section for the correctness argument
//! this exists to support.
/// How many chunks one [`super::backend::GpuBackend::try_accumulate_cancellable`] "turn"
/// dispatches before releasing the renderer and rejoining the back of the [`Turnstile`]
/// queue.
///
/// `1` would lose the chunk pipeline's overlap entirely: `accumulate_turn`'s dispatch
/// loop only overlaps chunk i's GPU work with chunk i-1's readback (see
/// `renderer::gpu::frame`'s "Overlapped chunk pipeline" doc section), and a turn always
/// drains whatever it dispatches before returning (see the parent module's doc comment) --
/// with a one-chunk turn, that "previous" chunk is always ITS OWN, so the CPU would
/// submit and then immediately block on the very dispatch it just queued, exactly the
/// un-pipelined behaviour the double-buffering exists to avoid. `2` keeps that one
/// window of overlap (dispatch chunk 0, dispatch chunk 1 while draining chunk 0, drain
/// chunk 1) inside every turn, while still yielding often enough that a large request
/// can't monopolise the GPU for long -- at `super::gpu::frame::TARGET_CHUNK_MS`'s
/// default, at most ~300ms of wall-clock GPU time per turn once the chunk-timing EMA has
/// converged, far less on the first (uncalibrated) turn of a session. See "Scene
/// re-upload: the fairness cost" above for what a larger value would trade against:
/// fewer re-uploads, coarser fairness.
pub const CHUNKS_PER_TURN: usize = 2;
/// A FIFO ticket lock.
///
/// [`Self::take_ticket`] hands out ticket numbers in call order; [`Self::wait_for_turn`]
/// blocks the calling thread until its ticket is the one being served, returning an RAII
/// [`TurnstileTurn`] that advances to the next ticket (waking every other waiter) on
/// drop. Unlike a plain `Mutex` -- which makes no ordering promise among blocked waiters,
/// so the OS scheduler can and does let one thread relock it repeatedly ahead of others
/// already queued -- this guarantees admissions happen in the exact order threads asked
/// for one. That is what lets [`super::backend::GpuBackend::try_accumulate_cancellable`]'s
/// "take a turn, dispatch a few chunks, rejoin the back of the queue" loop alternate
/// FAIRLY among several concurrent requests rather than one starving the rest. Std-only:
/// an `AtomicU64` ticket counter plus a `Mutex`+`Condvar` "now serving" pair, the
/// standard ticket-lock construction -- no new dependency.
pub
/// RAII: holding this value IS holding the [`Turnstile`]'s current turn. `notify_all`
/// (not `notify_one`) on drop -- of the threads waiting on tickets other than the very
/// next one, none can proceed yet, so waking all of them just to have them re-check and
/// go back to sleep is the standard, simple ticket-lock shape; with turns held only for a
/// bounded [`CHUNKS_PER_TURN`]-chunk slice, that wasted wakeup is not worth optimising
/// away with a per-ticket condvar.
pub