The open source OpenXR runtime
1// Copyright 2022, Collabora, Ltd.
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief Frame pacing tests.
6 * @author Rylie Pavlik <rylie.pavlik@collabora.com>
7 */
8
9#include <util/u_pacing.h>
10
11#include "catch_amalgamated.hpp"
12
13#include "time_utils.hpp"
14
15#include <iostream>
16#include <chrono>
17#include <sstream>
18#include <iomanip>
19#include <queue>
20
21using namespace std::chrono_literals;
22using namespace std::chrono;
23
24static constexpr unanoseconds frame_interval_ns(16ms);
25
26namespace {
27
28int64_t
29getNextPresentAfterTimestampAndKnownPresent(int64_t timestamp_ns, int64_t known_present_ns)
30{
31
32 while (known_present_ns < timestamp_ns) {
33 known_present_ns += frame_interval_ns.count();
34 }
35 return known_present_ns;
36}
37int64_t
38getPresentBefore(int64_t timestamp_ns, int64_t known_present_ns)
39{
40
41 while (known_present_ns >= timestamp_ns && known_present_ns > frame_interval_ns.count()) {
42 known_present_ns -= frame_interval_ns.count();
43 }
44 return known_present_ns;
45}
46int64_t
47getNextPresentAfterTimestamp(int64_t timestamp_ns, int64_t known_present_ns)
48{
49 auto present_before_ns = getPresentBefore(timestamp_ns, known_present_ns);
50 return getNextPresentAfterTimestampAndKnownPresent(timestamp_ns, present_before_ns);
51}
52
53struct CompositorPredictions
54{
55 int64_t frame_id{0};
56 int64_t wake_up_time_ns{0};
57 int64_t desired_present_time_ns{0};
58 int64_t present_slop_ns{0};
59 int64_t predicted_display_time_ns{0};
60 int64_t predicted_display_period_ns{0};
61 int64_t min_display_period_ns{0};
62};
63} // namespace
64
65static void
66basicPredictionConsistencyChecks(int64_t now_ns, CompositorPredictions const &predictions)
67{
68 INFO(predictions.frame_id);
69 INFO(now_ns);
70 CHECK(predictions.wake_up_time_ns >= now_ns);
71 CHECK(predictions.desired_present_time_ns > now_ns);
72 CHECK(predictions.desired_present_time_ns > predictions.wake_up_time_ns);
73 CHECK(predictions.predicted_display_time_ns > now_ns);
74 CHECK(predictions.predicted_display_time_ns > predictions.desired_present_time_ns);
75 // display period predicted to be +- 2ms (arbitrary)
76 CHECK(unanoseconds(predictions.predicted_display_period_ns) < (frame_interval_ns + unanoseconds(2ms)));
77 CHECK(unanoseconds(predictions.predicted_display_period_ns) > (frame_interval_ns - unanoseconds(2ms)));
78}
79
80struct SimulatedDisplayTimingData
81{
82 SimulatedDisplayTimingData(int64_t id, int64_t desired_present_time, int64_t gpu_finish, int64_t now)
83 : frame_id(id), desired_present_time_ns(desired_present_time),
84 actual_present_time_ns(getNextPresentAfterTimestampAndKnownPresent(gpu_finish, desired_present_time)),
85 earliest_present_time_ns(getNextPresentAfterTimestamp(gpu_finish, desired_present_time)),
86 present_margin_ns(earliest_present_time_ns - gpu_finish), now_ns(now)
87 {}
88
89 int64_t frame_id;
90 int64_t desired_present_time_ns;
91 int64_t actual_present_time_ns;
92 int64_t earliest_present_time_ns;
93 int64_t present_margin_ns;
94 int64_t now_ns;
95 void
96 call_u_pc_info(u_pacing_compositor *upc) const
97 {
98 std::cout << "frame_id: " << frame_id << std::endl;
99 std::cout << "desired_present_time_ns: " << desired_present_time_ns << std::endl;
100 std::cout << "actual_present_time_ns: " << actual_present_time_ns << std::endl;
101 std::cout << "earliest_present_time_ns: " << earliest_present_time_ns << std::endl;
102 std::cout << "present_margin_ns: " << present_margin_ns << std::endl;
103 std::cout << "now_ns: " << now_ns << "\n" << std::endl;
104 u_pc_info(upc, frame_id, desired_present_time_ns, actual_present_time_ns, earliest_present_time_ns,
105 present_margin_ns, now_ns);
106 }
107};
108static inline bool
109operator>(SimulatedDisplayTimingData const &lhs, SimulatedDisplayTimingData const &rhs)
110{
111 return lhs.now_ns > rhs.now_ns;
112}
113using SimulatedDisplayTimingQueue = std::priority_queue<SimulatedDisplayTimingData,
114 std::vector<SimulatedDisplayTimingData>,
115 std::greater<SimulatedDisplayTimingData>>;
116
117//! Process all simulated timing data in the queue that should be processed by now.
118static void
119processDisplayTimingQueue(SimulatedDisplayTimingQueue &display_timing_queue, int64_t now_ns, u_pacing_compositor *upc)
120{
121 while (!display_timing_queue.empty() && display_timing_queue.top().now_ns <= now_ns) {
122 display_timing_queue.top().call_u_pc_info(upc);
123 display_timing_queue.pop();
124 }
125}
126//! Process all remaining simulated timing data in the queue and return the timestamp of the last one.
127static int64_t
128drainDisplayTimingQueue(SimulatedDisplayTimingQueue &display_timing_queue, int64_t now_ns, u_pacing_compositor *upc)
129{
130 while (!display_timing_queue.empty()) {
131 now_ns = display_timing_queue.top().now_ns;
132 display_timing_queue.top().call_u_pc_info(upc);
133 display_timing_queue.pop();
134 }
135 return now_ns;
136}
137
138static void
139doFrame(SimulatedDisplayTimingQueue &display_timing_queue,
140 u_pacing_compositor *upc,
141 MockClock &clock,
142 int64_t wake_time_ns,
143 int64_t desired_present_time_ns,
144 int64_t frame_id,
145 unanoseconds wake_delay,
146 unanoseconds begin_delay,
147 unanoseconds draw_delay,
148 unanoseconds submit_delay,
149 unanoseconds gpu_time_after_submit)
150{
151 REQUIRE(clock.now() <= wake_time_ns);
152 // wake up (after delay)
153 clock.advance_to(wake_time_ns);
154 clock.advance(wake_delay);
155 processDisplayTimingQueue(display_timing_queue, clock.now(), upc);
156 u_pc_mark_point(upc, U_TIMING_POINT_WAKE_UP, frame_id, clock.now());
157
158 // begin (after delay)
159 clock.advance(begin_delay);
160 processDisplayTimingQueue(display_timing_queue, clock.now(), upc);
161 u_pc_mark_point(upc, U_TIMING_POINT_BEGIN, frame_id, clock.now());
162
163 // spend cpu time drawing
164 clock.advance(draw_delay);
165 u_pc_mark_point(upc, U_TIMING_POINT_SUBMIT_BEGIN, frame_id, clock.now());
166
167 // spend cpu time before submit
168 clock.advance(submit_delay);
169 processDisplayTimingQueue(display_timing_queue, clock.now(), upc);
170 u_pc_mark_point(upc, U_TIMING_POINT_SUBMIT_END, frame_id, clock.now());
171
172 // spend gpu time before present
173 clock.advance(gpu_time_after_submit);
174 auto gpu_finish = clock.now();
175 auto next_scanout_timepoint = getNextPresentAfterTimestampAndKnownPresent(gpu_finish, desired_present_time_ns);
176
177 REQUIRE(next_scanout_timepoint >= gpu_finish);
178
179 // our wisdom arrives after scanout
180 MockClock infoClock;
181 infoClock.advance_to(next_scanout_timepoint);
182 infoClock.advance(1ms);
183 display_timing_queue.push({frame_id, desired_present_time_ns, gpu_finish, infoClock.now()});
184}
185
186// u_pc is for the compositor, we should take way less than a frame to do our job.
187static constexpr auto wakeDelay = microseconds(20);
188
189static constexpr auto shortBeginDelay = microseconds(20);
190static constexpr auto shortDrawDelay = 150us;
191static constexpr auto shortSubmitDelay = 50us;
192static constexpr auto shortGpuTime = 1ms;
193
194
195static constexpr auto longBeginDelay = 1ms;
196static constexpr auto longDrawDelay = 2ms;
197static constexpr auto longGpuTime = 2ms;
198
199TEST_CASE("u_pacing_compositor_display_timing")
200{
201 u_pacing_compositor *upc = nullptr;
202 MockClock clock;
203 REQUIRE(XRT_SUCCESS ==
204 u_pc_display_timing_create(frame_interval_ns.count(), &U_PC_DISPLAY_TIMING_CONFIG_DEFAULT, &upc));
205 REQUIRE(upc != nullptr);
206
207 clock.advance(1ms);
208
209 CompositorPredictions predictions;
210 u_pc_predict(upc, clock.now(), &predictions.frame_id, &predictions.wake_up_time_ns,
211 &predictions.desired_present_time_ns, &predictions.present_slop_ns,
212 &predictions.predicted_display_time_ns, &predictions.predicted_display_period_ns,
213 &predictions.min_display_period_ns);
214 basicPredictionConsistencyChecks(clock.now(), predictions);
215
216 auto frame_id = predictions.frame_id;
217 SimulatedDisplayTimingQueue queue;
218
219
220 SECTION("faster than expected")
221 {
222 // We have a 16ms period
223 // wake promptly
224 clock.advance(wakeDelay);
225 u_pc_mark_point(upc, U_TIMING_POINT_WAKE_UP, frame_id, clock.now());
226
227 // start promptly
228 clock.advance(shortBeginDelay);
229 u_pc_mark_point(upc, U_TIMING_POINT_BEGIN, frame_id, clock.now());
230
231 // spend cpu time drawing
232 clock.advance(shortDrawDelay);
233 u_pc_mark_point(upc, U_TIMING_POINT_SUBMIT_BEGIN, frame_id, clock.now());
234
235 // spend a little cpu time submitting the work to the GPU
236 clock.advance(shortSubmitDelay);
237 u_pc_mark_point(upc, U_TIMING_POINT_SUBMIT_END, frame_id, clock.now());
238
239 // spend time in gpu rendering until present
240 clock.advance(shortGpuTime);
241 auto gpu_finish = clock.now();
242
243 auto next_scanout_timepoint =
244 getNextPresentAfterTimestampAndKnownPresent(gpu_finish, predictions.desired_present_time_ns);
245
246 // our wisdom arrives after scanout
247 MockClock infoClock;
248 infoClock.advance_to(next_scanout_timepoint);
249 infoClock.advance(1ms);
250 queue.push({frame_id, predictions.desired_present_time_ns, gpu_finish, infoClock.now()});
251
252 // Do basically the same thing a few more frames.
253 for (int i = 0; i < 20; ++i) {
254 CompositorPredictions loopPred;
255 u_pc_predict(upc, clock.now(), &loopPred.frame_id, &loopPred.wake_up_time_ns,
256 &loopPred.desired_present_time_ns, &loopPred.present_slop_ns,
257 &loopPred.predicted_display_time_ns, &loopPred.predicted_display_period_ns,
258 &loopPred.min_display_period_ns);
259 CHECK(loopPred.frame_id > i);
260 INFO("frame id" << loopPred.frame_id);
261 INFO(clock.now());
262 basicPredictionConsistencyChecks(clock.now(), loopPred);
263 doFrame(queue, upc, clock, loopPred.wake_up_time_ns, loopPred.desired_present_time_ns,
264 loopPred.frame_id, wakeDelay, shortBeginDelay, shortDrawDelay, shortSubmitDelay,
265 shortGpuTime);
266 }
267 // we should now get a shorter time before present to wake up.
268 CompositorPredictions newPred;
269 u_pc_predict(upc, clock.now(), &newPred.frame_id, &newPred.wake_up_time_ns,
270 &newPred.desired_present_time_ns, &newPred.present_slop_ns,
271 &newPred.predicted_display_time_ns, &newPred.predicted_display_period_ns,
272 &newPred.min_display_period_ns);
273 basicPredictionConsistencyChecks(clock.now(), newPred);
274 CHECK(unanoseconds(newPred.desired_present_time_ns - newPred.wake_up_time_ns) <
275 unanoseconds(predictions.desired_present_time_ns - predictions.wake_up_time_ns));
276 CHECK(unanoseconds(newPred.desired_present_time_ns - newPred.wake_up_time_ns) >
277 unanoseconds(shortDrawDelay + shortSubmitDelay + shortGpuTime));
278 }
279
280 SECTION("slower than desired")
281 {
282 // We have a 16ms period
283 // wake promptly
284 clock.advance(wakeDelay);
285 u_pc_mark_point(upc, U_TIMING_POINT_WAKE_UP, frame_id, clock.now());
286
287 // waste time before beginframe
288 clock.advance(longBeginDelay);
289 u_pc_mark_point(upc, U_TIMING_POINT_BEGIN, frame_id, clock.now());
290
291 // spend cpu time drawing
292 clock.advance(longDrawDelay);
293 u_pc_mark_point(upc, U_TIMING_POINT_SUBMIT_BEGIN, frame_id, clock.now());
294
295 // spend a little cpu time submitting the work to the GPU
296 clock.advance(shortSubmitDelay);
297 u_pc_mark_point(upc, U_TIMING_POINT_SUBMIT_END, frame_id, clock.now());
298
299 // spend time in gpu rendering until present
300 clock.advance(longGpuTime);
301 auto gpu_finish = clock.now();
302
303 auto next_scanout_timepoint =
304 getNextPresentAfterTimestampAndKnownPresent(gpu_finish, predictions.desired_present_time_ns);
305
306 REQUIRE(next_scanout_timepoint > gpu_finish);
307
308
309 // our wisdom arrives after scanout
310 MockClock infoClock;
311 infoClock.advance_to(next_scanout_timepoint);
312 infoClock.advance(1ms);
313 queue.push({frame_id, predictions.desired_present_time_ns, gpu_finish, infoClock.now()});
314
315 // Do basically the same thing a few more frames.
316 for (int i = 0; i < 50; ++i) {
317 CompositorPredictions loopPred;
318 u_pc_predict(upc, clock.now(), &loopPred.frame_id, &loopPred.wake_up_time_ns,
319 &loopPred.desired_present_time_ns, &loopPred.present_slop_ns,
320 &loopPred.predicted_display_time_ns, &loopPred.predicted_display_period_ns,
321 &loopPred.min_display_period_ns);
322 INFO(loopPred.frame_id);
323 INFO(clock.now());
324 basicPredictionConsistencyChecks(clock.now(), loopPred);
325 doFrame(queue, upc, clock, loopPred.wake_up_time_ns, loopPred.desired_present_time_ns,
326 loopPred.frame_id, wakeDelay, longBeginDelay, longDrawDelay, shortSubmitDelay,
327 longGpuTime);
328 }
329
330 // we should now get a bigger time before present to wake up.
331 CompositorPredictions newPred;
332 u_pc_predict(upc, clock.now(), &newPred.frame_id, &newPred.wake_up_time_ns,
333 &newPred.desired_present_time_ns, &newPred.present_slop_ns,
334 &newPred.predicted_display_time_ns, &newPred.predicted_display_period_ns,
335 &newPred.min_display_period_ns);
336 basicPredictionConsistencyChecks(clock.now(), newPred);
337 CHECK(unanoseconds(newPred.desired_present_time_ns - newPred.wake_up_time_ns) >
338 unanoseconds(longBeginDelay + longDrawDelay + shortSubmitDelay + longGpuTime));
339 }
340
341 u_pc_destroy(&upc);
342}
343
344TEST_CASE("u_pacing_compositor_fake")
345{
346 MockClock clock;
347 u_pacing_compositor *upc = nullptr;
348 REQUIRE(XRT_SUCCESS == u_pc_fake_create(frame_interval_ns.count(), clock.now(), &upc));
349 REQUIRE(upc != nullptr);
350
351 clock.advance(1ms);
352
353 SECTION("Standalone predictions")
354 {
355 CompositorPredictions predictions;
356 u_pc_predict(upc, clock.now(), &predictions.frame_id, &predictions.wake_up_time_ns,
357 &predictions.desired_present_time_ns, &predictions.present_slop_ns,
358 &predictions.predicted_display_time_ns, &predictions.predicted_display_period_ns,
359 &predictions.min_display_period_ns);
360 basicPredictionConsistencyChecks(clock.now(), predictions);
361 }
362 SECTION("Consistency in loop")
363 {
364 SimulatedDisplayTimingQueue queue;
365 SECTION("Fast")
366 {
367
368 for (int i = 0; i < 10; ++i) {
369 CompositorPredictions predictions;
370 u_pc_predict(upc, clock.now(), &predictions.frame_id, &predictions.wake_up_time_ns,
371 &predictions.desired_present_time_ns, &predictions.present_slop_ns,
372 &predictions.predicted_display_time_ns,
373 &predictions.predicted_display_period_ns,
374 &predictions.min_display_period_ns);
375 INFO(predictions.frame_id);
376 INFO(clock.now());
377 basicPredictionConsistencyChecks(clock.now(), predictions);
378 doFrame(queue, upc, clock, predictions.wake_up_time_ns,
379 predictions.desired_present_time_ns, predictions.frame_id, wakeDelay,
380 shortBeginDelay, shortDrawDelay, shortSubmitDelay, shortGpuTime);
381 }
382 drainDisplayTimingQueue(queue, clock.now(), upc);
383 }
384 SECTION("Slow")
385 {
386 for (int i = 0; i < 10; ++i) {
387 CompositorPredictions predictions;
388 u_pc_predict(upc, clock.now(), &predictions.frame_id, &predictions.wake_up_time_ns,
389 &predictions.desired_present_time_ns, &predictions.present_slop_ns,
390 &predictions.predicted_display_time_ns,
391 &predictions.predicted_display_period_ns,
392 &predictions.min_display_period_ns);
393 INFO(predictions.frame_id);
394 INFO(clock.now());
395 basicPredictionConsistencyChecks(clock.now(), predictions);
396 doFrame(queue, upc, clock, predictions.wake_up_time_ns,
397 predictions.desired_present_time_ns, predictions.frame_id, wakeDelay,
398 longBeginDelay, longDrawDelay, shortSubmitDelay, longGpuTime);
399 }
400 drainDisplayTimingQueue(queue, clock.now(), upc);
401 }
402 }
403 u_pc_destroy(&upc);
404}