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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! Pipelined frame scheduling: in-flight timeline ring, depth cap, present stamp.
//!
//! [`FrameOrchestrator`] is a **client pacing** helper only. It does not own GPU
//! bytes or run cleanup callbacks — recycle lives in the transient pool /
//! [`crate::RetainedPool`]. Use it to bound how far the CPU runs ahead of the GPU
//! and to track open-frame / present-timeline bookkeeping.
//!
//! When cross-frame ordering is enforced elsewhere (scheme submit sidecars,
//! present easement), close with [`FrameOrchestrator::end_frame_externally_ordered`]
//! so the ring stays empty and [`FrameOrchestrator::begin_frame`] does not wait.
use crate::context::Context;
use crate::error::GoldyError;
use crate::scheme::Submission;
use crate::timeline::TimelineValue;
use crate::tracy_frame_mark;
use crate::tracy_zone;
use anyhow::anyhow;
use std::collections::VecDeque;
/// Token returned from [`FrameOrchestrator::begin_frame`]; must be passed to
/// [`FrameOrchestrator::end_frame_standalone`], [`FrameOrchestrator::end_frame_for_present`],
/// [`FrameOrchestrator::end_frame_externally_ordered`], or [`FrameOrchestrator::abort_frame`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FrameHandle(pub(crate) u64);
struct FrameSlot {
timeline: Option<TimelineValue>,
}
/// Owns an in-flight ring of frame timelines and enforces a maximum pipelining depth.
///
/// Typical use:
/// 1. [`Self::begin_frame`]
/// 2. Record and submit work via [`crate::Scheme`]
/// 3. [`Self::end_frame_standalone`], [`Self::end_frame_for_present`], or
/// [`Self::end_frame_externally_ordered`]
/// 4. For swapchain frames on the ring path, [`Self::note_presented`] after present.
pub struct FrameOrchestrator {
context: Context,
max_depth: usize,
ring: VecDeque<FrameSlot>,
/// Monotonic id for the next [`FrameHandle`].
next_id: u64,
/// `Some` between [`Self::begin_frame`] and a matching end-frame call.
open: Option<FrameHandle>,
}
impl FrameOrchestrator {
/// Create an orchestrator. `max_depth` bounds how many frames may be in flight before the
/// next [`Self::begin_frame`] blocks on the oldest slot.
pub fn new(context: &Context, max_depth: usize) -> Self {
Self {
context: context.clone(),
max_depth: max_depth.max(1),
ring: VecDeque::new(),
next_id: 1,
open: None,
}
}
/// `true` when depth is 1 and command-buffer retention may be used.
#[inline]
pub fn retains_command_buffers(&self) -> bool {
self.max_depth == 1
}
/// Maximum number of in-flight frame slots (configured at construction).
#[inline]
pub fn max_depth(&self) -> usize {
self.max_depth
}
/// Current number of slots waiting on the GPU or a swapchain present timeline.
#[inline]
pub fn pending_frames(&self) -> usize {
self.ring.len()
}
/// `true` if [`Self::begin_frame`] was called and the frame was not yet ended.
#[inline]
pub fn has_open_frame(&self) -> bool {
self.open.is_some()
}
/// Discard the currently open frame without pushing a ring slot.
///
/// Call this when a `run_frame` error makes it impossible to call an end-frame
/// method. Leaves the ring intact so subsequent frames can begin normally.
pub fn abort_frame(&mut self, handle: FrameHandle) {
if self.open == Some(handle) {
self.open = None;
}
}
/// Non-blocking drain of slots whose GPU timeline has completed, plus mandatory pops when the
/// ring is deeper than [`Self::max_depth`].
pub fn reclaim(&mut self) -> Result<(), GoldyError> {
self.drain_ring()
}
/// Block until the oldest in-flight ring slot retires (or reclaim if already done).
///
/// Used under memory pressure when the client needs the GPU to make progress before
/// retrying an allocation. No-op when the ring is empty.
pub fn wait_for_progress(&mut self) -> Result<(), GoldyError> {
let Some(front) = self.ring.front() else {
self.context.flush_deferred_deletions();
return Ok(());
};
if let Some(tv) = front.timeline {
if self.context.gpu_progress() < tv {
self.context.wait_until(tv)?;
}
} else {
let hw = self.context.high_water_timeline().max(self.context.gpu_progress());
if self.context.gpu_progress() < hw {
self.context.wait_until(hw)?;
}
}
self.drain_ring()?;
self.context.flush_deferred_deletions();
Ok(())
}
/// Begin recording a new frame: drains completed slots, then returns a handle if there is
/// capacity (possibly after blocking on the oldest in-flight work).
///
/// # Errors
///
/// Returns [`GoldyError`] if a frame is already open (missing end-frame call), or if a
/// depth-cap wait fails.
pub fn begin_frame(&mut self) -> Result<FrameHandle, GoldyError> {
let _tz = tracy_zone!("orchestrator.begin_frame");
if self.open.is_some() {
return Err(GoldyError::Backend(anyhow!(
"FrameOrchestrator::begin_frame: a frame is already open"
)));
}
self.drain_ring()?;
let h = FrameHandle(self.next_id);
self.next_id = self.next_id.wrapping_add(1);
self.open = Some(h);
Ok(h)
}
/// End a standalone (headless / render-to-texture) frame whose GPU work was already
/// submitted (e.g. via [`crate::Scheme::submit`]).
///
/// Pushes a ring slot stamped from `submission`, clears the open handle, and
/// emits a Tracy frame mark.
pub fn end_frame_standalone(&mut self, handle: FrameHandle, submission: &Submission) -> Result<(), GoldyError> {
let _tz = tracy_zone!("orchestrator.end_frame_standalone");
self.expect_open(handle)?;
self.ring.push_back(FrameSlot {
timeline: Some(submission.timeline_value()),
});
self.open = None;
tracy_frame_mark!();
Ok(())
}
/// End a frame whose scanout is deferred to surface present or
/// [`crate::Claim::consume`].
///
/// Pushes a ring slot whose timeline is filled later via [`Self::note_presented`], and does
/// **not** emit a Tracy frame mark (the mark belongs at present time).
pub fn end_frame_for_present(&mut self, handle: FrameHandle, _submission: &Submission) -> Result<(), GoldyError> {
let _tz = tracy_zone!("orchestrator.end_frame_for_present");
self.expect_open(handle)?;
self.ring.push_back(FrameSlot { timeline: None });
self.open = None;
Ok(())
}
/// Close an open frame without creating a retirement-ring slot.
///
/// Use when cross-frame resource ordering is enforced externally (scheme reuse epochs,
/// deferred host writes, present-easement ledger) so `begin_frame` must not wait on a
/// coarse frame timeline. The open handle is cleared; no Tracy frame mark is emitted.
pub fn end_frame_externally_ordered(&mut self, handle: FrameHandle) -> Result<(), GoldyError> {
let _tz = tracy_zone!("orchestrator.end_frame_externally_ordered");
self.expect_open(handle)?;
self.open = None;
Ok(())
}
/// After surface present / claim consume, stamp the most recent surface slot from `submission`.
pub fn note_presented(&mut self, submission: &Submission) {
if let Some(back) = self.ring.back_mut() {
if back.timeline.is_none() {
back.timeline = Some(submission.timeline_value());
}
}
}
/// Block until every pending slot has retired.
///
/// Slots whose timeline is still unknown (`None`, i.e. surface path before
/// [`Self::note_presented`]) use the context high-water as a completion fence —
/// callers draining mid-presentation should prefer [`Self::reclaim`] / presenting first.
pub fn drain_all(&mut self) -> Result<(), GoldyError> {
while let Some(slot) = self.ring.pop_front() {
let timeline = match slot.timeline {
Some(t) => t,
None => self.context.high_water_timeline().max(self.context.gpu_progress()),
};
if self.context.gpu_progress() < timeline {
self.context.wait_until(timeline)?;
}
}
Ok(())
}
fn expect_open(&self, handle: FrameHandle) -> Result<(), GoldyError> {
match self.open {
Some(h) if h == handle => Ok(()),
Some(_) => Err(GoldyError::Backend(anyhow!(
"FrameOrchestrator: wrong FrameHandle for this frame"
))),
None => Err(GoldyError::Backend(anyhow!(
"FrameOrchestrator: no open frame (call begin_frame first)"
))),
}
}
fn drain_ring(&mut self) -> Result<(), GoldyError> {
let _tz = tracy_zone!("orchestrator.drain_ring");
let mut progress = {
let _pg = tracy_zone!("orchestrator.drain_ring.gpu_progress");
self.context.gpu_progress()
};
while let Some(front) = self.ring.front() {
let done = match front.timeline {
Some(tv) => progress >= tv,
None => false,
};
let must_wait = !done && self.ring.len() >= self.max_depth;
if done || must_wait {
let slot = self.ring.pop_front().unwrap();
if let Some(tv) = slot.timeline {
if progress < tv && must_wait {
let _wz = tracy_zone!("orchestrator.wait_gpu");
self.context.wait_until(tv)?;
progress = tv;
}
}
} else {
break;
}
}
Ok(())
}
}