Implement more detailed diagnostics for OpenXR stages

This commit is contained in:
iChris4 committed 2026-09-20 17:24:16 +02:00
1 parent c4c7078afc
commit 787dbcf7d2
8 files changed
+212 -34

No files matched your search

+30 -3
View File
@@ -414,22 +414,49 @@ When it is on, `console.log` receives lines tagged `[runtime] [xr-diag]`
| Field | Meaning |
| --- | --- |
| `Hz`, `cycles` | Display rate from the predicted display period; compositor cycles (xrWaitFrame/xrEndFrame pairs, repeats included). |
| `predicted-rate`, `cycles` | Reciprocal of the runtime's predicted display period, **not necessarily physical headset refresh rate**; compositor cycles (xrWaitFrame/xrEndFrame pairs, repeats included). |
| `skipped-slots` | Display slots the predicted display time jumped over: the runtime throttled or dropped frames. |
| `late` | Frames whose xrEndFrame came after their predicted display time (needs `XR_KHR_win32_convert_performance_counter_time` or `XR_KHR_convert_timespec_time`). |
| `layers new/repeat/empty` | Cycles ending with a newly rendered layer, the retained layer again, or no layer at all (black). |
| `discarded`, `layer-rejected` | Retained layers dropped by a session or reference-space change; rendered layers not submitted (invalid pose or views, failed release). |
| `wait-frame`, `open`, `end-call` | Time blocked in xrWaitFrame, from xrBeginFrame to xrEndFrame, and inside xrEndFrame. |
| `end-margin`, `end-gap` | Predicted display time minus the xrEndFrame time; interval between xrEndFrame calls. |
| `pickup`, `render` | Stereo packet published until Aurora's frame worker takes it (without interpolation this includes waiting for the next 60 Hz game frame); taken until the eye copy is submitted. |
| `pickup`, `render` | Stereo packet published until Aurora's frame worker takes it (without interpolation this includes waiting for the next 60 Hz game frame); taken until the pacing thread observes the submission result. These are CPU wall times for completed submissions, **not GPU timestamps**; canceled packets are measured separately below. |
| `acquire`, `release` | Swapchain image acquire+wait and release. |
| `keepalive` | Retained-layer repeats while Aurora was still encoding past the 50 ms keep-alive. |
| `packet-unused`, `packet-rejected`, `submit-failed` | Packets no game frame took within 50 ms; packets Aurora took but rendered mono (content tag or transform check); failed stereo copies. |
| `packet-unused`, `packet-rejected`, `submit-failed` | Packets not picked up before cancellation; packets picked up but not encoded by the bridge before cancellation (the precise rejection cause is not known); failed stereo copies. |
| `interp-skip` | Cycles the VR interpolation rate cap chose not to render. |
| `frames immersive/screen` | Cycles per presentation mode; `not-rendered` counts cycles without views to render. |
| `no-orientation`, `no-position` | Cycles whose head orientation or position was not valid. |
| `suppressed` | Event lines dropped by the rate limit. |
- **Stage timings.** A separate `[xr-diag] stages ms` line accompanies each nonempty
window, independently of the event rate limit. Each field is `median/worst` in ms;
`@cycle=N,t=Ts` identifies the worst call's diagnostic cycle and completion time
since logging/session reset. The main summary also includes the last `cycle` and `t`.
Cycle 0 is before the first wait; work between cycles belongs to the previous cycle.
These are wall times, including time the OS did not schedule the thread. Nested
measurements (notably `sync-actions` within `input-sync`) must not be added together.
| Stage | What it isolates |
| --- | --- |
| `poll-events`, `begin-call`, `locate-views` | Event polling, the xrBeginFrame call itself, and xrLocateViews. |
| `input-sync`, `sync-actions` | Complete input update and its xrSyncActions call. |
| `publish`, `withdraw` | Packet construction/publication and withdrawal, including mutex waits. |
| `set-targets` | D3D12 bridge target registration, including its mutex wait. |
| `submission-wait` | Actual time waiting for a render result, including timeout paths; compare against the requested 50 ms. |
| `cancel` | Bridge cancellation attempt, whether it succeeds or fails. |
| `cancel-age` | Publication to cancellation, including packets never picked up. |
| `cancel-pickup`, `cancel-after-pickup` | Publication to pickup and pickup to cancellation for consumed, canceled packets. |
For a blackout report, enable logging before entering a race, reproduce the blackout,
and export the logs immediately afterward. Include the approximate time and whether
both eyes and the desktop mirror went black. Check `frames immersive` is nonzero for
an immersive-race capture. A large stage maximum identifies where the pacing thread
spent time, but cannot distinguish API blocking from OS scheduling without a system
trace. No empty layers does not rule out black image contents or compositor/display
problems. This instrumentation does not change frame pacing or inspect image pixels.
- **Event lines.** At most 8 per second; the rest are counted in `suppressed`. They report late
frames, skipped display slots, stalls (more than 2.5 display periods, and at least 25 ms, between
xrEndFrame calls), empty frames and their reason, discarded retained layers, rejected layers,
+45 -1
View File
@@ -23,6 +23,12 @@
// the settings overlay and the headless tests can use it in any build.
namespace mkw::vr::diagnostics {
// Pacing-thread wall time, including scheduling delays. Stages may nest.
enum class Stage : uint8_t {
PollEvents, BeginCall, LocateViews, InputSync, SyncActions, Publish,
Withdraw, SubmissionWait, Cancel, SetTargets, Count
};
enum class EmptyFrameReason : uint8_t {
NoRetainedLayer,
ShouldRenderOff,
@@ -105,7 +111,8 @@ public:
void OnInterpolationSkip();
void OnPacketPublished(int64_t now_ns);
// consumed_ns is when Aurora took the packet, or 0 if it never did.
void OnPacketCanceled(int64_t consumed_ns);
void OnPacketCanceled(int64_t now_ns, int64_t consumed_ns);
void OnStage(Stage stage, int64_t ns, int64_t now_ns);
void OnKeepaliveRepeat();
void OnSubmission(int64_t now_ns, int64_t consumed_ns, bool success);
@@ -168,6 +175,18 @@ private:
uint32_t events_ = 0;
uint32_t suppressed_ = 0;
struct StageSamples {
Samples samples;
int64_t worst_ns = -1;
int64_t worst_at_ns = 0;
uint64_t cycle = 0;
};
std::array<StageSamples, static_cast<size_t>(Stage::Count)> stages_{};
int64_t session_start_ns_ = 0;
uint64_t cycle_sequence_ = 0;
Samples cancel_age_ms_;
Samples cancel_pickup_ms_;
Samples cancel_after_pickup_ms_;
Samples wait_frame_ms_;
Samples open_ms_;
Samples margin_ms_;
@@ -184,6 +203,7 @@ inline std::atomic_bool g_enabled{false};
inline std::atomic_int64_t g_packet_consumed_ns{0};
int64_t NowNs() noexcept;
void OnStage(Stage stage, int64_t ns);
void OnWaitFrame(int64_t wait_ns, int64_t display_time, int64_t display_period);
void OnBeginFrame();
void OnEndFrame(int64_t submit_ns, int64_t call_ns);
@@ -233,6 +253,30 @@ private:
int64_t start_ns_;
};
// Only use on the XR pacing thread. A diagnostic failure must not interrupt
// frame ownership or change the instrumented call's return value.
class ScopedStage {
public:
explicit ScopedStage(Stage stage) noexcept : stage_(stage) {}
~ScopedStage() noexcept {
const int64_t ns = timer_.ElapsedNs();
if (ns >= 0 && Enabled()) {
try { detail::OnStage(stage_, ns); } catch (...) {}
}
}
ScopedStage(const ScopedStage&) = delete;
ScopedStage& operator=(const ScopedStage&) = delete;
private:
Stage stage_;
Stopwatch timer_;
};
template <typename Call>
decltype(auto) Measure(Stage stage, Call&& call) {
const ScopedStage timer(stage);
return call();
}
inline void OnWaitFrame(const Stopwatch& wait, int64_t display_time, int64_t display_period) {
if (const int64_t ns = wait.ElapsedNs(); ns >= 0 && Enabled()) detail::OnWaitFrame(ns, display_time, display_period);
}
+3 -1
View File
@@ -316,7 +316,9 @@ public:
submission_success_ = false;
submission_unsafe_ = false;
}
if (!aurora_d3d12_set_stereo_targets(frame.xr_frame.serial, targets.data(), target_count)) {
if (!diagnostics::Measure(diagnostics::Stage::SetTargets, [&] {
return aurora_d3d12_set_stereo_targets(frame.xr_frame.serial, targets.data(), target_count);
})) {
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
+58 -8
View File
@@ -14,6 +14,10 @@
namespace mkw::vr::diagnostics {
namespace {
constexpr std::array<const char*, static_cast<size_t>(Stage::Count)> kStageNames{
"poll-events", "begin-call", "locate-views", "input-sync", "sync-actions",
"publish", "withdraw", "submission-wait", "cancel", "set-targets"};
constexpr double kNanosecondsPerMillisecond = 1'000'000.0;
double Milliseconds(int64_t nanoseconds) noexcept {
@@ -123,6 +127,7 @@ void FrameDiagnostics::Reset(int64_t now_ns) {
Sink sink = std::move(sink_);
*this = FrameDiagnostics(std::move(sink));
window_start_ns_ = now_ns;
session_start_ns_ = now_ns;
session_info_requested_ = true;
}
@@ -136,6 +141,7 @@ void FrameDiagnostics::OnWaitFrame(int64_t now_ns, int64_t wait_ns, int64_t disp
window_start_ns_ = now_ns;
}
++cycles_;
++cycle_sequence_;
wait_frame_ms_.Add(Milliseconds(wait_ns));
if (display_period > 0) {
display_period_ = display_period;
@@ -280,14 +286,31 @@ void FrameDiagnostics::OnPacketPublished(int64_t now_ns) {
published_ns_ = now_ns;
}
void FrameDiagnostics::OnPacketCanceled(int64_t consumed_ns) {
void FrameDiagnostics::OnStage(Stage stage, int64_t ns, int64_t now_ns) {
if (ns < 0 || static_cast<size_t>(stage) >= stages_.size()) return;
auto& sample = stages_[static_cast<size_t>(stage)];
sample.samples.Add(Milliseconds(ns));
if (ns > sample.worst_ns) {
sample.worst_ns = ns;
sample.worst_at_ns = now_ns;
sample.cycle = cycle_sequence_;
}
}
void FrameDiagnostics::OnPacketCanceled(int64_t now_ns, int64_t consumed_ns) {
if (published_ns_ != 0 && now_ns >= published_ns_) {
cancel_age_ms_.Add(Milliseconds(now_ns - published_ns_));
if (consumed_ns >= published_ns_ && consumed_ns <= now_ns) {
cancel_pickup_ms_.Add(Milliseconds(consumed_ns - published_ns_));
cancel_after_pickup_ms_.Add(Milliseconds(now_ns - consumed_ns));
}
}
if (consumed_ns == 0 || consumed_ns < published_ns_) {
++packet_unused_;
Event("stereo packet withdrawn: no game frame picked it up within 50 ms");
Event("stereo packet withdrawn: no game frame picked it up before cancellation");
} else {
++packet_rejected_;
Event("stereo packet canceled: Aurora picked it up but rendered that frame without it "
"(content tag or transform check)");
Event("stereo packet canceled: Aurora picked it up but the bridge had not encoded it before cancellation");
}
published_ns_ = 0;
}
@@ -355,8 +378,13 @@ void FrameDiagnostics::ClearWindow(int64_t now_ns) {
keepalive_ = packet_unused_ = packet_rejected_ = submit_failed_ = interp_skip_ = 0;
immersive_ = screen_ = not_rendered_ = no_orientation_ = no_position_ = 0;
events_ = suppressed_ = 0;
for (auto& stage : stages_) {
stage.samples.Clear();
stage.worst_ns = -1;
}
for (Samples* samples : {&wait_frame_ms_, &open_ms_, &margin_ms_, &end_gap_ms_, &end_call_ms_,
&acquire_ms_, &release_ms_, &game_wait_ms_, &encode_ms_}) {
&acquire_ms_, &release_ms_, &game_wait_ms_, &encode_ms_,
&cancel_age_ms_, &cancel_pickup_ms_, &cancel_after_pickup_ms_}) {
samples->Clear();
}
}
@@ -365,7 +393,7 @@ void FrameDiagnostics::EmitSummary(int64_t now_ns) {
std::string text;
AppendFormat(text, "%.2fs", Milliseconds(now_ns - window_start_ns_) / 1000.0);
if (display_period_ > 0) {
AppendFormat(text, " %.1fHz", 1.0e9 / static_cast<double>(display_period_));
AppendFormat(text, " predicted-rate=%.1fHz", 1.0e9 / static_cast<double>(display_period_));
}
AppendFormat(text, " cycles=%u skipped-slots=%u late=%u", cycles_, skipped_slots_, late_);
AppendFormat(text, " | layers new=%u repeat=%u empty=%u discarded=%u layer-rejected=%u", layers_new_,
@@ -384,8 +412,26 @@ void FrameDiagnostics::EmitSummary(int64_t now_ns) {
keepalive_, packet_unused_, packet_rejected_, submit_failed_, interp_skip_);
AppendFormat(text, " | frames immersive=%u screen=%u not-rendered=%u no-orientation=%u no-position=%u",
immersive_, screen_, not_rendered_, no_orientation_, no_position_);
AppendFormat(text, " | suppressed=%u", suppressed_);
AppendFormat(text, " | suppressed=%u cycle=%llu t=%.3fs", suppressed_,
static_cast<unsigned long long>(cycle_sequence_),
static_cast<double>(now_ns - session_start_ns_) / 1.0e9);
Info(text);
// Skipped-slot event spam must not suppress evidence of a blocking call.
std::string stages = "stages ms";
for (size_t i = 0; i < stages_.size(); ++i) {
auto& sample = stages_[i];
AppendStat(stages, kStageNames[i], sample.samples.values, false);
if (sample.worst_ns >= 0) {
AppendFormat(stages, "@cycle=%llu,t=%.3fs",
static_cast<unsigned long long>(sample.cycle),
static_cast<double>(sample.worst_at_ns - session_start_ns_) / 1.0e9);
}
}
AppendStat(stages, "cancel-age", cancel_age_ms_.values, false);
AppendStat(stages, "cancel-pickup", cancel_pickup_ms_.values, false);
AppendStat(stages, "cancel-after-pickup", cancel_after_pickup_ms_.values, false);
if (!cancel_age_ms_.values.empty() || std::any_of(stages_.begin(), stages_.end(),
[](const auto& stage) { return stage.worst_ns >= 0; })) Info(stages);
}
namespace {
@@ -473,6 +519,10 @@ void OnWaitFrame(int64_t wait_ns, int64_t display_time, int64_t display_period)
collector.OnWaitFrame(NowNs(), wait_ns, display_time, display_period, ConvertDisplayTime(display_time));
}
void OnStage(Stage stage, int64_t ns) {
Collector().OnStage(stage, ns, NowNs());
}
void OnBeginFrame() {
Collector().OnBeginFrame(NowNs());
}
@@ -524,7 +574,7 @@ void OnPacketPublished() {
}
void OnPacketCanceled() {
Collector().OnPacketCanceled(g_packet_consumed_ns.load(std::memory_order_relaxed));
Collector().OnPacketCanceled(NowNs(), g_packet_consumed_ns.load(std::memory_order_relaxed));
}
void OnKeepaliveRepeat() {
+4 -1
View File
@@ -10,6 +10,7 @@
#endif
#include "vr/openxr_input.h"
#include "vr/openxr_diagnostics.h"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_joystick.h>
@@ -518,7 +519,9 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
XrActionsSyncInfo sync{XR_TYPE_ACTIONS_SYNC_INFO};
sync.countActiveActionSets = 1;
sync.activeActionSets = &active;
const XrResult result = xrSyncActions(m_runtime->Session(), &sync);
const XrResult result = diagnostics::Measure(diagnostics::Stage::SyncActions, [&] {
return xrSyncActions(m_runtime->Session(), &sync);
});
m_runtime->ObserveResult(result);
if (XR_FAILED(result)) {
if (!m_logged_sync_failure) {
+26 -10
View File
@@ -736,7 +736,9 @@ private:
gx_registered = RegisterGxThread();
}
#endif
const OpenXREventStatus events = runtime_->PollEvents();
const OpenXREventStatus events = diagnostics::Measure(diagnostics::Stage::PollEvents, [&] {
return runtime_->PollEvents();
});
const bool session_active = runtime_->IsSessionRunning();
MkwVRPolicySetSessionActive(session_active);
const uint64_t session_run_serial = runtime_->SessionRunSerial();
@@ -859,6 +861,7 @@ private:
ServiceRecenterRequest();
UpdateVirtualScreenPose(frame);
if (input_ != nullptr) {
const diagnostics::ScopedStage input_timer(diagnostics::Stage::InputSync);
// After the screen is placed, so the pointer aims at this
// frame's screen rather than the previous one's.
input_->Sync(frame.xr_frame.predicted_display_time, PointerScreen(frame, policy, immersive),
@@ -876,7 +879,9 @@ private:
if (aurora_get_stereo_frame_interpolation() &&
!interpolation_pacing_.ShouldRender(frame.xr_frame.predicted_display_time, interpolation_target)) {
diagnostics::OnInterpolationSkip();
if (!backend_->TryCancelPendingFrame(frame) || !backend_->FinishFrame(frame, false)) {
if (!diagnostics::Measure(diagnostics::Stage::Cancel, [&] {
return backend_->TryCancelPendingFrame(frame);
}) || !backend_->FinishFrame(frame, false)) {
SetError(backend_->LastError());
fatal = true;
}
@@ -884,6 +889,7 @@ private:
}
{
const diagnostics::ScopedStage publish_timer(diagnostics::Stage::Publish);
std::lock_guard lock(published_mutex_);
// First person renders at life-size scale, third person at the
// configured diorama scale. Head translation and IPD are the
@@ -904,14 +910,18 @@ private:
submission == OpenXRSubmissionStatus::Timeout) {
// Fresh rendering wakes us immediately. A 50 ms keep-alive
// protects stalls without issuing eager repeats during GPU work.
submission = backend_->WaitForSubmission(frame, 50);
submission = diagnostics::Measure(diagnostics::Stage::SubmissionWait, [&] {
return backend_->WaitForSubmission(frame, 50);
});
if (submission == OpenXRSubmissionStatus::Timeout) {
// A pause, minimized window, or guest stall may leave no GX
// frame to consume this packet. Withdraw it, then cancel the
// matching bridge target only if Encode has not taken ownership.
if (std::chrono::steady_clock::now() >= cancel_after) {
WithdrawPublishedFrame();
canceled_before_encode = backend_->TryCancelPendingFrame(frame);
diagnostics::Measure(diagnostics::Stage::Withdraw, [&] { WithdrawPublishedFrame(); });
canceled_before_encode = diagnostics::Measure(diagnostics::Stage::Cancel, [&] {
return backend_->TryCancelPendingFrame(frame);
});
if (canceled_before_encode) {
diagnostics::OnPacketCanceled();
break;
@@ -925,7 +935,7 @@ private:
}
}
}
WithdrawPublishedFrame();
diagnostics::Measure(diagnostics::Stage::Withdraw, [&] { WithdrawPublishedFrame(); });
if (stop_.load(std::memory_order_acquire)) {
// Aurora has been drained by Shutdown(); backend shutdown below
// cancels its pending target, then either safely releases the
@@ -1035,6 +1045,7 @@ private:
ServiceRecenterRequest();
UpdateVirtualScreenPose(packet);
if (input_ != nullptr) {
const diagnostics::ScopedStage input_timer(diagnostics::Stage::InputSync);
input_->Sync(packet.xr_frame.predicted_display_time, PointerScreen(packet, policy, immersive),
SettingsPanelScreen(packet, policy, immersive));
}
@@ -1043,6 +1054,7 @@ private:
return KeepAlive();
}
{
const diagnostics::ScopedStage publish_timer(diagnostics::Stage::Publish);
std::lock_guard lock(published_mutex_);
BuildPublishedFrame(packet, immersive, policy.EffectiveUnitsPerMeter(), policy.content_tag);
diagnostics::OnPacketPublished();
@@ -1056,11 +1068,15 @@ private:
bool canceled_before_encode = false;
const auto cancel_after = std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
while (!stop_.load(std::memory_order_acquire) && submission == OpenXRSubmissionStatus::Timeout) {
submission = backend_->WaitForSubmission(packet, 50);
submission = diagnostics::Measure(diagnostics::Stage::SubmissionWait, [&] {
return backend_->WaitForSubmission(packet, 50);
});
if (submission == OpenXRSubmissionStatus::Timeout) {
if (std::chrono::steady_clock::now() >= cancel_after) {
WithdrawPublishedFrame();
canceled_before_encode = backend_->TryCancelPendingPacket(packet);
diagnostics::Measure(diagnostics::Stage::Withdraw, [&] { WithdrawPublishedFrame(); });
canceled_before_encode = diagnostics::Measure(diagnostics::Stage::Cancel, [&] {
return backend_->TryCancelPendingPacket(packet);
});
if (canceled_before_encode) {
diagnostics::OnPacketCanceled();
break;
@@ -1072,7 +1088,7 @@ private:
}
}
}
WithdrawPublishedFrame();
diagnostics::Measure(diagnostics::Stage::Withdraw, [&] { WithdrawPublishedFrame(); });
if (stop_.load(std::memory_order_acquire) || canceled_before_encode ||
submission == OpenXRSubmissionStatus::ShuttingDown) {
return true;
+8 -4
View File
@@ -723,7 +723,9 @@ bool OpenXRRuntime::BeginFrame(const OpenXRFrame& frame) {
}
XrFrameBeginInfo begin_info{XR_TYPE_FRAME_BEGIN_INFO};
const XrResult result = xrBeginFrame(m_session, &begin_info);
const XrResult result = diagnostics::Measure(diagnostics::Stage::BeginCall, [&] {
return xrBeginFrame(m_session, &begin_info);
});
if (XR_FAILED(result)) {
m_frame_phase = FramePhase::Idle;
m_active_frame_serial = 0;
@@ -769,9 +771,11 @@ bool OpenXRRuntime::LocateViewsForFrame(OpenXRFrame& frame) {
locate_info.space = m_app_space;
XrViewState view_state{XR_TYPE_VIEW_STATE};
uint32_t view_count = 0;
if (!Check(xrLocateViews(m_session, &locate_info, &view_state,
kOpenXREyeCount, &view_count, frame.views.data()),
"xrLocateViews")) {
const XrResult locate_result = diagnostics::Measure(diagnostics::Stage::LocateViews, [&] {
return xrLocateViews(m_session, &locate_info, &view_state,
kOpenXREyeCount, &view_count, frame.views.data());
});
if (!Check(locate_result, "xrLocateViews")) {
return false;
}
if (view_count != kOpenXREyeCount) {
+38 -6
View File
@@ -171,25 +171,53 @@ void TestPacketAccounting() {
diagnostics.OnSubmission(now + 18 * kMs, now + 15 * kMs, true);
diagnostics.OnPacketPublished(now + 20 * kMs);
diagnostics.OnPacketCanceled(0);
Require(capture.Count("no game frame picked it up within 50 ms") == 1);
diagnostics.OnPacketCanceled(now + 75 * kMs, 0);
Require(capture.Count("no game frame picked it up before cancellation") == 1);
diagnostics.OnPacketPublished(now + 80 * kMs);
diagnostics.OnPacketCanceled(now + 90 * kMs);
Require(capture.Count("Aurora picked it up but rendered that frame without it") == 1);
diagnostics.OnPacketCanceled(now + 280 * kMs, now + 90 * kMs);
Require(capture.Count("Aurora picked it up but the bridge had not encoded it") == 1);
diagnostics.OnPacketPublished(now + 200 * kMs);
diagnostics.OnSubmission(now + 210 * kMs, now + 205 * kMs, false);
diagnostics.OnPacketPublished(now + 400 * kMs);
diagnostics.OnSubmission(now + 410 * kMs, now + 405 * kMs, false);
Require(capture.Count("stereo submission failed") == 1);
diagnostics.Tick(now + 1'500 * kMs);
const std::string& summary = capture.Summary();
Require(summary.find("packet-unused=1 packet-rejected=1 submit-failed=1") != std::string::npos);
Require(capture.Count("cancel-age=200.0/200.0") == 1);
Require(capture.Count("cancel-pickup=10.0/10.0") == 1);
Require(capture.Count("cancel-after-pickup=190.0/190.0") == 1);
// Two submissions: pickup 15 and 5 ms, render 3 and 5 ms.
Require(summary.find("pickup=15.0/15.0") != std::string::npos);
Require(summary.find("render=5.0/5.0") != std::string::npos);
}
void TestStageTimingSurvivesEventFlood() {
Capture capture;
FrameDiagnostics diagnostics(capture.Sink());
const int64_t start = 10'000 * kMs;
diagnostics.Reset(start);
diagnostics.OnWaitFrame(start, 0, 1, kPeriod72Hz, 0);
for (int i = 0; i < 20; ++i) diagnostics.OnEmptyFrame(EmptyFrameReason::NoRetainedLayer);
diagnostics.OnStage(Stage::SyncActions, 2 * kMs, start + 2 * kMs);
diagnostics.OnStage(Stage::SyncActions, 180 * kMs, start + 500 * kMs);
diagnostics.OnStage(Stage::SyncActions, 3 * kMs, start + 600 * kMs);
diagnostics.OnStage(Stage::Cancel, -1, start);
diagnostics.Tick(start + 1'000 * kMs);
Require(capture.Count("sync-actions=3.0/180.0@cycle=1,t=0.500s") == 1);
Require(capture.Count("cancel=-") == 1);
Require(capture.Summary().find("suppressed=12") != std::string::npos);
diagnostics.OnStage(Stage::Cancel, 4 * kMs, start + 1'100 * kMs);
diagnostics.Tick(start + 2'000 * kMs);
Require(capture.Count("sync-actions=-") == 1);
Require(capture.Count("cancel=4.0/4.0@cycle=1,t=1.100s") == 1);
diagnostics.Reset(start + 3'000 * kMs);
diagnostics.OnStage(Stage::Cancel, kMs, start + 3'010 * kMs);
diagnostics.Tick(start + 4'000 * kMs);
Require(capture.Count("cancel=1.0/1.0@cycle=0,t=0.010s") == 1);
}
void TestViewGeometry() {
Capture capture;
FrameDiagnostics diagnostics(capture.Sink());
@@ -249,6 +277,9 @@ void TestGlobalSwitchAndConfig() {
const Stopwatch stopwatch;
Require(stopwatch.ElapsedNs() == -1);
}
int calls = 0;
Require(Measure(Stage::Cancel, [&] { ++calls; return 42; }) == 42);
Require(calls == 1);
OnEmptyFrame(EmptyFrameReason::NoRetainedLayer);
Require(lines.empty());
@@ -284,6 +315,7 @@ int main() {
TestEventsAreRateLimited();
TestTrackingEdgesAndRejections();
TestPacketAccounting();
TestStageTimingSurvivesEventFlood();
TestViewGeometry();
TestResetStartsANewSession();
TestGlobalSwitchAndConfig();