mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 04:04:18 +02:00
348 lines
12 KiB
C++
348 lines
12 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
// Opt-in OpenXR pacing and presentation diagnostics (F10 > Diagnostics).
|
|
//
|
|
// Off by default. Every hook below is an inline test of one atomic that returns
|
|
// at once while logging is off, so carrying them costs the XR pacing thread
|
|
// nothing measurable. When on, one-second windows are accumulated and written to
|
|
// console.log as a "[xr-diag]" summary line, plus rate-limited event lines for
|
|
// late, empty or discarded frames. OPENXR.md documents every field.
|
|
//
|
|
// All hooks except SetEnabled/Enabled and NotePacketConsumed run on the XR
|
|
// pacing thread, the collector's only writer. Nothing here includes OpenXR, so
|
|
// 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,
|
|
};
|
|
|
|
enum class DiscardReason : uint8_t {
|
|
SessionRestarted,
|
|
ReferenceSpaceChanged,
|
|
};
|
|
|
|
// Why a layer Aurora finished rendering was not submitted.
|
|
enum class RejectReason : uint8_t {
|
|
ReleaseFailed,
|
|
ShouldRenderOff,
|
|
ViewsInvalid,
|
|
PositionInvalid,
|
|
};
|
|
|
|
// The backends' submit condition, checked in the same order: release, then
|
|
// should_render, then views; a layer passing all three failed on head position.
|
|
inline RejectReason ClassifyRejectedLayer(bool release_ok, bool should_render, bool views_valid) noexcept {
|
|
if (!release_ok) {
|
|
return RejectReason::ReleaseFailed;
|
|
}
|
|
if (!should_render) {
|
|
return RejectReason::ShouldRenderOff;
|
|
}
|
|
return views_valid ? RejectReason::PositionInvalid : RejectReason::ViewsInvalid;
|
|
}
|
|
|
|
struct ViewGeometry {
|
|
// Per eye: left, right, up, down half-angles, in degrees.
|
|
std::array<std::array<float, 4>, 2> fov_degrees{};
|
|
// Angle between the two eyes' forward axes; non-zero on canted displays.
|
|
float cant_degrees = 0.0f;
|
|
// Negative when the runtime reported no valid eye positions.
|
|
float ipd_millimeters = -1.0f;
|
|
std::array<uint32_t, 2> width{};
|
|
std::array<uint32_t, 2> height{};
|
|
};
|
|
|
|
// Accumulates one window of frame measurements and formats it. Times are
|
|
// steady-clock nanoseconds passed in by the caller, so tests can drive it with
|
|
// a synthetic clock. Not synchronized: one thread owns an instance.
|
|
class FrameDiagnostics {
|
|
public:
|
|
using Sink = std::function<void(std::string_view line)>;
|
|
|
|
static constexpr int64_t kWindowNs = 1'000'000'000;
|
|
static constexpr uint32_t kMaxEventsPerWindow = 8;
|
|
|
|
explicit FrameDiagnostics(Sink sink = {});
|
|
|
|
void SetSink(Sink sink);
|
|
// Starts a fresh window and forgets per-session state (display-time grid,
|
|
// tracking, logged geometry), and asks for the session description again.
|
|
void Reset(int64_t now_ns);
|
|
// True once after each Reset: the owner should describe the session.
|
|
bool ConsumeSessionInfoRequest();
|
|
|
|
// Compositor cycle: xrWaitFrame returned. deadline_ns is the predicted
|
|
// display time on the steady clock, or 0 when it cannot be converted.
|
|
void OnWaitFrame(int64_t now_ns, int64_t wait_ns, int64_t display_time,
|
|
int64_t display_period, int64_t deadline_ns);
|
|
void OnBeginFrame(int64_t now_ns);
|
|
// xrEndFrame was entered at submit_ns and took call_ns.
|
|
void OnEndFrame(int64_t submit_ns, int64_t call_ns);
|
|
|
|
void OnFrameBegun(bool immersive, bool should_render, bool views_valid,
|
|
bool orientation_valid, bool position_valid);
|
|
void OnViewGeometry(const ViewGeometry& geometry);
|
|
void OnSwapchainAcquire(int64_t ns);
|
|
void OnSwapchainRelease(int64_t ns);
|
|
|
|
void OnLayer(bool fresh);
|
|
void OnEmptyFrame(EmptyFrameReason reason);
|
|
void OnRetainedLayerDiscarded(DiscardReason reason);
|
|
void OnLayerRejected(RejectReason reason);
|
|
|
|
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 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);
|
|
|
|
void OnReferenceSpaceChange(std::string_view space, bool pose_valid, bool effective_known,
|
|
double effective_in_ms);
|
|
|
|
// Unconditional line (session description); not rate-limited.
|
|
void Info(std::string_view text);
|
|
|
|
// Emits the summary when the window has lasted kWindowNs. Called from
|
|
// OnEndFrame, so any compositor cycle, repeated or not, flushes it.
|
|
void Tick(int64_t now_ns);
|
|
|
|
private:
|
|
struct Samples {
|
|
std::vector<float> values;
|
|
void Add(double ms);
|
|
void Clear() { values.clear(); }
|
|
};
|
|
|
|
void Event(std::string_view text);
|
|
void ClearWindow(int64_t now_ns);
|
|
void EmitSummary(int64_t now_ns);
|
|
|
|
Sink sink_;
|
|
int64_t window_start_ns_ = 0;
|
|
bool session_info_requested_ = true;
|
|
|
|
int64_t last_display_time_ = 0;
|
|
int64_t display_period_ = 0;
|
|
int64_t deadline_ns_ = 0;
|
|
int64_t begin_ns_ = 0;
|
|
int64_t last_submit_ns_ = 0;
|
|
int64_t published_ns_ = 0;
|
|
|
|
bool tracking_known_ = false;
|
|
bool orientation_valid_ = false;
|
|
bool position_valid_ = false;
|
|
bool geometry_logged_ = false;
|
|
ViewGeometry geometry_{};
|
|
|
|
uint32_t cycles_ = 0;
|
|
uint32_t skipped_slots_ = 0;
|
|
uint32_t late_ = 0;
|
|
uint32_t layers_new_ = 0;
|
|
uint32_t layers_repeat_ = 0;
|
|
uint32_t empty_ = 0;
|
|
uint32_t discarded_ = 0;
|
|
uint32_t layer_rejected_ = 0;
|
|
uint32_t keepalive_ = 0;
|
|
uint32_t packet_unused_ = 0;
|
|
uint32_t packet_rejected_ = 0;
|
|
uint32_t submit_failed_ = 0;
|
|
uint32_t interp_skip_ = 0;
|
|
uint32_t immersive_ = 0;
|
|
uint32_t screen_ = 0;
|
|
uint32_t not_rendered_ = 0;
|
|
uint32_t no_orientation_ = 0;
|
|
uint32_t no_position_ = 0;
|
|
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_;
|
|
Samples end_gap_ms_;
|
|
Samples end_call_ms_;
|
|
Samples acquire_ms_;
|
|
Samples release_ms_;
|
|
Samples game_wait_ms_;
|
|
Samples encode_ms_;
|
|
};
|
|
|
|
namespace detail {
|
|
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);
|
|
void OnFrameBegun(bool immersive, bool should_render, bool views_valid, bool orientation_valid,
|
|
bool position_valid);
|
|
void OnViewGeometry(const ViewGeometry& geometry);
|
|
void OnSwapchainAcquire(int64_t ns);
|
|
void OnSwapchainRelease(int64_t ns);
|
|
void OnLayer(bool fresh);
|
|
void OnEmptyFrame(EmptyFrameReason reason);
|
|
void OnRetainedLayerDiscarded(DiscardReason reason);
|
|
void OnLayerRejected(RejectReason reason);
|
|
void OnInterpolationSkip();
|
|
void OnPacketPublished();
|
|
void OnPacketCanceled();
|
|
void OnKeepaliveRepeat();
|
|
void OnSubmission(bool success);
|
|
void OnReferenceSpaceChange(std::string_view space, bool pose_valid, int64_t change_time);
|
|
void OnSessionStarted();
|
|
bool ConsumeSessionInfoRequest();
|
|
void Info(std::string_view text);
|
|
} // namespace detail
|
|
|
|
inline bool Enabled() noexcept { return detail::g_enabled.load(std::memory_order_relaxed); }
|
|
|
|
// Live switch. Turning logging on starts a new window and re-describes the
|
|
// session on the next frame.
|
|
void SetEnabled(bool enabled) noexcept;
|
|
|
|
// Where lines go; installed by the OpenXR integration. Set it only while the
|
|
// pacing thread is not running.
|
|
void SetLogSink(FrameDiagnostics::Sink sink);
|
|
|
|
// Converts an XrTime to steady-clock nanoseconds, or returns 0. Set it only
|
|
// while the pacing thread is not running, and clear it before its runtime dies.
|
|
void SetDisplayTimeConverter(std::function<int64_t(int64_t xr_time)> converter);
|
|
|
|
// Measures a span only when logging was on at its start.
|
|
class Stopwatch {
|
|
public:
|
|
Stopwatch() noexcept : start_ns_(Enabled() ? detail::NowNs() : 0) {}
|
|
int64_t StartNs() const noexcept { return start_ns_; }
|
|
// -1 when the stopwatch did not start.
|
|
int64_t ElapsedNs() const noexcept { return start_ns_ == 0 ? -1 : detail::NowNs() - start_ns_; }
|
|
|
|
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);
|
|
}
|
|
inline void OnBeginFrame() {
|
|
if (Enabled()) detail::OnBeginFrame();
|
|
}
|
|
inline void OnEndFrame(const Stopwatch& call) {
|
|
if (const int64_t ns = call.ElapsedNs(); ns >= 0 && Enabled()) detail::OnEndFrame(call.StartNs(), ns);
|
|
}
|
|
inline void OnFrameBegun(bool immersive, bool should_render, bool views_valid, bool orientation_valid,
|
|
bool position_valid) {
|
|
if (Enabled()) detail::OnFrameBegun(immersive, should_render, views_valid, orientation_valid, position_valid);
|
|
}
|
|
inline void OnSwapchainAcquire(const Stopwatch& acquire) {
|
|
if (const int64_t ns = acquire.ElapsedNs(); ns >= 0 && Enabled()) detail::OnSwapchainAcquire(ns);
|
|
}
|
|
inline void OnSwapchainRelease(const Stopwatch& release) {
|
|
if (const int64_t ns = release.ElapsedNs(); ns >= 0 && Enabled()) detail::OnSwapchainRelease(ns);
|
|
}
|
|
inline void OnLayer(bool fresh) {
|
|
if (Enabled()) detail::OnLayer(fresh);
|
|
}
|
|
inline void OnEmptyFrame(EmptyFrameReason reason) {
|
|
if (Enabled()) detail::OnEmptyFrame(reason);
|
|
}
|
|
inline void OnRetainedLayerDiscarded(DiscardReason reason) {
|
|
if (Enabled()) detail::OnRetainedLayerDiscarded(reason);
|
|
}
|
|
inline void OnLayerRejected(RejectReason reason) {
|
|
if (Enabled()) detail::OnLayerRejected(reason);
|
|
}
|
|
inline void OnInterpolationSkip() {
|
|
if (Enabled()) detail::OnInterpolationSkip();
|
|
}
|
|
inline void OnPacketPublished() {
|
|
if (Enabled()) detail::OnPacketPublished();
|
|
}
|
|
// Aurora's frame worker took the published packet. Any thread.
|
|
inline void NotePacketConsumed() {
|
|
if (Enabled()) detail::g_packet_consumed_ns.store(detail::NowNs(), std::memory_order_relaxed);
|
|
}
|
|
inline void OnPacketCanceled() {
|
|
if (Enabled()) detail::OnPacketCanceled();
|
|
}
|
|
inline void OnKeepaliveRepeat() {
|
|
if (Enabled()) detail::OnKeepaliveRepeat();
|
|
}
|
|
inline void OnSubmission(bool success) {
|
|
if (Enabled()) detail::OnSubmission(success);
|
|
}
|
|
inline void OnReferenceSpaceChange(std::string_view space, bool pose_valid, int64_t change_time) {
|
|
if (Enabled()) detail::OnReferenceSpaceChange(space, pose_valid, change_time);
|
|
}
|
|
inline void OnSessionStarted() {
|
|
if (Enabled()) detail::OnSessionStarted();
|
|
}
|
|
inline void OnViewGeometry(const ViewGeometry& geometry) {
|
|
if (Enabled()) detail::OnViewGeometry(geometry);
|
|
}
|
|
// True once after logging starts or a session begins: describe the session.
|
|
inline bool ConsumeSessionInfoRequest() {
|
|
return Enabled() && detail::ConsumeSessionInfoRequest();
|
|
}
|
|
inline void Info(std::string_view text) {
|
|
if (Enabled()) detail::Info(text);
|
|
}
|
|
|
|
} // namespace mkw::vr::diagnostics
|