mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 06:00:25 +02:00
Implement OpenXR Settings Panel and Stereo Overlay
This commit is contained in:
1 parent
f222eedf6b
commit
1346e53cd6
22 files changed
+1466
-101
No files matched your search
@@ -182,6 +182,9 @@ cursor for 100 ms before it disappears, so tracking spikes during fast motion do
|
|||||||
Raw IR camera dots in `KPADGetUnifiedWpadStatus` stay invalid; the game reads the pointer from
|
Raw IR camera dots in `KPADGetUnifiedWpadStatus` stay invalid; the game reads the pointer from
|
||||||
`KPADStatus`.
|
`KPADStatus`.
|
||||||
|
|
||||||
|
**Settings in the headset.** Clicking both thumbsticks together opens the settings panel described
|
||||||
|
below; while it is open the controllers operate the panel and the game sees them idle.
|
||||||
|
|
||||||
`"gamepad"` keeps the controllers one ordinary gamepad read through PAD as a GameCube controller:
|
`"gamepad"` keeps the controllers one ordinary gamepad read through PAD as a GameCube controller:
|
||||||
A/B → South/East, X/Y → West/North, index triggers → trigger axes, grips → shoulders, thumbsticks
|
A/B → South/East, X/Y → West/North, index triggers → trigger axes, grips → shoulders, thumbsticks
|
||||||
→ sticks (clicks → stick buttons), left menu → Start. Every binding in the F10 controller menu
|
→ sticks (clicks → stick buttons), left menu → Start. Every binding in the F10 controller menu
|
||||||
@@ -191,6 +194,45 @@ Bindings are suggested for `oculus/touch_controller` (Quest 2, 3 and Pro) and
|
|||||||
`khr/simple_controller`. `mkw_vr_wii_remote_tests` checks the accelerometer frame, the pointer
|
`khr/simple_controller`. `mkw_vr_wii_remote_tests` checks the accelerometer frame, the pointer
|
||||||
raycast and debounce, the picture placement and the button profile without a headset.
|
raycast and debounce, the picture placement and the button profile without a headset.
|
||||||
|
|
||||||
|
## Settings in the headset
|
||||||
|
|
||||||
|
The F10 settings bar is only visible on the desktop window, so the same settings are also offered on
|
||||||
|
a panel inside the headset, in menus and during an immersive race alike, including on the Quest.
|
||||||
|
**Click both thumbsticks together** to open it, and again to close it; the left controller's menu
|
||||||
|
button and the panel's *Close* button also close it. It can be opened from the desktop as well, with
|
||||||
|
**F10 → VR → Show these settings in the headset**.
|
||||||
|
|
||||||
|
The panel has the F10 bar's menus as tabs (VR, Graphics, Controllers, Audio, Diagnostics) and a
|
||||||
|
*Recenter view* button. Aim a controller at it: the cursor goes where you aim, a trigger (or A / X)
|
||||||
|
selects and drags sliders, and a thumbstick scrolls. Whichever hand last pulled its trigger does the
|
||||||
|
pointing. Changes apply exactly as they do from the F10 bar, and the two stay in step.
|
||||||
|
|
||||||
|
While the panel is open, and until every button has been released after it closes, the game sees
|
||||||
|
the VR controllers idle: no buttons, no pointer and a remote at rest. Nothing reaches the game from
|
||||||
|
the chord, the trigger that clicked *Close*, or the menu press that closed the panel. The game is
|
||||||
|
not paused, so a race carries on while you change settings. Other controllers (keyboard, desktop
|
||||||
|
gamepads, Bluetooth remotes) are not affected.
|
||||||
|
|
||||||
|
The panel sits centred on the virtual screen, three quarters of its width across (1.8 m with the
|
||||||
|
default `hud_width_meters`). On a menu that is the anchored menu quad; in a race it is the 2D layer's
|
||||||
|
screen, `hud_distance_meters` ahead of the latched race origin and turned by the lean-back angle,
|
||||||
|
whether or not `hud_virtual_screen` places the HUD there. **Recenter view** brings both back in front
|
||||||
|
of you.
|
||||||
|
|
||||||
|
How it is drawn: `settings_overlay.cpp` builds the panel with a second Dear ImGui context of its own,
|
||||||
|
a 1440 × 1080 canvas at twice the desktop menu's scale with its own font atlas, fed by the pointer
|
||||||
|
that `openxr_input.cpp` publishes through `vr/openxr_settings_panel.h`. Aurora renders that draw data
|
||||||
|
into a panel texture once per sealed frame and lays it over each eye after the eye is finished
|
||||||
|
(`aurora-main/lib/stereo_overlay.cpp`): through the eye's frustum and `viewFromCenter` onto the
|
||||||
|
screen rectangle for an immersive eye (including headset-rate interpolated eyes, which reuse the
|
||||||
|
texture), and as a centred rectangle on a virtual-screen eye image. The eye images the OpenXR
|
||||||
|
backends already submit carry it, so no extra swapchain or composition layer is involved. The
|
||||||
|
ImGui backend keeps a single projection uniform, so the panel's pass is submitted on its own command
|
||||||
|
buffer before the desktop's ImGui pass of the same frame is recorded.
|
||||||
|
|
||||||
|
`mkw_vr_settings_panel_tests` covers the chord, the release latch, selection, scrolling and the
|
||||||
|
canvas mapping; `gx_fifo_tests` covers where the panel lands in each eye.
|
||||||
|
|
||||||
## The first-person camera
|
## The first-person camera
|
||||||
|
|
||||||
By default the headset sits where Mario Kart's own chase camera sits, and `world_units_per_meter`
|
By default the headset sits where Mario Kart's own chase camera sits, and `world_units_per_meter`
|
||||||
@@ -448,8 +490,10 @@ ends, including mid-frame flushes, so live setting changes cannot invalidate pen
|
|||||||
- The Quest build (`android/`, `docs/quest-port.md`) runs on a Quest 3 through menus and races.
|
- The Quest build (`android/`, `docs/quest-port.md`) runs on a Quest 3 through menus and races.
|
||||||
Lifecycle events and performance (about 43 game FPS) are still open. Apple visionOS packaging
|
Lifecycle events and performance (about 43 game FPS) are still open. Apple visionOS packaging
|
||||||
is not implemented.
|
is not implemented.
|
||||||
- Scene-specific comfort options, culling fixes, replay/spectator classification, and a broader VR
|
- Scene-specific comfort options, culling fixes and replay/spectator classification are future work.
|
||||||
settings UI beyond the current enable/replay controls are future work.
|
- The headset settings panel is drawn into the eye images rather than submitted as its own quad
|
||||||
|
layer, so its text is resampled once more than a compositor layer's would be. It has no laser
|
||||||
|
beam, only the cursor on the panel itself, and text fields cannot be typed into without a keyboard.
|
||||||
- The desktop window remains available as a mirror/fallback.
|
- The desktop window remains available as a mirror/fallback.
|
||||||
|
|
||||||
OpenXR diagnostics are written to the normal run log under
|
OpenXR diagnostics are written to the normal run log under
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ elseif (APPLE)
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (AURORA_ENABLE_GX)
|
if (AURORA_ENABLE_GX)
|
||||||
target_sources(aurora_core PRIVATE lib/imgui.cpp)
|
target_sources(aurora_core PRIVATE lib/imgui.cpp lib/stereo_overlay.cpp)
|
||||||
target_link_libraries(aurora_core PUBLIC imgui)
|
target_link_libraries(aurora_core PUBLIC imgui)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ extern "C" {
|
|||||||
|
|
||||||
ImTextureID aurora_imgui_add_texture(uint32_t width, uint32_t height, const void* rgba8);
|
ImTextureID aurora_imgui_add_texture(uint32_t width, uint32_t height, const void* rgba8);
|
||||||
|
|
||||||
|
// A panel shown only in the headset, drawn by the host with a Dear ImGui context of its own (for
|
||||||
|
// instance a settings menu reachable without the desktop window). It is laid over whatever the eyes
|
||||||
|
// carry: centred on the virtual screen, `widthFraction` of that screen's width, its height following
|
||||||
|
// the draw data's aspect. On a menu frame the screen is the quad the eye image is shown on; in an
|
||||||
|
// immersive race it is the 2D layer's screen (aurora_set_stereo_hud_screen's width and distance,
|
||||||
|
// whether or not the 2D layer is placed on it).
|
||||||
|
//
|
||||||
|
// The draw data belongs to the host's context and is rendered with Aurora's ImGui backend, so its
|
||||||
|
// texture ids must be views that backend can bind (aurora_imgui_add_texture). Call this from the
|
||||||
|
// producer before the frame is sealed, and leave the draw data untouched until the frame worker is
|
||||||
|
// done with that frame (aurora_wait_for_frame_worker). Null hides the panel.
|
||||||
|
void aurora_imgui_set_stereo_overlay(ImDrawData* drawData, float widthFraction);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "stereo.hpp"
|
#include "stereo.hpp"
|
||||||
#include "stereo_mirror.hpp"
|
#include "stereo_mirror.hpp"
|
||||||
#include "stereo_interpolation.hpp"
|
#include "stereo_interpolation.hpp"
|
||||||
|
#include "stereo_overlay.hpp"
|
||||||
#include "webgpu/gpu.hpp"
|
#include "webgpu/gpu.hpp"
|
||||||
#include <webgpu/webgpu_cpp.h>
|
#include <webgpu/webgpu_cpp.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -1613,6 +1614,7 @@ void shutdown() noexcept {
|
|||||||
g_stereoEyeTargets = {};
|
g_stereoEyeTargets = {};
|
||||||
g_stereoMirrorState.Reset();
|
g_stereoMirrorState.Reset();
|
||||||
g_presentationImagePools = {};
|
g_presentationImagePools = {};
|
||||||
|
stereo_overlay::shutdown();
|
||||||
imgui::shutdown();
|
imgui::shutdown();
|
||||||
gfx::shutdown();
|
gfx::shutdown();
|
||||||
webgpu::shutdown();
|
webgpu::shutdown();
|
||||||
@@ -1734,6 +1736,7 @@ struct SealedFrameContext {
|
|||||||
bool replayInterpolatedFrames = false;
|
bool replayInterpolatedFrames = false;
|
||||||
std::optional<AuroraStereoFrame> stereoInput;
|
std::optional<AuroraStereoFrame> stereoInput;
|
||||||
bool retainStereo = false;
|
bool retainStereo = false;
|
||||||
|
imgui::StereoOverlay stereoOverlay;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Worker-owned scene state. A separate buffer generation check protects against
|
// Worker-owned scene state. A separate buffer generation check protects against
|
||||||
@@ -1779,6 +1782,9 @@ void run_retained_stereo_frame(gfx::SealedFrame& sealedFrame) noexcept {
|
|||||||
return;
|
return;
|
||||||
for (uint32_t eye = 0; eye < AURORA_STEREO_EYE_COUNT; ++eye) {
|
for (uint32_t eye = 0; eye < AURORA_STEREO_EYE_COUNT; ++eye) {
|
||||||
gfx::render_stereo_eye(sealedFrame, encoder, replay, eye, false);
|
gfx::render_stereo_eye(sealedFrame, encoder, replay, eye, false);
|
||||||
|
// The panel texture from the last sealed frame; its draw data is not ours to touch here.
|
||||||
|
stereo_overlay::composite_immersive(encoder, g_stereoEyeTargets[eye].output().view, replay.eyes[eye].projection,
|
||||||
|
replay.eyes[eye].viewFromCenter, eye);
|
||||||
}
|
}
|
||||||
const auto sink = run_stereo_sink(encoder, input->frameToken, g_retainedStereo.logicalFrame, input->mode);
|
const auto sink = run_stereo_sink(encoder, input->frameToken, g_retainedStereo.logicalFrame, input->mode);
|
||||||
const auto buffer = encoder.Finish();
|
const auto buffer = encoder.Finish();
|
||||||
@@ -1847,6 +1853,8 @@ void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx, u
|
|||||||
// ImGui draw lists are built once per frame and replayed by each slot's ImGui pass, which is why
|
// ImGui draw lists are built once per frame and replayed by each slot's ImGui pass, which is why
|
||||||
// the next ImGui frame cannot start until the encode phase is done.
|
// the next ImGui frame cannot start until the encode phase is done.
|
||||||
imgui::render_frame_data();
|
imgui::render_frame_data();
|
||||||
|
// The headset panel's draw data follows the same rule on the host's side.
|
||||||
|
ctx.stereoOverlay = imgui::latch_stereo_overlay();
|
||||||
// Drop the sealed frame's lazy RAM-readback requests while the producer is still excluded; it
|
// Drop the sealed frame's lazy RAM-readback requests while the producer is still excluded; it
|
||||||
// starts registering the next frame's as soon as SEALED is published.
|
// starts registering the next frame's as soon as SEALED is published.
|
||||||
gfx::efb_ram::cancel();
|
gfx::efb_ram::cancel();
|
||||||
@@ -1923,6 +1931,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ahead of every other ImGui pass of this frame: the ImGui backend keeps one projection uniform,
|
||||||
|
// and the headset panel's canvas is not the desktop's size, so its pass has to reach the queue
|
||||||
|
// before a desktop pass rewrites that uniform. The eyes below sample the texture it fills.
|
||||||
|
if (const auto panel = stereo_overlay::prepare(stereo_frame_provider_active() ? ctx.stereoOverlay.drawData : nullptr,
|
||||||
|
ctx.stereoOverlay.widthFraction)) {
|
||||||
|
std::lock_guard submitLock(g_queueSubmitMutex);
|
||||||
|
g_queue.Submit(1, &panel);
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.replayInterpolatedFrames) {
|
if (ctx.replayInterpolatedFrames) {
|
||||||
for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; ++interpolatedFrame) {
|
for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; ++interpolatedFrame) {
|
||||||
gfx::render(sealedFrame, encoder, static_cast<int32_t>(interpolatedFrame), false);
|
gfx::render(sealedFrame, encoder, static_cast<int32_t>(interpolatedFrame), false);
|
||||||
@@ -1976,6 +1993,9 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
|||||||
for (uint32_t eye = 0; eye < AURORA_STEREO_EYE_COUNT; ++eye) {
|
for (uint32_t eye = 0; eye < AURORA_STEREO_EYE_COUNT; ++eye) {
|
||||||
gfx::render_stereo_eye(sealedFrame, encoder, *ctx.stereoReplay, eye,
|
gfx::render_stereo_eye(sealedFrame, encoder, *ctx.stereoReplay, eye,
|
||||||
!ctx.retainStereo && eye + 1 == AURORA_STEREO_EYE_COUNT);
|
!ctx.retainStereo && eye + 1 == AURORA_STEREO_EYE_COUNT);
|
||||||
|
stereo_overlay::composite_immersive(encoder, g_stereoEyeTargets[eye].output().view,
|
||||||
|
ctx.stereoReplay->eyes[eye].projection,
|
||||||
|
ctx.stereoReplay->eyes[eye].viewFromCenter, eye);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2003,6 +2023,8 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
|||||||
};
|
};
|
||||||
for (uint32_t eye = 0; eye < AURORA_STEREO_EYE_COUNT; ++eye) {
|
for (uint32_t eye = 0; eye < AURORA_STEREO_EYE_COUNT; ++eye) {
|
||||||
encode_virtual_screen_eye(encoder, completedMono, eye);
|
encode_virtual_screen_eye(encoder, completedMono, eye);
|
||||||
|
const auto& output = g_stereoEyeTargets[eye].output();
|
||||||
|
stereo_overlay::composite_flat(encoder, output.view, output.size, eye);
|
||||||
}
|
}
|
||||||
if (mirrorPlan == MirrorPlan::Black && !headsetOnly) {
|
if (mirrorPlan == MirrorPlan::Black && !headsetOnly) {
|
||||||
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, MirrorPlan::Black);
|
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, MirrorPlan::Black);
|
||||||
|
|||||||
@@ -238,6 +238,10 @@ void set_stereo_hud_screen(bool enabled, float width, float distance) noexcept {
|
|||||||
g_stereoHudScreenEnabled.store(enabled, std::memory_order_relaxed);
|
g_stereoHudScreenEnabled.store(enabled, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
bool get_stereo_hud_screen_enabled() noexcept { return g_stereoHudScreenEnabled.load(std::memory_order_relaxed); }
|
bool get_stereo_hud_screen_enabled() noexcept { return g_stereoHudScreenEnabled.load(std::memory_order_relaxed); }
|
||||||
|
void get_stereo_hud_screen_size(float& width, float& distance) noexcept {
|
||||||
|
width = g_stereoHudScreenWidth.load(std::memory_order_relaxed);
|
||||||
|
distance = g_stereoHudScreenDistance.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
// The desktop mirror choice. Normal is the ordinary mono presentation, so a
|
// The desktop mirror choice. Normal is the ordinary mono presentation, so a
|
||||||
// build that never touches this setting presents exactly as it did before.
|
// build that never touches this setting presents exactly as it did before.
|
||||||
|
|||||||
@@ -400,6 +400,8 @@ bool get_stereo_skip_copy_clears() noexcept;
|
|||||||
// the game's presented aspect ratio. Also live.
|
// the game's presented aspect ratio. Also live.
|
||||||
void set_stereo_hud_screen(bool enabled, float width, float distance) noexcept;
|
void set_stereo_hud_screen(bool enabled, float width, float distance) noexcept;
|
||||||
bool get_stereo_hud_screen_enabled() noexcept;
|
bool get_stereo_hud_screen_enabled() noexcept;
|
||||||
|
// The screen's width and distance in world units, kept while the 2D layer is off it.
|
||||||
|
void get_stereo_hud_screen_size(float& width, float& distance) noexcept;
|
||||||
|
|
||||||
// What the desktop window presents while a stereo provider is feeding a
|
// What the desktop window presents while a stereo provider is feeding a
|
||||||
// headset. Live, and read once per presentation group by the frame worker.
|
// headset. Live, and read once per presentation group by the frame worker.
|
||||||
|
|||||||
@@ -306,4 +306,62 @@ inline Mat4x4<float> compose_hud_screen_projection(const Mat4x4<float>& eyeFrust
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The headset settings panel (aurora_imgui_set_stereo_overlay): a rectangle
|
||||||
|
// centred on the virtual screen, in the same world units as the screen.
|
||||||
|
struct OverlayPanel {
|
||||||
|
float halfWidth = 0.0f;
|
||||||
|
float halfHeight = 0.0f;
|
||||||
|
float distance = 0.0f;
|
||||||
|
|
||||||
|
[[nodiscard]] bool valid() const noexcept { return halfWidth > 0.0f && halfHeight > 0.0f && distance > 0.0f; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// `widthFraction` of a virtual screen `screenWidth` across and `screenDistance`
|
||||||
|
// ahead, its height following the panel's own aspect.
|
||||||
|
inline OverlayPanel overlay_panel_on_screen(float screenWidth, float screenDistance, float widthFraction,
|
||||||
|
float panelAspect) noexcept {
|
||||||
|
if (!(panelAspect > 0.0f)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const float halfWidth = 0.5f * screenWidth * widthFraction;
|
||||||
|
return {.halfWidth = halfWidth, .halfHeight = halfWidth / panelAspect, .distance = screenDistance};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip position of a point on the panel for one eye of an immersive frame, as a
|
||||||
|
// matrix applied to (x, y, 0, 1) with x and y running -1..1 across the panel,
|
||||||
|
// +y up. It is the 2D layer's chain with the panel's corners in place of a
|
||||||
|
// game draw's NDC, so the panel sits exactly where the race HUD's screen does.
|
||||||
|
// The panel is drawn over the finished eye with no depth test, so its depth is
|
||||||
|
// simply parked mid-volume.
|
||||||
|
inline Mat4x4<float> compose_overlay_panel_projection(const Mat4x4<float>& eyeFrustum,
|
||||||
|
const Mat3x4<float>& viewFromCenter,
|
||||||
|
const OverlayPanel& panel) noexcept {
|
||||||
|
Mat4x4<float> corners{};
|
||||||
|
corners.m0 = {1.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
corners.m1 = {0.0f, 1.0f, 0.0f, 0.0f};
|
||||||
|
corners.m3 = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
const HudScreen screen{.halfWidth = panel.halfWidth, .halfHeight = panel.halfHeight, .distance = panel.distance};
|
||||||
|
auto out = compose_hud_screen_projection(eyeFrustum, viewFromCenter, screen, corners);
|
||||||
|
for (size_t i = 0; i < 4; ++i) {
|
||||||
|
out.m2[i] = 0.5f * out.m3[i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same panel on a virtual-screen eye image, which the runtime shows as a
|
||||||
|
// quad layer the screen's width across: a centred rectangle `widthFraction` of
|
||||||
|
// the image's width, with the panel's aspect.
|
||||||
|
inline Mat4x4<float> overlay_panel_flat_projection(float widthFraction, float panelAspect,
|
||||||
|
float imageAspect) noexcept {
|
||||||
|
Mat4x4<float> out{};
|
||||||
|
if (!(panelAspect > 0.0f) || !(imageAspect > 0.0f)) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
out.m0 = {widthFraction, 0.0f, 0.0f, 0.0f};
|
||||||
|
out.m1 = {0.0f, widthFraction * imageAspect / panelAspect, 0.0f, 0.0f};
|
||||||
|
out.m2 = {0.0f, 0.0f, 0.0f, 0.5f};
|
||||||
|
out.m3 = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace aurora::gfx::stereo_replay
|
} // namespace aurora::gfx::stereo_replay
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -32,6 +33,10 @@ static bool g_frameDataBuilt = false;
|
|||||||
static std::vector<SDL_Texture*> g_sdlTextures;
|
static std::vector<SDL_Texture*> g_sdlTextures;
|
||||||
static std::vector<wgpu::Texture> g_wgpuTextures;
|
static std::vector<wgpu::Texture> g_wgpuTextures;
|
||||||
|
|
||||||
|
// Set by the producer, latched by the seal.
|
||||||
|
static std::mutex g_stereoOverlayMutex;
|
||||||
|
static StereoOverlay g_stereoOverlay;
|
||||||
|
|
||||||
void remove_legacy_ini_file(const char* basePath) noexcept {
|
void remove_legacy_ini_file(const char* basePath) noexcept {
|
||||||
if (basePath == nullptr || *basePath == '\0') {
|
if (basePath == nullptr || *basePath == '\0') {
|
||||||
return;
|
return;
|
||||||
@@ -200,6 +205,23 @@ void render(const wgpu::RenderPassEncoder& pass) noexcept {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StereoOverlay latch_stereo_overlay() noexcept {
|
||||||
|
std::lock_guard lock(g_stereoOverlayMutex);
|
||||||
|
return g_stereoOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool render_draw_data(const wgpu::RenderPassEncoder& pass, ImDrawData* data) noexcept {
|
||||||
|
ZoneScoped;
|
||||||
|
// The SDL renderer fallback has no render passes to draw into.
|
||||||
|
if (g_useSdlRenderer || data == nullptr || ImGui::GetCurrentContext() == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pass.PushDebugGroup("Aurora: Dear Imgui headset panel");
|
||||||
|
ImGui_ImplWGPU_RenderDrawData(data, pass.Get());
|
||||||
|
pass.PopDebugGroup();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ImTextureID add_texture(uint32_t width, uint32_t height, const uint8_t* data) noexcept {
|
ImTextureID add_texture(uint32_t width, uint32_t height, const uint8_t* data) noexcept {
|
||||||
if (SDL_Renderer* renderer = window::get_sdl_renderer()) {
|
if (SDL_Renderer* renderer = window::get_sdl_renderer()) {
|
||||||
SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, width, height);
|
SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, width, height);
|
||||||
@@ -251,4 +273,12 @@ extern "C" {
|
|||||||
ImTextureID aurora_imgui_add_texture(uint32_t width, uint32_t height, const void* rgba8) {
|
ImTextureID aurora_imgui_add_texture(uint32_t width, uint32_t height, const void* rgba8) {
|
||||||
return aurora::imgui::add_texture(width, height, static_cast<const uint8_t*>(rgba8));
|
return aurora::imgui::add_texture(width, height, static_cast<const uint8_t*>(rgba8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void aurora_imgui_set_stereo_overlay(ImDrawData* drawData, float widthFraction) {
|
||||||
|
std::lock_guard lock(aurora::imgui::g_stereoOverlayMutex);
|
||||||
|
aurora::imgui::g_stereoOverlay = {
|
||||||
|
.drawData = drawData,
|
||||||
|
.widthFraction = drawData != nullptr ? widthFraction : 0.f,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <aurora/event.h>
|
#include <aurora/event.h>
|
||||||
|
|
||||||
union SDL_Event;
|
union SDL_Event;
|
||||||
|
struct ImDrawData;
|
||||||
|
|
||||||
namespace wgpu {
|
namespace wgpu {
|
||||||
class RenderPassEncoder;
|
class RenderPassEncoder;
|
||||||
@@ -20,4 +21,15 @@ void new_frame(const AuroraWindowSize& size) noexcept;
|
|||||||
// once and every presentation slot replays them. Reset by new_frame.
|
// once and every presentation slot replays them. Reset by new_frame.
|
||||||
void render_frame_data() noexcept;
|
void render_frame_data() noexcept;
|
||||||
void render(const wgpu::RenderPassEncoder& pass) noexcept;
|
void render(const wgpu::RenderPassEncoder& pass) noexcept;
|
||||||
|
|
||||||
|
// The headset panel as aurora_imgui_set_stereo_overlay last set it.
|
||||||
|
struct StereoOverlay {
|
||||||
|
ImDrawData* drawData = nullptr;
|
||||||
|
float widthFraction = 0.f;
|
||||||
|
};
|
||||||
|
StereoOverlay latch_stereo_overlay() noexcept;
|
||||||
|
// Renders another context's draw data with this context's backend. The backend keeps one projection
|
||||||
|
// uniform for every pass, so a pass whose display size differs from the desktop's must be submitted
|
||||||
|
// before the next pass is recorded.
|
||||||
|
bool render_draw_data(const wgpu::RenderPassEncoder& pass, ImDrawData* data) noexcept;
|
||||||
} // namespace aurora::imgui
|
} // namespace aurora::imgui
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
#include "stereo_overlay.hpp"
|
||||||
|
|
||||||
|
#include "gfx/common.hpp"
|
||||||
|
#include "gfx/stereo_replay.hpp"
|
||||||
|
#include "imgui.hpp"
|
||||||
|
#include "webgpu/gpu.hpp"
|
||||||
|
|
||||||
|
#include <aurora/aurora.h>
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include "tracy/Tracy.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace aurora::stereo_overlay {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using webgpu::g_device;
|
||||||
|
using webgpu::g_queue;
|
||||||
|
|
||||||
|
// Uploaded as-is into a WGSL mat4x4<f32>, like the GX uniforms' matrices.
|
||||||
|
static_assert(sizeof(Mat4x4<float>) == 64);
|
||||||
|
|
||||||
|
// The panel's corners, (-1, 1) top left to (1, -1) bottom right, carried through
|
||||||
|
// one clip-from-panel matrix per eye. UVs are interpolated perspective-correct,
|
||||||
|
// so the texture stays straight on a panel seen at an angle. The texture holds
|
||||||
|
// premultiplied colour, which is what ImGui's blending leaves in a cleared target.
|
||||||
|
constexpr const char* kShader = R"""(
|
||||||
|
struct Panel {
|
||||||
|
clip_from_panel: mat4x4<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> panel: Panel;
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var panel_sampler: sampler;
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var panel_texture: texture_2d<f32>;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) pos: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
var<private> corners: array<vec2<f32>, 4> = array<vec2<f32>, 4>(
|
||||||
|
vec2(-1.0, 1.0),
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2(1.0, 1.0),
|
||||||
|
vec2(1.0, -1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput {
|
||||||
|
let corner = corners[vtxIdx];
|
||||||
|
var out: VertexOutput;
|
||||||
|
// Row-vector convention, like the GX shaders: m0..m3 are the clip x/y/z/w rows.
|
||||||
|
out.pos = vec4<f32>(corner, 0.0, 1.0) * panel.clip_from_panel;
|
||||||
|
out.uv = vec2<f32>(0.5 + 0.5 * corner.x, 0.5 - 0.5 * corner.y);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return textureSample(panel_texture, panel_sampler, in.uv);
|
||||||
|
}
|
||||||
|
)""";
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
webgpu::TextureWithSampler panel;
|
||||||
|
wgpu::RenderPipeline pipeline;
|
||||||
|
wgpu::BindGroupLayout bindGroupLayout;
|
||||||
|
wgpu::TextureFormat pipelineFormat = wgpu::TextureFormat::Undefined;
|
||||||
|
std::array<wgpu::Buffer, AURORA_STEREO_EYE_COUNT> uniforms;
|
||||||
|
std::array<wgpu::BindGroup, AURORA_STEREO_EYE_COUNT> bindGroups;
|
||||||
|
float widthFraction = 0.f;
|
||||||
|
bool visible = false;
|
||||||
|
};
|
||||||
|
State g_state;
|
||||||
|
|
||||||
|
bool ensure_pipeline() {
|
||||||
|
auto& state = g_state;
|
||||||
|
const auto format = webgpu::g_graphicsConfig.surfaceConfiguration.format;
|
||||||
|
if (state.pipeline && state.pipelineFormat == format) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
state.pipeline = {};
|
||||||
|
state.bindGroups = {};
|
||||||
|
|
||||||
|
wgpu::ShaderSourceWGSL source{};
|
||||||
|
source.code = kShader;
|
||||||
|
const wgpu::ShaderModuleDescriptor moduleDescriptor{
|
||||||
|
.nextInChain = &source,
|
||||||
|
.label = "Headset panel module",
|
||||||
|
};
|
||||||
|
const auto module = g_device.CreateShaderModule(&moduleDescriptor);
|
||||||
|
|
||||||
|
const std::array layoutEntries{
|
||||||
|
wgpu::BindGroupLayoutEntry{
|
||||||
|
.binding = 0,
|
||||||
|
.visibility = wgpu::ShaderStage::Vertex,
|
||||||
|
.buffer =
|
||||||
|
wgpu::BufferBindingLayout{
|
||||||
|
.type = wgpu::BufferBindingType::Uniform,
|
||||||
|
.minBindingSize = sizeof(Mat4x4<float>),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry{
|
||||||
|
.binding = 1,
|
||||||
|
.visibility = wgpu::ShaderStage::Fragment,
|
||||||
|
.sampler =
|
||||||
|
wgpu::SamplerBindingLayout{
|
||||||
|
.type = wgpu::SamplerBindingType::Filtering,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry{
|
||||||
|
.binding = 2,
|
||||||
|
.visibility = wgpu::ShaderStage::Fragment,
|
||||||
|
.texture =
|
||||||
|
wgpu::TextureBindingLayout{
|
||||||
|
.sampleType = wgpu::TextureSampleType::Float,
|
||||||
|
.viewDimension = wgpu::TextureViewDimension::e2D,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wgpu::BindGroupLayoutDescriptor layoutDescriptor{
|
||||||
|
.label = "Headset panel bind group layout",
|
||||||
|
.entryCount = layoutEntries.size(),
|
||||||
|
.entries = layoutEntries.data(),
|
||||||
|
};
|
||||||
|
state.bindGroupLayout = g_device.CreateBindGroupLayout(&layoutDescriptor);
|
||||||
|
const wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor{
|
||||||
|
.label = "Headset panel pipeline layout",
|
||||||
|
.bindGroupLayoutCount = 1,
|
||||||
|
.bindGroupLayouts = &state.bindGroupLayout,
|
||||||
|
};
|
||||||
|
const auto pipelineLayout = g_device.CreatePipelineLayout(&pipelineLayoutDescriptor);
|
||||||
|
|
||||||
|
constexpr wgpu::BlendComponent kPremultipliedOver{
|
||||||
|
.operation = wgpu::BlendOperation::Add,
|
||||||
|
.srcFactor = wgpu::BlendFactor::One,
|
||||||
|
.dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||||
|
};
|
||||||
|
const wgpu::BlendState blend{
|
||||||
|
.color = kPremultipliedOver,
|
||||||
|
.alpha = kPremultipliedOver,
|
||||||
|
};
|
||||||
|
const std::array colorTargets{wgpu::ColorTargetState{
|
||||||
|
.format = format,
|
||||||
|
.blend = &blend,
|
||||||
|
.writeMask = wgpu::ColorWriteMask::All,
|
||||||
|
}};
|
||||||
|
const wgpu::FragmentState fragmentState{
|
||||||
|
.module = module,
|
||||||
|
.entryPoint = "fs_main",
|
||||||
|
.targetCount = colorTargets.size(),
|
||||||
|
.targets = colorTargets.data(),
|
||||||
|
};
|
||||||
|
const wgpu::RenderPipelineDescriptor pipelineDescriptor{
|
||||||
|
.label = "Headset panel pipeline",
|
||||||
|
.layout = pipelineLayout,
|
||||||
|
.vertex =
|
||||||
|
wgpu::VertexState{
|
||||||
|
.module = module,
|
||||||
|
.entryPoint = "vs_main",
|
||||||
|
},
|
||||||
|
.primitive =
|
||||||
|
wgpu::PrimitiveState{
|
||||||
|
.topology = wgpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
.cullMode = wgpu::CullMode::None,
|
||||||
|
},
|
||||||
|
.multisample =
|
||||||
|
wgpu::MultisampleState{
|
||||||
|
.count = 1,
|
||||||
|
.mask = UINT32_MAX,
|
||||||
|
},
|
||||||
|
.fragment = &fragmentState,
|
||||||
|
};
|
||||||
|
state.pipeline = g_device.CreateRenderPipeline(&pipelineDescriptor);
|
||||||
|
if (!state.pipeline) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (auto& uniform : state.uniforms) {
|
||||||
|
if (!uniform) {
|
||||||
|
const wgpu::BufferDescriptor bufferDescriptor{
|
||||||
|
.label = "Headset panel uniform",
|
||||||
|
.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst,
|
||||||
|
.size = sizeof(Mat4x4<float>),
|
||||||
|
};
|
||||||
|
uniform = g_device.CreateBuffer(&bufferDescriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.pipelineFormat = format;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void composite(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& target, const Mat4x4<float>& clipFromPanel,
|
||||||
|
uint32_t eyeIndex) noexcept {
|
||||||
|
auto& state = g_state;
|
||||||
|
if (!state.visible || !state.pipeline || eyeIndex >= AURORA_STEREO_EYE_COUNT || !target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& bindGroup = state.bindGroups[eyeIndex];
|
||||||
|
if (!bindGroup) {
|
||||||
|
const std::array entries{
|
||||||
|
wgpu::BindGroupEntry{
|
||||||
|
.binding = 0,
|
||||||
|
.buffer = state.uniforms[eyeIndex],
|
||||||
|
.size = sizeof(Mat4x4<float>),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry{
|
||||||
|
.binding = 1,
|
||||||
|
.sampler = state.panel.sampler,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry{
|
||||||
|
.binding = 2,
|
||||||
|
.textureView = state.panel.view,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wgpu::BindGroupDescriptor descriptor{
|
||||||
|
.label = "Headset panel bind group",
|
||||||
|
.layout = state.bindGroupLayout,
|
||||||
|
.entryCount = entries.size(),
|
||||||
|
.entries = entries.data(),
|
||||||
|
};
|
||||||
|
bindGroup = g_device.CreateBindGroup(&descriptor);
|
||||||
|
}
|
||||||
|
// Each eye has its own uniform, and every pass that reads it is submitted
|
||||||
|
// before this worker writes it again.
|
||||||
|
g_queue.WriteBuffer(state.uniforms[eyeIndex], 0, &clipFromPanel, sizeof(clipFromPanel));
|
||||||
|
|
||||||
|
const std::array attachments{
|
||||||
|
wgpu::RenderPassColorAttachment{
|
||||||
|
.view = target,
|
||||||
|
.loadOp = wgpu::LoadOp::Load,
|
||||||
|
.storeOp = wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wgpu::RenderPassDescriptor descriptor{
|
||||||
|
.label = eyeIndex == 0 ? "Headset panel left eye" : "Headset panel right eye",
|
||||||
|
.colorAttachmentCount = attachments.size(),
|
||||||
|
.colorAttachments = attachments.data(),
|
||||||
|
};
|
||||||
|
const auto pass = encoder.BeginRenderPass(&descriptor);
|
||||||
|
pass.SetPipeline(state.pipeline);
|
||||||
|
pass.SetBindGroup(0, bindGroup, 0, nullptr);
|
||||||
|
pass.Draw(4);
|
||||||
|
pass.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
float panel_aspect() noexcept {
|
||||||
|
const auto& size = g_state.panel.size;
|
||||||
|
return size.height != 0 ? static_cast<float>(size.width) / static_cast<float>(size.height) : 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
wgpu::CommandBuffer prepare(ImDrawData* drawData, float widthFraction) noexcept {
|
||||||
|
ZoneScoped;
|
||||||
|
auto& state = g_state;
|
||||||
|
state.visible = false;
|
||||||
|
if (drawData == nullptr || !(widthFraction > 0.f)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto width = static_cast<uint32_t>(std::lround(drawData->DisplaySize.x * drawData->FramebufferScale.x));
|
||||||
|
const auto height = static_cast<uint32_t>(std::lround(drawData->DisplaySize.y * drawData->FramebufferScale.y));
|
||||||
|
if (width == 0 || height == 0 || !ensure_pipeline()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto format = webgpu::g_graphicsConfig.surfaceConfiguration.format;
|
||||||
|
if (!state.panel.texture || state.panel.size.width != width || state.panel.size.height != height ||
|
||||||
|
state.panel.format != format) {
|
||||||
|
state.panel = webgpu::create_render_texture(width, height, false);
|
||||||
|
state.bindGroups = {};
|
||||||
|
}
|
||||||
|
// The ImGui backend sets its viewport from the draw data, so a texture the
|
||||||
|
// device clamped to a smaller size cannot hold the pass.
|
||||||
|
if (state.panel.size.width != width || state.panel.size.height != height) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const wgpu::CommandEncoderDescriptor encoderDescriptor{
|
||||||
|
.label = "Headset panel encoder",
|
||||||
|
};
|
||||||
|
auto encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
|
||||||
|
const std::array attachments{
|
||||||
|
wgpu::RenderPassColorAttachment{
|
||||||
|
.view = state.panel.view,
|
||||||
|
.loadOp = wgpu::LoadOp::Clear,
|
||||||
|
.storeOp = wgpu::StoreOp::Store,
|
||||||
|
.clearValue = {.r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wgpu::RenderPassDescriptor passDescriptor{
|
||||||
|
.label = "Headset panel ImGui pass",
|
||||||
|
.colorAttachmentCount = attachments.size(),
|
||||||
|
.colorAttachments = attachments.data(),
|
||||||
|
};
|
||||||
|
bool drawn = false;
|
||||||
|
{
|
||||||
|
const auto pass = encoder.BeginRenderPass(&passDescriptor);
|
||||||
|
drawn = imgui::render_draw_data(pass, drawData);
|
||||||
|
pass.End();
|
||||||
|
}
|
||||||
|
if (!drawn) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
state.widthFraction = widthFraction;
|
||||||
|
state.visible = true;
|
||||||
|
return encoder.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void composite_immersive(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& eye,
|
||||||
|
const Mat4x4<float>& eyeFrustum, const Mat3x4<float>& viewFromCenter,
|
||||||
|
uint32_t eyeIndex) noexcept {
|
||||||
|
if (!g_state.visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float screenWidth = 0.f;
|
||||||
|
float screenDistance = 0.f;
|
||||||
|
gfx::get_stereo_hud_screen_size(screenWidth, screenDistance);
|
||||||
|
const auto panel = gfx::stereo_replay::overlay_panel_on_screen(screenWidth, screenDistance, g_state.widthFraction,
|
||||||
|
panel_aspect());
|
||||||
|
if (!panel.valid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
composite(encoder, eye, gfx::stereo_replay::compose_overlay_panel_projection(eyeFrustum, viewFromCenter, panel),
|
||||||
|
eyeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void composite_flat(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& eye, const wgpu::Extent3D& size,
|
||||||
|
uint32_t eyeIndex) noexcept {
|
||||||
|
if (!g_state.visible || size.width == 0 || size.height == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float imageAspect = static_cast<float>(size.width) / static_cast<float>(size.height);
|
||||||
|
composite(encoder, eye,
|
||||||
|
gfx::stereo_replay::overlay_panel_flat_projection(g_state.widthFraction, panel_aspect(), imageAspect),
|
||||||
|
eyeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdown() noexcept { g_state = {}; }
|
||||||
|
|
||||||
|
} // namespace aurora::stereo_overlay
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <aurora/math.hpp>
|
||||||
|
#include <webgpu/webgpu_cpp.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
struct ImDrawData;
|
||||||
|
|
||||||
|
// The headset-only panel of aurora_imgui_set_stereo_overlay. Everything here
|
||||||
|
// belongs to the frame worker, like the eye targets it draws into.
|
||||||
|
namespace aurora::stereo_overlay {
|
||||||
|
|
||||||
|
// Renders the panel's draw data into the panel texture and returns the command
|
||||||
|
// buffer holding that pass, which the caller submits before recording any other
|
||||||
|
// ImGui pass: the ImGui backend's projection uniform is shared by every pass, and
|
||||||
|
// the panel's canvas is not the desktop's size. Null draw data hides the panel,
|
||||||
|
// and so does any failure; both return a null command buffer.
|
||||||
|
wgpu::CommandBuffer prepare(ImDrawData* drawData, float widthFraction) noexcept;
|
||||||
|
|
||||||
|
// Draws the most recently prepared panel over a finished immersive eye, on the
|
||||||
|
// 2D layer's virtual screen seen through that eye's frustum and view.
|
||||||
|
void composite_immersive(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& eye,
|
||||||
|
const Mat4x4<float>& eyeFrustum, const Mat3x4<float>& viewFromCenter,
|
||||||
|
uint32_t eyeIndex) noexcept;
|
||||||
|
|
||||||
|
// Draws it over a virtual-screen eye image, which is shown flat as a quad.
|
||||||
|
void composite_flat(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& eye, const wgpu::Extent3D& size,
|
||||||
|
uint32_t eyeIndex) noexcept;
|
||||||
|
|
||||||
|
void shutdown() noexcept;
|
||||||
|
|
||||||
|
} // namespace aurora::stereo_overlay
|
||||||
@@ -245,6 +245,70 @@ TEST(StereoReplayTest, HudScreenParksRasterDepthAtMidrangeUnderHeadMotion) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(StereoReplayTest, OverlayPanelIsCentredOnTheVirtualScreen) {
|
||||||
|
// Three quarters of a 1200-unit screen, 1000 ahead, with a 4:3 panel.
|
||||||
|
const auto panel = overlay_panel_on_screen(1200.0f, 1000.0f, 0.75f, 4.0f / 3.0f);
|
||||||
|
EXPECT_FLOAT_EQ(panel.halfWidth, 450.0f);
|
||||||
|
EXPECT_FLOAT_EQ(panel.halfHeight, 337.5f);
|
||||||
|
EXPECT_FLOAT_EQ(panel.distance, 1000.0f);
|
||||||
|
EXPECT_TRUE(panel.valid());
|
||||||
|
EXPECT_FALSE(overlay_panel_on_screen(1200.0f, 1000.0f, 0.75f, 0.0f).valid());
|
||||||
|
EXPECT_FALSE(overlay_panel_on_screen(1200.0f, 0.0f, 0.75f, 4.0f / 3.0f).valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StereoReplayTest, OverlayPanelCornersFollowTheEyeChain) {
|
||||||
|
Mat4x4<float> eyeFrustum{};
|
||||||
|
eyeFrustum.m0 = {1.15f, 0.0f, 0.08f, 0.0f};
|
||||||
|
eyeFrustum.m1 = {0.0f, 1.02f, -0.03f, 0.0f};
|
||||||
|
const float angle = 0.3f;
|
||||||
|
const float c = std::cos(angle);
|
||||||
|
const float s = std::sin(angle);
|
||||||
|
Mat3x4<float> viewFromCenter{};
|
||||||
|
viewFromCenter.m0 = {c, 0.0f, s, 15.0f};
|
||||||
|
viewFromCenter.m1 = {0.0f, 1.0f, 0.0f, -4.0f};
|
||||||
|
viewFromCenter.m2 = {-s, 0.0f, c, 7.0f};
|
||||||
|
|
||||||
|
const OverlayPanel panel{.halfWidth = 450.0f, .halfHeight = 337.5f, .distance = 1000.0f};
|
||||||
|
const auto composed = compose_overlay_panel_projection(eyeFrustum, viewFromCenter, panel);
|
||||||
|
|
||||||
|
const std::array<Vec4<float>, 5> corners{{
|
||||||
|
{-1.0f, 1.0f, 0.0f, 1.0f},
|
||||||
|
{1.0f, 1.0f, 0.0f, 1.0f},
|
||||||
|
{-1.0f, -1.0f, 0.0f, 1.0f},
|
||||||
|
{1.0f, -1.0f, 0.0f, 1.0f},
|
||||||
|
{0.0f, 0.0f, 0.0f, 1.0f},
|
||||||
|
}};
|
||||||
|
for (const auto& corner : corners) {
|
||||||
|
// Top left is (-1, +1): the panel's +y is up, like the screen it sits on.
|
||||||
|
const Vec4<float> centerPoint{corner[0] * panel.halfWidth, corner[1] * panel.halfHeight, -panel.distance, 1.0f};
|
||||||
|
const float eyeX = dot4(viewFromCenter.m0, centerPoint);
|
||||||
|
const float eyeY = dot4(viewFromCenter.m1, centerPoint);
|
||||||
|
const float eyeZ = dot4(viewFromCenter.m2, centerPoint);
|
||||||
|
const float w = dot4(composed.m3, corner);
|
||||||
|
EXPECT_NEAR(dot4(composed.m0, corner), eyeFrustum.m0[0] * eyeX + eyeFrustum.m0[2] * eyeZ, 1e-2f);
|
||||||
|
EXPECT_NEAR(dot4(composed.m1, corner), eyeFrustum.m1[1] * eyeY + eyeFrustum.m1[2] * eyeZ, 1e-2f);
|
||||||
|
EXPECT_NEAR(w, -eyeZ, 1e-2f);
|
||||||
|
ASSERT_GT(w, 0.0f);
|
||||||
|
EXPECT_NEAR(dot4(composed.m2, corner) / w, 0.5f, 1e-5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StereoReplayTest, OverlayPanelOnAFlatEyeKeepsItsAspect) {
|
||||||
|
// A 4:3 panel three quarters across a 2064x2208 eye image.
|
||||||
|
const float imageAspect = 2064.0f / 2208.0f;
|
||||||
|
const auto flat = overlay_panel_flat_projection(0.75f, 4.0f / 3.0f, imageAspect);
|
||||||
|
const Vec4<float> topRight{1.0f, 1.0f, 0.0f, 1.0f};
|
||||||
|
const float ndcX = dot4(flat.m0, topRight) / dot4(flat.m3, topRight);
|
||||||
|
const float ndcY = dot4(flat.m1, topRight) / dot4(flat.m3, topRight);
|
||||||
|
EXPECT_FLOAT_EQ(ndcX, 0.75f);
|
||||||
|
// In pixels: 0.75 * 2064 wide over ndcY * 2208 tall is the panel's 4:3.
|
||||||
|
EXPECT_NEAR((ndcX * 2064.0f) / (ndcY * 2208.0f), 4.0f / 3.0f, 1e-4f);
|
||||||
|
EXPECT_FLOAT_EQ(dot4(flat.m2, topRight), 0.5f);
|
||||||
|
const Vec4<float> centre{0.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
EXPECT_FLOAT_EQ(dot4(flat.m0, centre), 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(dot4(flat.m1, centre), 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
Mat3x4<float> identity3x4() {
|
Mat3x4<float> identity3x4() {
|
||||||
Mat3x4<float> m{};
|
Mat3x4<float> m{};
|
||||||
m.m0 = {1.0f, 0.0f, 0.0f, 0.0f};
|
m.m0 = {1.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
|||||||
+1
-1
@@ -256,7 +256,7 @@ the app:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `debug.wiicompiled.vtxpad 0` | Turns the stride padding off, to re-check a driver update |
|
| `debug.wiicompiled.vtxpad 0` | Turns the stride padding off, to re-check a driver update |
|
||||||
| `debug.wiicompiled.validation 1` | Keeps WebGPU validation and robustness on in release builds |
|
| `debug.wiicompiled.validation 1` | Keeps WebGPU validation and robustness on in release builds |
|
||||||
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist |
|
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist. `panel` clicks both thumbsticks, opening or closing the settings panel (see `OPENXR.md`) |
|
||||||
| `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s. The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%` and app GPU time (`App=`) |
|
| `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s. The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%` and app GPU time (`App=`) |
|
||||||
|
|
||||||
The injector makes headset tests possible with nobody wearing the headset.
|
The injector makes headset tests possible with nobody wearing the headset.
|
||||||
|
|||||||
@@ -404,6 +404,14 @@ target_include_directories(mkw_vr_wii_remote_tests PRIVATE "${CMAKE_CURRENT_LIST
|
|||||||
target_compile_features(mkw_vr_wii_remote_tests PRIVATE cxx_std_17)
|
target_compile_features(mkw_vr_wii_remote_tests PRIVATE cxx_std_17)
|
||||||
add_test(NAME mkw_vr_wii_remote_tests COMMAND mkw_vr_wii_remote_tests)
|
add_test(NAME mkw_vr_wii_remote_tests COMMAND mkw_vr_wii_remote_tests)
|
||||||
|
|
||||||
|
# The in-headset settings panel's controller chord, release latch, selection,
|
||||||
|
# scrolling and canvas mapping, plus the thread bridge they publish through.
|
||||||
|
add_executable(mkw_vr_settings_panel_tests tests/vr_settings_panel_tests.cpp src/vr/openxr_settings_panel.cpp)
|
||||||
|
target_include_directories(mkw_vr_settings_panel_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||||
|
target_compile_features(mkw_vr_settings_panel_tests PRIVATE cxx_std_17)
|
||||||
|
set_target_properties(mkw_vr_settings_panel_tests PROPERTIES UNITY_BUILD OFF)
|
||||||
|
add_test(NAME mkw_vr_settings_panel_tests COMMAND mkw_vr_settings_panel_tests)
|
||||||
|
|
||||||
# Resolve the real local-kart pointer walk against synthetic offline/online rosters.
|
# Resolve the real local-kart pointer walk against synthetic offline/online rosters.
|
||||||
add_executable(mkw_vr_player_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_player_tests.cpp")
|
add_executable(mkw_vr_player_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_player_tests.cpp")
|
||||||
target_include_directories(mkw_vr_player_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
target_include_directories(mkw_vr_player_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#if defined(MKW_ENABLE_OPENXR)
|
#if defined(MKW_ENABLE_OPENXR)
|
||||||
|
|
||||||
#include "vr/openxr_runtime.h"
|
#include "vr/openxr_runtime.h"
|
||||||
|
#include "vr/openxr_settings_panel.h"
|
||||||
#include "vr/openxr_wii_remote.h"
|
#include "vr/openxr_wii_remote.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -54,6 +55,11 @@ struct OpenXRPointerScreen {
|
|||||||
// Both are bound for the Oculus Touch profile; khr/simple_controller gets
|
// Both are bound for the Oculus Touch profile; khr/simple_controller gets
|
||||||
// select/menu and the poses so an unknown runtime still offers something.
|
// select/menu and the poses so an unknown runtime still offers something.
|
||||||
//
|
//
|
||||||
|
// Clicking both thumbsticks opens the in-headset settings panel
|
||||||
|
// (openxr_settings_panel.h). While it is open, and until every button has been
|
||||||
|
// released after it closes, the game sees idle controllers: the chord, the
|
||||||
|
// pointer and the triggers belong to the panel.
|
||||||
|
//
|
||||||
// Lifetime: Create after the session exists (attaches the action set, which
|
// Lifetime: Create after the session exists (attaches the action set, which
|
||||||
// OpenXR permits once per session), Sync once per xrWaitFrame, Idle while the
|
// OpenXR permits once per session), Sync once per xrWaitFrame, Idle while the
|
||||||
// session is not running, Destroy before the session is destroyed. All of them
|
// session is not running, Destroy before the session is destroyed. All of them
|
||||||
@@ -75,8 +81,10 @@ public:
|
|||||||
|
|
||||||
// xrSyncActions + state reads, then publishes to the virtual gamepad and
|
// xrSyncActions + state reads, then publishes to the virtual gamepad and
|
||||||
// the Wii Remote bridge. predicted_display_time is the frame's XrTime;
|
// the Wii Remote bridge. predicted_display_time is the frame's XrTime;
|
||||||
// screen is where the pointer can land this frame.
|
// screen is where the Wii Remote pointer can land this frame, and
|
||||||
void Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen);
|
// settings_panel where the settings panel is (its whole rectangle).
|
||||||
|
void Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
|
||||||
|
const OpenXRPointerScreen& settings_panel);
|
||||||
|
|
||||||
// Publishes a remote with nothing held, at rest and not pointing, and stops
|
// Publishes a remote with nothing held, at rest and not pointing, and stops
|
||||||
// the haptics, for frames without focused input.
|
// the haptics, for frames without focused input.
|
||||||
@@ -100,8 +108,13 @@ private:
|
|||||||
XrTime InputSampleTime(XrTime predicted_display_time) const;
|
XrTime InputSampleTime(XrTime predicted_display_time) const;
|
||||||
bool AttachVirtualGamepad();
|
bool AttachVirtualGamepad();
|
||||||
void DetachVirtualGamepad();
|
void DetachVirtualGamepad();
|
||||||
void PublishWiiRemote(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
|
// `withheld` publishes a remote at rest with nothing held and no pointer,
|
||||||
const std::array<wii_remote::HandInputs, kHands>& hands, uint32_t injected_buttons);
|
// while still tracking motion so releasing it does not read as a jolt.
|
||||||
|
void PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen& screen,
|
||||||
|
const std::array<wii_remote::HandInputs, kHands>& hands, uint32_t injected_buttons,
|
||||||
|
bool withheld);
|
||||||
|
void PublishSettingsPanel(XrTime input_time, const OpenXRPointerScreen& panel,
|
||||||
|
const settings_panel::Frame& frame);
|
||||||
void UpdateRumble();
|
void UpdateRumble();
|
||||||
void StopRumble();
|
void StopRumble();
|
||||||
bool Check(XrResult result, const char* operation);
|
bool Check(XrResult result, const char* operation);
|
||||||
@@ -128,6 +141,9 @@ private:
|
|||||||
PFN_xrVoidFunction m_convert_now_to_xr_time = nullptr;
|
PFN_xrVoidFunction m_convert_now_to_xr_time = nullptr;
|
||||||
wii_remote::MotionTracker m_motion[kHands];
|
wii_remote::MotionTracker m_motion[kHands];
|
||||||
wii_remote::PointerFilter m_pointer;
|
wii_remote::PointerFilter m_pointer;
|
||||||
|
settings_panel::Controls m_panel_controls;
|
||||||
|
XrTime m_last_input_time = 0;
|
||||||
|
bool m_panel_select_held = false;
|
||||||
std::array<float, 2> m_horizon{1.0f, 0.0f};
|
std::array<float, 2> m_horizon{1.0f, 0.0f};
|
||||||
bool m_haptics_active[kHands]{};
|
bool m_haptics_active[kHands]{};
|
||||||
uint32_t m_joystick_id = 0; // SDL_JoystickID; 0 when detached
|
uint32_t m_joystick_id = 0; // SDL_JoystickID; 0 when detached
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "vr/openxr_wii_remote.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace mkw::vr {
|
||||||
|
|
||||||
|
// The settings panel shown inside the headset: the F10 settings, drawn by the
|
||||||
|
// settings overlay with an ImGui context of its own and laid over the eyes by
|
||||||
|
// Aurora (aurora_imgui_set_stereo_overlay). It sits on the virtual screen, the
|
||||||
|
// menu quad or the race's 2D screen, and is operated with the tracked
|
||||||
|
// controllers, which are withheld from the game while it is open.
|
||||||
|
//
|
||||||
|
// The XR pacing thread owns the controllers: it opens and closes the panel from
|
||||||
|
// the controller chord, aims the pointer and publishes it here. The game thread
|
||||||
|
// draws the panel, may close it (its Close button) or open it (the F10 bar), and
|
||||||
|
// reads the pointer once per presented frame. Nothing in this header depends on
|
||||||
|
// OpenXR, so the settings overlay compiles the same in builds without it.
|
||||||
|
|
||||||
|
// The panel's canvas in ImGui pixels, drawn at twice the desktop menu's scale.
|
||||||
|
inline constexpr float kSettingsPanelWidthPixels = 1440.0f;
|
||||||
|
inline constexpr float kSettingsPanelHeightPixels = 1080.0f;
|
||||||
|
inline constexpr float kSettingsPanelUiScale = 2.0f;
|
||||||
|
// Its width as a fraction of the virtual screen's.
|
||||||
|
inline constexpr float kSettingsPanelWidthFraction = 0.75f;
|
||||||
|
|
||||||
|
struct OpenXRSettingsPanelPointer {
|
||||||
|
bool valid = false;
|
||||||
|
float x = 0.0f; // canvas pixels, +x right
|
||||||
|
float y = 0.0f; // canvas pixels, +y down
|
||||||
|
bool select = false;
|
||||||
|
float wheel = 0.0f; // ImGui wheel steps since the last read, +up
|
||||||
|
};
|
||||||
|
|
||||||
|
// Either thread.
|
||||||
|
void OpenXRSetSettingsPanelOpen(bool open) noexcept;
|
||||||
|
bool OpenXRSettingsPanelOpen() noexcept;
|
||||||
|
// XR thread: this frame's pointer, and wheel steps to add to what is pending.
|
||||||
|
void OpenXRPublishSettingsPanelPointer(bool valid, float x, float y, bool select, float wheel) noexcept;
|
||||||
|
// Game thread: the latest pointer, taking the wheel steps accumulated since the last call.
|
||||||
|
OpenXRSettingsPanelPointer OpenXRTakeSettingsPanelPointer() noexcept;
|
||||||
|
|
||||||
|
namespace settings_panel {
|
||||||
|
|
||||||
|
using wii_remote::HandInputs;
|
||||||
|
|
||||||
|
// Thumbstick deflection below this does not scroll.
|
||||||
|
inline constexpr float kScrollDeadzone = 0.25f;
|
||||||
|
// Wheel steps per second at full deflection. ImGui scrolls about five lines a step.
|
||||||
|
inline constexpr float kScrollStepsPerSecond = 8.0f;
|
||||||
|
|
||||||
|
// The panel's half extents, in metres, on a virtual screen `screen_width` metres across.
|
||||||
|
inline std::array<float, 2> HalfExtents(float screen_width) noexcept {
|
||||||
|
const float half_width = 0.5f * screen_width * kSettingsPanelWidthFraction;
|
||||||
|
return {half_width, half_width * kSettingsPanelHeightPixels / kSettingsPanelWidthPixels};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hit on the panel (-1..1 across it, +v up) in canvas pixels.
|
||||||
|
inline std::array<float, 2> CanvasPoint(const wii_remote::ScreenHit& hit) noexcept {
|
||||||
|
return {0.5f * (hit.u + 1.0f) * kSettingsPanelWidthPixels, 0.5f * (1.0f - hit.v) * kSettingsPanelHeightPixels};
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the controllers do this frame.
|
||||||
|
struct Frame {
|
||||||
|
bool open = false; // the panel is showing
|
||||||
|
bool withheld = false; // the game must not see the controllers
|
||||||
|
uint32_t pointing_hand = 1;
|
||||||
|
bool select = false;
|
||||||
|
float wheel = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The controller side of the panel, one Update per XR frame:
|
||||||
|
//
|
||||||
|
// - Clicking both thumbsticks together opens or closes it. Nothing else opens
|
||||||
|
// it from the controllers: every other button already means something to the
|
||||||
|
// game.
|
||||||
|
// - While it is open, the left menu button also closes it, both triggers and
|
||||||
|
// the A / X buttons select, the thumbsticks scroll, and the hand whose trigger
|
||||||
|
// was pulled last is the one pointing.
|
||||||
|
// - The game gets the controllers back only once everything is released, so the
|
||||||
|
// press that closed the panel never lands in the game as well.
|
||||||
|
class Controls {
|
||||||
|
public:
|
||||||
|
// `open` is the shared flag: the chord flips it, and the game thread may
|
||||||
|
// have changed it since the last frame. `dt_seconds` is the time since the
|
||||||
|
// previous frame.
|
||||||
|
Frame Update(const std::array<HandInputs, 2>& hands, bool& open, float dt_seconds) noexcept {
|
||||||
|
const bool chord = hands[0].thumbstick_click && hands[1].thumbstick_click;
|
||||||
|
if (chord && !m_chord_held) {
|
||||||
|
open = !open;
|
||||||
|
}
|
||||||
|
m_chord_held = chord;
|
||||||
|
|
||||||
|
if (open && hands[0].menu && !m_menu_held && m_was_open) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
m_menu_held = hands[0].menu;
|
||||||
|
|
||||||
|
for (uint32_t hand = 0; hand < 2; ++hand) {
|
||||||
|
const bool pulled = hands[hand].trigger > wii_remote::kPressThreshold;
|
||||||
|
if (pulled && !m_trigger_held[hand]) {
|
||||||
|
m_pointing_hand = hand;
|
||||||
|
}
|
||||||
|
m_trigger_held[hand] = pulled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open != m_was_open) {
|
||||||
|
// Whatever is held across the change belongs to that change.
|
||||||
|
m_release_pending = true;
|
||||||
|
}
|
||||||
|
if (m_release_pending && !AnyHeld(hands)) {
|
||||||
|
m_release_pending = false;
|
||||||
|
}
|
||||||
|
m_was_open = open;
|
||||||
|
|
||||||
|
Frame frame{};
|
||||||
|
frame.open = open;
|
||||||
|
frame.withheld = open || m_release_pending;
|
||||||
|
frame.pointing_hand = m_pointing_hand;
|
||||||
|
if (!open) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
// Opening held nothing that selects, but a trigger still down from the
|
||||||
|
// game must not click the first thing under the pointer.
|
||||||
|
frame.select = !m_release_pending && (m_trigger_held[0] || m_trigger_held[1] || hands[0].primary ||
|
||||||
|
hands[1].primary);
|
||||||
|
const float stick = std::fabs(hands[1].stick_y) >= std::fabs(hands[0].stick_y) ? hands[1].stick_y
|
||||||
|
: hands[0].stick_y;
|
||||||
|
if (std::fabs(stick) > kScrollDeadzone) {
|
||||||
|
const float magnitude = (std::fabs(stick) - kScrollDeadzone) / (1.0f - kScrollDeadzone);
|
||||||
|
frame.wheel = std::copysign(magnitude, stick) * kScrollStepsPerSecond * std::clamp(dt_seconds, 0.0f, 0.1f);
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reset() noexcept { *this = Controls{}; }
|
||||||
|
|
||||||
|
static bool AnyHeld(const std::array<HandInputs, 2>& hands) noexcept {
|
||||||
|
for (const HandInputs& hand : hands) {
|
||||||
|
if (hand.primary || hand.secondary || hand.menu || hand.thumbstick_click ||
|
||||||
|
hand.trigger > wii_remote::kPressThreshold || hand.squeeze > wii_remote::kPressThreshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_chord_held = false;
|
||||||
|
bool m_menu_held = false;
|
||||||
|
std::array<bool, 2> m_trigger_held{};
|
||||||
|
bool m_was_open = false;
|
||||||
|
bool m_release_pending = false;
|
||||||
|
uint32_t m_pointing_hand = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace settings_panel
|
||||||
|
|
||||||
|
} // namespace mkw::vr
|
||||||
+34
-16
@@ -962,6 +962,31 @@ constexpr DWORD kCppExceptionCodeMsvc = 0xE06D7363;
|
|||||||
constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT
|
constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT
|
||||||
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info);
|
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info);
|
||||||
|
|
||||||
|
// Exceptions the processor itself raises for the faulting instruction.
|
||||||
|
bool IsCpuFaultException(DWORD code) noexcept {
|
||||||
|
switch (code) {
|
||||||
|
case EXCEPTION_ACCESS_VIOLATION:
|
||||||
|
case EXCEPTION_IN_PAGE_ERROR:
|
||||||
|
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||||
|
case EXCEPTION_PRIV_INSTRUCTION:
|
||||||
|
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||||
|
case EXCEPTION_INT_OVERFLOW:
|
||||||
|
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||||
|
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||||
|
case EXCEPTION_STACK_OVERFLOW:
|
||||||
|
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||||
|
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||||
|
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||||
|
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||||
|
case EXCEPTION_FLT_OVERFLOW:
|
||||||
|
case EXCEPTION_FLT_STACK_CHECK:
|
||||||
|
case EXCEPTION_FLT_UNDERFLOW:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ReportStructuredException(EXCEPTION_POINTERS* info) {
|
void ReportStructuredException(EXCEPTION_POINTERS* info) {
|
||||||
if (!info || !info->ExceptionRecord) {
|
if (!info || !info->ExceptionRecord) {
|
||||||
RT_LOG(RT_TAG_RUNTIME) << "Structured exception occurred, but no diagnostic info was captured." << std::endl;
|
RT_LOG(RT_TAG_RUNTIME) << "Structured exception occurred, but no diagnostic info was captured." << std::endl;
|
||||||
@@ -1051,22 +1076,15 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
|
|||||||
if (g_suppressSehReporting) {
|
if (g_suppressSehReporting) {
|
||||||
return EXCEPTION_CONTINUE_SEARCH;
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
}
|
}
|
||||||
// Let C++ exceptions propagate to std::terminate so we can log their what().
|
// This handler sees every exception on every thread before any frame handler
|
||||||
if (info->ExceptionRecord->ExceptionCode == kCppExceptionCodeGcc ||
|
// does, so only CPU faults are fatal here. Everything else is raised in
|
||||||
info->ExceptionRecord->ExceptionCode == kCppExceptionCodeMsvc) {
|
// software and is often caught by its own frames: C++ exceptions (left to
|
||||||
return EXCEPTION_CONTINUE_SEARCH;
|
// reach std::terminate so their what() is logged), OutputDebugString,
|
||||||
}
|
// SetThreadName, ASan reports, msxml6's customer codes while mscms parses a
|
||||||
if (info->ExceptionRecord->ExceptionCode == 0x40010006 || // DBG_PRINTEXCEPTION_C
|
// display colour profile, and RPC's 0x6BA (RPC_S_SERVER_UNAVAILABLE) inside
|
||||||
info->ExceptionRecord->ExceptionCode == 0x4001000A || // DBG_PRINTEXCEPTION_WIDE_C (OutputDebugStringW)
|
// the shell folder picker behind F10 > Diagnostics > Export Logs. Any of
|
||||||
info->ExceptionRecord->ExceptionCode == 0x406D1388 || // SetThreadName
|
// them that truly goes unhandled still reaches UnhandledSehFilter.
|
||||||
info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first
|
if (!IsCpuFaultException(info->ExceptionRecord->ExceptionCode)) {
|
||||||
return EXCEPTION_CONTINUE_SEARCH;
|
|
||||||
}
|
|
||||||
// Software-raised exceptions (customer bit set) are used for internal control flow by
|
|
||||||
// system DLLs (e.g. msxml6 while mscms parses a display colour profile) and are caught
|
|
||||||
// by their own frame handlers. Only hardware faults are fatal at first chance; anything
|
|
||||||
// else that truly goes unhandled reaches UnhandledSehFilter.
|
|
||||||
if ((info->ExceptionRecord->ExceptionCode & 0x20000000u) != 0) {
|
|
||||||
return EXCEPTION_CONTINUE_SEARCH;
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
}
|
}
|
||||||
return ReportFatalSehAndExit(info);
|
return ReportFatalSehAndExit(info);
|
||||||
|
|||||||
@@ -12,9 +12,11 @@
|
|||||||
#include "vr/mkw_vr_policy.h"
|
#include "vr/mkw_vr_policy.h"
|
||||||
#include "vr/openxr_diagnostics.h"
|
#include "vr/openxr_diagnostics.h"
|
||||||
#include "vr/openxr_integration.h"
|
#include "vr/openxr_integration.h"
|
||||||
|
#include "vr/openxr_settings_panel.h"
|
||||||
#include "vr/openxr_wii_remote.h"
|
#include "vr/openxr_wii_remote.h"
|
||||||
#include "wii_remote_input.h"
|
#include "wii_remote_input.h"
|
||||||
|
|
||||||
|
#include <aurora/imgui.h>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <SDL3/SDL_dialog.h>
|
#include <SDL3/SDL_dialog.h>
|
||||||
#include <SDL3/SDL_error.h>
|
#include <SDL3/SDL_error.h>
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cfloat>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -81,6 +84,12 @@ const char* GraphicsApiDisplayName() {
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixed widths in the menus are written for the desktop bar's 13 px font. The
|
||||||
|
// headset's settings panel draws the same menus with a larger one.
|
||||||
|
float Scaled(float pixels) {
|
||||||
|
return pixels * ImGui::GetFontSize() / 13.0f;
|
||||||
|
}
|
||||||
|
|
||||||
bool g_topBarVisible = false;
|
bool g_topBarVisible = false;
|
||||||
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
|
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
|
||||||
int g_controllerPort = 0;
|
int g_controllerPort = 0;
|
||||||
@@ -364,7 +373,7 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
|
|||||||
// and it falls back to a nominal zero point, leaving a small per-axis bias;
|
// and it falls back to a nominal zero point, leaving a small per-axis bias;
|
||||||
// measured here with the remote at rest.
|
// measured here with the remote at rest.
|
||||||
if (WiiRemoteInput::IsAccelCalibrating()) {
|
if (WiiRemoteInput::IsAccelCalibrating()) {
|
||||||
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(Scaled(220.0f), 0.0f), "Hold still...");
|
||||||
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
||||||
WiiRemoteInput::StartAccelCalibration(port);
|
WiiRemoteInput::StartAccelCalibration(port);
|
||||||
}
|
}
|
||||||
@@ -492,7 +501,7 @@ int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
|
|||||||
|
|
||||||
void DrawExpressionSettings() {
|
void DrawExpressionSettings() {
|
||||||
ImGui::SeparatorText("Expressions (Dolphin syntax)");
|
ImGui::SeparatorText("Expressions (Dolphin syntax)");
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 440.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(440.0f));
|
||||||
ImGui::TextDisabled(
|
ImGui::TextDisabled(
|
||||||
"Optional. An expression overrides nothing: its result is combined with the "
|
"Optional. An expression overrides nothing: its result is combined with the "
|
||||||
"button mapping above. Operators ! & | ^ and functions if, min, max, clamp, "
|
"button mapping above. Operators ! & | ^ and functions if, min, max, clamp, "
|
||||||
@@ -538,7 +547,7 @@ void DrawExpressionSettings() {
|
|||||||
for (size_t i = 0; i < InputBindings::kControls.size(); ++i) {
|
for (size_t i = 0; i < InputBindings::kControls.size(); ++i) {
|
||||||
ImGui::PushID(static_cast<int>(i) + 2000);
|
ImGui::PushID(static_cast<int>(i) + 2000);
|
||||||
std::string& text = buffers[i];
|
std::string& text = buffers[i];
|
||||||
ImGui::SetNextItemWidth(300.0f);
|
ImGui::SetNextItemWidth(Scaled(300.0f));
|
||||||
if (ImGui::InputText(InputBindings::kControls[i].label, text.data(), text.capacity() + 1,
|
if (ImGui::InputText(InputBindings::kControls[i].label, text.data(), text.capacity() + 1,
|
||||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackResize,
|
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackResize,
|
||||||
ExpressionResizeCallback, &text)) {
|
ExpressionResizeCallback, &text)) {
|
||||||
@@ -708,7 +717,7 @@ void DrawControllerSettings() {
|
|||||||
|
|
||||||
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
|
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
|
||||||
ImGui::PushID(static_cast<int>(i));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
ImGui::SetNextItemWidth(190.0f);
|
ImGui::SetNextItemWidth(Scaled(190.0f));
|
||||||
if (ImGui::BeginCombo("##primary", current.label)) {
|
if (ImGui::BeginCombo("##primary", current.label)) {
|
||||||
for (const auto& candidate : kNativeButtons) {
|
for (const auto& candidate : kNativeButtons) {
|
||||||
const bool selected = candidate.nativeButton == mappingIt->nativeButton;
|
const bool selected = candidate.nativeButton == mappingIt->nativeButton;
|
||||||
@@ -741,7 +750,7 @@ void DrawControllerSettings() {
|
|||||||
ImGui::TextUnformatted("or");
|
ImGui::TextUnformatted("or");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
|
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
|
||||||
ImGui::SetNextItemWidth(190.0f);
|
ImGui::SetNextItemWidth(Scaled(190.0f));
|
||||||
if (ImGui::BeginCombo("##alt", altLabel)) {
|
if (ImGui::BeginCombo("##alt", altLabel)) {
|
||||||
for (const auto& candidate : kNativeButtons) {
|
for (const auto& candidate : kNativeButtons) {
|
||||||
const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID;
|
const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID;
|
||||||
@@ -772,7 +781,7 @@ void DrawControllerSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DrawAudioSettings() {
|
void DrawAudioSettings() {
|
||||||
ImGui::SetNextItemWidth(220.0f);
|
ImGui::SetNextItemWidth(Scaled(220.0f));
|
||||||
if (ImGui::SliderInt("Master", &g_audioVolumePercent, 0, 100, "%d%%")) {
|
if (ImGui::SliderInt("Master", &g_audioVolumePercent, 0, 100, "%d%%")) {
|
||||||
const float volume = static_cast<float>(g_audioVolumePercent) / 100.0f;
|
const float volume = static_cast<float>(g_audioVolumePercent) / 100.0f;
|
||||||
AudioBackend::Instance().SetMasterVolume(volume);
|
AudioBackend::Instance().SetMasterVolume(volume);
|
||||||
@@ -918,7 +927,7 @@ void DrawGraphicsSettings() {
|
|||||||
aurora_set_display_mode(AURORA_DISPLAY_MODE_EXCLUSIVE);
|
aurora_set_display_mode(AURORA_DISPLAY_MODE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||||
ImGui::TextDisabled("Frame interpolation is experimental, you might find visual artifacts");
|
ImGui::TextDisabled("Frame interpolation is experimental, you might find visual artifacts");
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) {
|
if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) {
|
||||||
@@ -963,6 +972,19 @@ void DrawVrSettings() {
|
|||||||
"and None leaves the window black. Menus reach the headset as a screen showing this "
|
"and None leaves the window black. Menus reach the headset as a screen showing this "
|
||||||
"same desktop image, so the eye choices only differ from Normal during a race.");
|
"same desktop image, so the eye choices only differ from Normal during a race.");
|
||||||
}
|
}
|
||||||
|
ImGui::BeginDisabled(!mkw::vr::OpenXRIsRunning());
|
||||||
|
bool settingsPanelOpen = mkw::vr::OpenXRSettingsPanelOpen();
|
||||||
|
if (ImGui::Checkbox("Show these settings in the headset", &settingsPanelOpen)) {
|
||||||
|
mkw::vr::OpenXRSetSettingsPanelOpen(settingsPanelOpen);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||||
|
ImGui::SetTooltip(
|
||||||
|
"Opens these settings on a panel in front of you, in menus and races alike.\n"
|
||||||
|
"In the headset, clicking both thumbsticks together opens and closes it too.\n"
|
||||||
|
"Aim at it and pull a trigger to change a setting; push a thumbstick to scroll.\n"
|
||||||
|
"While it is open the game does not see the VR controllers.");
|
||||||
|
}
|
||||||
if (ImGui::Combo("VR controllers", &g_vrControllerMode, kVrControllerModeLabels.data(),
|
if (ImGui::Combo("VR controllers", &g_vrControllerMode, kVrControllerModeLabels.data(),
|
||||||
static_cast<int>(kVrControllerModeLabels.size()))) {
|
static_cast<int>(kVrControllerModeLabels.size()))) {
|
||||||
mkw::vr::OpenXRSetControllerMode(static_cast<mkw::vr::OpenXRControllerMode>(g_vrControllerMode));
|
mkw::vr::OpenXRSetControllerMode(static_cast<mkw::vr::OpenXRControllerMode>(g_vrControllerMode));
|
||||||
@@ -1035,7 +1057,7 @@ void DrawVrSettings() {
|
|||||||
"Wii's reused EFB for the next frame; an eye attachment is built fresh, so "
|
"Wii's reused EFB for the next frame; an eye attachment is built fresh, so "
|
||||||
"replaying it only erases the eye.");
|
"replaying it only erases the eye.");
|
||||||
}
|
}
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||||
ImGui::TextDisabled(
|
ImGui::TextDisabled(
|
||||||
"Both apply on the next frame. Turning either off restores the raw replay and is "
|
"Both apply on the next frame. Turning either off restores the raw replay and is "
|
||||||
"expected to black out the eyes.");
|
"expected to black out the eyes.");
|
||||||
@@ -1139,7 +1161,7 @@ void DrawVrSettings() {
|
|||||||
RuntimeConfigFile::SetVrFirstPersonHeadRightMeters(g_vrFirstPersonHeadRight);
|
RuntimeConfigFile::SetVrFirstPersonHeadRightMeters(g_vrFirstPersonHeadRight);
|
||||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||||
}
|
}
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||||
ImGui::TextDisabled("Where the head sits in the kart's own frame.");
|
ImGui::TextDisabled("Where the head sits in the kart's own frame.");
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
constexpr std::array<const char*, 3> kRotationLabels{"Yaw only", "Yaw + Pitch", "Full rotation"};
|
constexpr std::array<const char*, 3> kRotationLabels{"Yaw only", "Yaw + Pitch", "Full rotation"};
|
||||||
@@ -1310,7 +1332,7 @@ void DrawDiagnosticsSettings() {
|
|||||||
"Turn it on to report stutter or black frames in VR, and off again afterwards.\n"
|
"Turn it on to report stutter or black frames in VR, and off again afterwards.\n"
|
||||||
"Off by default. Applies immediately and is remembered.");
|
"Off by default. Applies immediately and is remembered.");
|
||||||
}
|
}
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||||
if (g_openxrDiagnosticsLogging && !mkw::vr::OpenXRIsRunning()) {
|
if (g_openxrDiagnosticsLogging && !mkw::vr::OpenXRIsRunning()) {
|
||||||
ImGui::TextDisabled("OpenXR is not running, so nothing is logged until a VR session starts.");
|
ImGui::TextDisabled("OpenXR is not running, so nothing is logged until a VR session starts.");
|
||||||
}
|
}
|
||||||
@@ -1337,7 +1359,7 @@ void DrawDiagnosticsSettings() {
|
|||||||
failed = g_logExport.failed;
|
failed = g_logExport.failed;
|
||||||
}
|
}
|
||||||
if (!message.empty()) {
|
if (!message.empty()) {
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||||
if (failed) {
|
if (failed) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", message.c_str());
|
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", message.c_str());
|
||||||
} else {
|
} else {
|
||||||
@@ -1444,13 +1466,7 @@ void DrawStartupScreen() {
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawTopBar() {
|
void DrawResolutionMenu() {
|
||||||
if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::TextUnformatted("WiiCompiled");
|
|
||||||
ImGui::Separator();
|
|
||||||
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
|
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
|
||||||
return std::fabs(item.scale - g_resolutionScale) < 0.001f;
|
return std::fabs(item.scale - g_resolutionScale) < 0.001f;
|
||||||
});
|
});
|
||||||
@@ -1469,6 +1485,16 @@ void DrawTopBar() {
|
|||||||
}
|
}
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTopBar() {
|
||||||
|
if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TextUnformatted("WiiCompiled");
|
||||||
|
ImGui::Separator();
|
||||||
|
DrawResolutionMenu();
|
||||||
|
|
||||||
if (ImGui::BeginMenu("Graphics")) {
|
if (ImGui::BeginMenu("Graphics")) {
|
||||||
DrawGraphicsSettings();
|
DrawGraphicsSettings();
|
||||||
@@ -1574,6 +1600,143 @@ void PersistDisplayModeIfChanged() {
|
|||||||
g_displayMode = active;
|
g_displayMode = active;
|
||||||
RuntimeConfigFile::SetDisplayMode(std::string(kDisplayModeConfigNames[static_cast<size_t>(active)]));
|
RuntimeConfigFile::SetDisplayMode(std::string(kDisplayModeConfigNames[static_cast<size_t>(active)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The headset's settings panel (vr/openxr_settings_panel.h): the same menus as
|
||||||
|
// the F10 bar, drawn with an ImGui context of its own at a size fixed in panel
|
||||||
|
// pixels, operated by the VR controllers' pointer and handed to Aurora, which
|
||||||
|
// lays it over the eyes. The desktop context is untouched, so the F10 bar and
|
||||||
|
// the panel can both be open.
|
||||||
|
struct VrSettingsPanel {
|
||||||
|
ImGuiContext* context = nullptr;
|
||||||
|
Clock::time_point lastFrame{};
|
||||||
|
bool selectHeld = false;
|
||||||
|
};
|
||||||
|
VrSettingsPanel g_vrSettingsPanel;
|
||||||
|
|
||||||
|
ImGuiContext* CreateVrSettingsPanelContext() {
|
||||||
|
ImGuiContext* const desktop = ImGui::GetCurrentContext();
|
||||||
|
ImGuiContext* const context = ImGui::CreateContext();
|
||||||
|
ImGui::SetCurrentContext(context);
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
io.IniFilename = nullptr;
|
||||||
|
io.LogFilename = nullptr;
|
||||||
|
// No platform backend draws a cursor for it, and nothing else shows where
|
||||||
|
// the controller is aiming.
|
||||||
|
io.MouseDrawCursor = true;
|
||||||
|
// Aurora's WebGPU backend, which renders this context's draw data, honours it.
|
||||||
|
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset;
|
||||||
|
// The default font rasterised at the panel's scale rather than magnified,
|
||||||
|
// with an atlas of its own so the desktop font texture is left alone.
|
||||||
|
ImFontConfig font;
|
||||||
|
font.SizePixels = 13.0f * mkw::vr::kSettingsPanelUiScale;
|
||||||
|
io.Fonts->AddFontDefault(&font);
|
||||||
|
unsigned char* pixels = nullptr;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
|
||||||
|
io.Fonts->SetTexID(aurora_imgui_add_texture(static_cast<uint32_t>(width), static_cast<uint32_t>(height), pixels));
|
||||||
|
io.Fonts->ClearTexData();
|
||||||
|
ImGui::StyleColorsDark();
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
style.ScaleAllSizes(mkw::vr::kSettingsPanelUiScale);
|
||||||
|
// Nothing behind the panel is meant to be read through it.
|
||||||
|
style.Colors[ImGuiCol_WindowBg].w = 0.97f;
|
||||||
|
style.Colors[ImGuiCol_PopupBg].w = 0.98f;
|
||||||
|
ImGui::SetCurrentContext(desktop);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawVrSettingsPanelWindow() {
|
||||||
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
|
ImGui::SetNextWindowPos(viewport->Pos, ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowSize(viewport->Size, ImGuiCond_Always);
|
||||||
|
constexpr ImGuiWindowFlags kFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||||
|
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus;
|
||||||
|
if (ImGui::Begin("WiiCompiled settings", nullptr, kFlags)) {
|
||||||
|
ImGui::TextUnformatted("WiiCompiled settings");
|
||||||
|
const ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
const float recenterWidth = ImGui::CalcTextSize("Recenter view").x + style.FramePadding.x * 2.0f;
|
||||||
|
const float closeWidth = ImGui::CalcTextSize("Close").x + style.FramePadding.x * 2.0f;
|
||||||
|
ImGui::SameLine(ImGui::GetContentRegionMax().x - recenterWidth - closeWidth - style.ItemSpacing.x);
|
||||||
|
if (ImGui::Button("Recenter view")) {
|
||||||
|
mkw::vr::OpenXRRequestRecenter();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Close")) {
|
||||||
|
mkw::vr::OpenXRSetSettingsPanelOpen(false);
|
||||||
|
}
|
||||||
|
ImGui::TextDisabled("Aim and pull a trigger to change a setting, push a thumbstick to scroll.");
|
||||||
|
ImGui::TextDisabled("Click both thumbsticks or press Menu to close. The game does not see the controllers meanwhile.");
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::BeginTabBar("Settings")) {
|
||||||
|
const auto tab = [](const char* label, void (*draw)()) {
|
||||||
|
if (ImGui::BeginTabItem(label)) {
|
||||||
|
// Its own scrolling region, so the header stays in view.
|
||||||
|
ImGui::BeginChild("Contents");
|
||||||
|
draw();
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tab("VR", DrawVrSettings);
|
||||||
|
tab("Graphics", [] {
|
||||||
|
DrawResolutionMenu();
|
||||||
|
ImGui::Separator();
|
||||||
|
DrawGraphicsSettings();
|
||||||
|
});
|
||||||
|
tab("Controllers", DrawControllerSettings);
|
||||||
|
tab("Audio", DrawAudioSettings);
|
||||||
|
tab("Diagnostics", DrawDiagnosticsSettings);
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called once per presented frame after the desktop menus, with the frame
|
||||||
|
// worker done: the draw data handed to Aurora must stay put until the next
|
||||||
|
// frame is encoded, and only the next call here rebuilds it.
|
||||||
|
void DrawVrSettingsPanel() {
|
||||||
|
if (!mkw::vr::OpenXRIsRunning() || !mkw::vr::OpenXRSettingsPanelOpen()) {
|
||||||
|
aurora_imgui_set_stereo_overlay(nullptr, 0.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VrSettingsPanel& panel = g_vrSettingsPanel;
|
||||||
|
if (panel.context == nullptr) {
|
||||||
|
panel.context = CreateVrSettingsPanelContext();
|
||||||
|
}
|
||||||
|
const mkw::vr::OpenXRSettingsPanelPointer pointer = mkw::vr::OpenXRTakeSettingsPanelPointer();
|
||||||
|
const Clock::time_point now = Clock::now();
|
||||||
|
float deltaSeconds = 1.0f / 60.0f;
|
||||||
|
if (panel.lastFrame != Clock::time_point{}) {
|
||||||
|
deltaSeconds = std::clamp(std::chrono::duration<float>(now - panel.lastFrame).count(), 1.0e-4f, 0.25f);
|
||||||
|
}
|
||||||
|
panel.lastFrame = now;
|
||||||
|
|
||||||
|
ImGuiContext* const desktop = ImGui::GetCurrentContext();
|
||||||
|
ImGui::SetCurrentContext(panel.context);
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
io.DisplaySize = ImVec2(mkw::vr::kSettingsPanelWidthPixels, mkw::vr::kSettingsPanelHeightPixels);
|
||||||
|
io.DeltaTime = deltaSeconds;
|
||||||
|
if (pointer.valid) {
|
||||||
|
io.AddMousePosEvent(pointer.x, pointer.y);
|
||||||
|
} else {
|
||||||
|
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||||
|
}
|
||||||
|
if (pointer.select != panel.selectHeld) {
|
||||||
|
io.AddMouseButtonEvent(ImGuiMouseButton_Left, pointer.select);
|
||||||
|
panel.selectHeld = pointer.select;
|
||||||
|
}
|
||||||
|
if (pointer.wheel != 0.0f) {
|
||||||
|
io.AddMouseWheelEvent(0.0f, pointer.wheel);
|
||||||
|
}
|
||||||
|
ImGui::NewFrame();
|
||||||
|
DrawVrSettingsPanelWindow();
|
||||||
|
ImGui::Render();
|
||||||
|
ImDrawData* const drawData = ImGui::GetDrawData();
|
||||||
|
ImGui::SetCurrentContext(desktop);
|
||||||
|
aurora_imgui_set_stereo_overlay(drawData, mkw::vr::kSettingsPanelWidthFraction);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void InitializeRuntimeSettings() noexcept {
|
void InitializeRuntimeSettings() noexcept {
|
||||||
@@ -1685,6 +1848,7 @@ void Draw() noexcept {
|
|||||||
PADBlockInput(inputBlocked);
|
PADBlockInput(inputBlocked);
|
||||||
InputBindings::SetInputBlocked(inputBlocked);
|
InputBindings::SetInputBlocked(inputBlocked);
|
||||||
DrawStartupScreen();
|
DrawStartupScreen();
|
||||||
|
DrawVrSettingsPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StartupScreenVisible() noexcept {
|
bool StartupScreenVisible() noexcept {
|
||||||
|
|||||||
+105
-18
@@ -40,8 +40,9 @@ namespace {
|
|||||||
// A new sequence number holds the button for kInjectHoldFrames XR frames.
|
// A new sequence number holds the button for kInjectHoldFrames XR frames.
|
||||||
// Buttons: a, b, x, y, start, up, down, left, right, and for the Wii Remote
|
// Buttons: a, b, x, y, start, up, down, left, right, and for the Wii Remote
|
||||||
// presentation also home, c and z (x/y/start press 1/2/+ there, and the
|
// presentation also home, c and z (x/y/start press 1/2/+ there, and the
|
||||||
// directions push the Nunchuk stick). The property is unset in normal use, so
|
// directions push the Nunchuk stick). `panel` clicks both thumbsticks, opening
|
||||||
// this costs one property read every few frames.
|
// or closing the settings panel, where `a` then selects. The property is unset
|
||||||
|
// in normal use, so this costs one property read every few frames.
|
||||||
constexpr uint32_t kInjectHoldFrames = 12;
|
constexpr uint32_t kInjectHoldFrames = 12;
|
||||||
constexpr uint32_t kInjectPollFrames = 4;
|
constexpr uint32_t kInjectPollFrames = 4;
|
||||||
|
|
||||||
@@ -468,6 +469,9 @@ void OpenXRInput::Destroy() {
|
|||||||
}
|
}
|
||||||
m_pointer.Reset();
|
m_pointer.Reset();
|
||||||
m_horizon = {1.0f, 0.0f};
|
m_horizon = {1.0f, 0.0f};
|
||||||
|
m_panel_controls.Reset();
|
||||||
|
m_last_input_time = 0;
|
||||||
|
m_panel_select_held = false;
|
||||||
m_created = false;
|
m_created = false;
|
||||||
m_runtime = nullptr;
|
m_runtime = nullptr;
|
||||||
}
|
}
|
||||||
@@ -482,6 +486,11 @@ void OpenXRInput::Idle() {
|
|||||||
m_pointer.Reset();
|
m_pointer.Reset();
|
||||||
m_horizon = {1.0f, 0.0f};
|
m_horizon = {1.0f, 0.0f};
|
||||||
OpenXRPublishWiiRemote(m_joystick_id, OpenXRWiiRemoteSample{});
|
OpenXRPublishWiiRemote(m_joystick_id, OpenXRWiiRemoteSample{});
|
||||||
|
// The panel stays as it was; only what the controllers were holding is forgotten.
|
||||||
|
m_panel_controls.Reset();
|
||||||
|
m_last_input_time = 0;
|
||||||
|
m_panel_select_held = false;
|
||||||
|
OpenXRPublishSettingsPanelPointer(false, 0.0f, 0.0f, false, 0.0f);
|
||||||
StopRumble();
|
StopRumble();
|
||||||
// Nothing stays held on the gamepad either while input is away.
|
// Nothing stays held on the gamepad either while input is away.
|
||||||
if (m_joystick != nullptr) {
|
if (m_joystick != nullptr) {
|
||||||
@@ -495,7 +504,8 @@ void OpenXRInput::Idle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen) {
|
void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
|
||||||
|
const OpenXRPointerScreen& settings_panel) {
|
||||||
if (!m_created || m_runtime == nullptr) {
|
if (!m_created || m_runtime == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -566,18 +576,48 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
|
|||||||
}
|
}
|
||||||
|
|
||||||
PollInjection();
|
PollInjection();
|
||||||
wii_remote::HandInputs& left = hands[0];
|
|
||||||
const wii_remote::HandInputs& right = hands[1];
|
|
||||||
if (Injected("up")) {
|
if (Injected("up")) {
|
||||||
left.stick_y = 1.0f;
|
hands[0].stick_y = 1.0f;
|
||||||
} else if (Injected("down")) {
|
} else if (Injected("down")) {
|
||||||
left.stick_y = -1.0f;
|
hands[0].stick_y = -1.0f;
|
||||||
} else if (Injected("left")) {
|
} else if (Injected("left")) {
|
||||||
left.stick_x = -1.0f;
|
hands[0].stick_x = -1.0f;
|
||||||
} else if (Injected("right")) {
|
} else if (Injected("right")) {
|
||||||
left.stick_x = 1.0f;
|
hands[0].stick_x = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const XrTime input_time = InputSampleTime(predicted_display_time);
|
||||||
|
const float dt_seconds =
|
||||||
|
m_last_input_time != 0 && input_time > m_last_input_time
|
||||||
|
? static_cast<float>(input_time - m_last_input_time) * 1.0e-9f
|
||||||
|
: 0.0f;
|
||||||
|
m_last_input_time = input_time;
|
||||||
|
std::array<wii_remote::HandInputs, kHands> panel_hands = hands;
|
||||||
|
if (Injected("panel")) {
|
||||||
|
panel_hands[0].thumbstick_click = true;
|
||||||
|
panel_hands[1].thumbstick_click = true;
|
||||||
|
}
|
||||||
|
if (Injected("a")) {
|
||||||
|
panel_hands[1].primary = true;
|
||||||
|
}
|
||||||
|
// The game thread may open or close the panel too; only a change made here
|
||||||
|
// is written back.
|
||||||
|
const bool was_open = OpenXRSettingsPanelOpen();
|
||||||
|
bool open = was_open;
|
||||||
|
const settings_panel::Frame panel = m_panel_controls.Update(panel_hands, open, dt_seconds);
|
||||||
|
// Pointer first: the game thread reads it as soon as it sees the panel open.
|
||||||
|
PublishSettingsPanel(input_time, settings_panel, panel);
|
||||||
|
if (open != was_open) {
|
||||||
|
OpenXRSetSettingsPanelOpen(open);
|
||||||
|
}
|
||||||
|
|
||||||
|
// While the panel has the controllers, the game sees them idle.
|
||||||
|
static const std::array<wii_remote::HandInputs, kHands> kIdleHands{};
|
||||||
|
const auto& game_hands = panel.withheld ? kIdleHands : hands;
|
||||||
|
const wii_remote::HandInputs& left = game_hands[0];
|
||||||
|
const wii_remote::HandInputs& right = game_hands[1];
|
||||||
|
const auto injected = [&panel](const char* button) { return !panel.withheld && Injected(button); };
|
||||||
|
|
||||||
if (m_joystick != nullptr) {
|
if (m_joystick != nullptr) {
|
||||||
auto* joystick = static_cast<SDL_Joystick*>(m_joystick);
|
auto* joystick = static_cast<SDL_Joystick*>(m_joystick);
|
||||||
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
|
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
|
||||||
@@ -588,27 +628,65 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
|
|||||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, ToTrigger(left.trigger));
|
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, ToTrigger(left.trigger));
|
||||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, ToTrigger(right.trigger));
|
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, ToTrigger(right.trigger));
|
||||||
|
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_SOUTH, right.primary || Injected("a"));
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_SOUTH, right.primary || injected("a"));
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST, right.secondary || Injected("b"));
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST, right.secondary || injected("b"));
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_WEST, left.primary || Injected("x"));
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_WEST, left.primary || injected("x"));
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_NORTH, left.secondary || Injected("y"));
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_NORTH, left.secondary || injected("y"));
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_START, left.menu || Injected("start"));
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_START, left.menu || injected("start"));
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, left.thumbstick_click);
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, left.thumbstick_click);
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, right.thumbstick_click);
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, right.thumbstick_click);
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, left.squeeze > 0.5f);
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, left.squeeze > 0.5f);
|
||||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, right.squeeze > 0.5f);
|
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, right.squeeze > 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
PublishWiiRemote(predicted_display_time, screen, hands, InjectedWiiRemoteButtons());
|
PublishWiiRemote(input_time, screen, game_hands, panel.withheld ? 0u : InjectedWiiRemoteButtons(),
|
||||||
|
panel.withheld);
|
||||||
UpdateRumble();
|
UpdateRumble();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenXRInput::PublishWiiRemote(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
|
// The pointing hand's aim ray against the whole panel, in canvas pixels, with a
|
||||||
|
// short tick in that hand when a selection starts.
|
||||||
|
void OpenXRInput::PublishSettingsPanel(XrTime input_time, const OpenXRPointerScreen& panel,
|
||||||
|
const settings_panel::Frame& frame) {
|
||||||
|
if (!frame.open) {
|
||||||
|
// Nothing may still read as held when the panel next opens.
|
||||||
|
m_panel_select_held = false;
|
||||||
|
OpenXRPublishSettingsPanelPointer(false, 0.0f, 0.0f, false, 0.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
constexpr XrSpaceLocationFlags kPoseValid =
|
||||||
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
||||||
|
bool valid = false;
|
||||||
|
std::array<float, 2> point{};
|
||||||
|
const XrSpace aim_space = m_aim_spaces[frame.pointing_hand];
|
||||||
|
if (panel.valid && aim_space != XR_NULL_HANDLE) {
|
||||||
|
XrSpaceLocation location{XR_TYPE_SPACE_LOCATION};
|
||||||
|
if (XR_SUCCEEDED(xrLocateSpace(aim_space, m_runtime->AppSpace(), input_time, &location)) &&
|
||||||
|
(location.locationFlags & kPoseValid) == kPoseValid) {
|
||||||
|
wii_remote::Screen target{};
|
||||||
|
target.pose = ToWiiRemotePose(panel.pose);
|
||||||
|
target.half_width = panel.half_width_meters;
|
||||||
|
target.half_height = panel.half_height_meters;
|
||||||
|
const wii_remote::ScreenHit hit = wii_remote::RaycastScreen(ToWiiRemotePose(location.pose), target);
|
||||||
|
if (hit.valid) {
|
||||||
|
valid = true;
|
||||||
|
point = settings_panel::CanvasPoint(hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (frame.select && !m_panel_select_held) {
|
||||||
|
constexpr XrDuration kTickNs = 15'000'000;
|
||||||
|
ApplyHaptic(frame.pointing_hand, 0.35f, kTickNs);
|
||||||
|
}
|
||||||
|
m_panel_select_held = frame.select;
|
||||||
|
OpenXRPublishSettingsPanelPointer(valid, point[0], point[1], frame.select, frame.wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenXRInput::PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen& screen,
|
||||||
const std::array<wii_remote::HandInputs, kHands>& hands,
|
const std::array<wii_remote::HandInputs, kHands>& hands,
|
||||||
uint32_t injected_buttons) {
|
uint32_t injected_buttons, bool withheld) {
|
||||||
constexpr XrSpaceLocationFlags kPoseValid =
|
constexpr XrSpaceLocationFlags kPoseValid =
|
||||||
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
||||||
const XrTime input_time = InputSampleTime(predicted_display_time);
|
|
||||||
|
|
||||||
OpenXRWiiRemoteSample sample{};
|
OpenXRWiiRemoteSample sample{};
|
||||||
sample.hold = wii_remote::RemoteButtons(hands[0], hands[1]) | injected_buttons;
|
sample.hold = wii_remote::RemoteButtons(hands[0], hands[1]) | injected_buttons;
|
||||||
@@ -648,6 +726,15 @@ void OpenXRInput::PublishWiiRemote(XrTime predicted_display_time, const OpenXRPo
|
|||||||
(hand == 0 ? sample.nunchuk_acc : sample.acc) = acc;
|
(hand == 0 ? sample.nunchuk_acc : sample.acc) = acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withheld) {
|
||||||
|
// Waving a controller around the panel must not trick or wheelie.
|
||||||
|
sample.acc = OpenXRWiiRemoteSample{}.acc;
|
||||||
|
sample.nunchuk_acc = OpenXRWiiRemoteSample{}.nunchuk_acc;
|
||||||
|
m_pointer.Reset();
|
||||||
|
OpenXRPublishWiiRemote(m_joystick_id, sample);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
wii_remote::Screen target{};
|
wii_remote::Screen target{};
|
||||||
wii_remote::ScreenHit hit{};
|
wii_remote::ScreenHit hit{};
|
||||||
if (screen.valid && aim_valid[1]) {
|
if (screen.valid && aim_valid[1]) {
|
||||||
|
|||||||
@@ -741,7 +741,8 @@ private:
|
|||||||
if (input_ != nullptr) {
|
if (input_ != nullptr) {
|
||||||
// After the screen is placed, so the pointer aims at this
|
// After the screen is placed, so the pointer aims at this
|
||||||
// frame's screen rather than the previous one's.
|
// frame's screen rather than the previous one's.
|
||||||
input_->Sync(frame.xr_frame.predicted_display_time, PointerScreen(frame, policy, immersive));
|
input_->Sync(frame.xr_frame.predicted_display_time, PointerScreen(frame, policy, immersive),
|
||||||
|
SettingsPanelScreen(frame, policy, immersive));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!frame.expects_gpu_submission) {
|
if (!frame.expects_gpu_submission) {
|
||||||
@@ -951,55 +952,19 @@ private:
|
|||||||
if (!aurora_get_stereo_screen_aspects(&picture_aspect, &snapshot_aspect)) {
|
if (!aurora_get_stereo_screen_aspects(&picture_aspect, &snapshot_aspect)) {
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
const OpenXRFrame& xr_frame = frame.xr_frame;
|
|
||||||
const bool views_usable = xr_frame.views_valid &&
|
|
||||||
(xr_frame.view_state_flags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0;
|
|
||||||
const bool position_usable =
|
|
||||||
views_usable && (xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
|
|
||||||
|
|
||||||
if (immersive) {
|
if (immersive) {
|
||||||
const float distance = policy.config.hud_distance_meters;
|
if (!aurora_get_stereo_hud_screen_enabled() || !(policy.config.hud_width_meters > 0.0f) ||
|
||||||
const float width = policy.config.hud_width_meters;
|
!RaceScreenPose(frame, policy, screen.pose)) {
|
||||||
if (!aurora_get_stereo_hud_screen_enabled() || !(distance > 0.0f) || !(width > 0.0f)) {
|
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
std::array<float, 3> base{};
|
screen.half_width_meters = 0.5f * policy.config.hud_width_meters;
|
||||||
if (base_position_valid_ && last_immersive_) {
|
|
||||||
base = base_position_;
|
|
||||||
} else if (position_usable) {
|
|
||||||
// BuildPublishedFrame latches exactly this for the frame.
|
|
||||||
base = CenterPosition(xr_frame);
|
|
||||||
} else {
|
|
||||||
return screen;
|
|
||||||
}
|
|
||||||
const float half_angle =
|
|
||||||
0.5f * lean_back_degrees_.load(std::memory_order_relaxed) * kDegreesToRadians;
|
|
||||||
const Quaternion lean{std::sin(half_angle), 0.0f, 0.0f, std::cos(half_angle)};
|
|
||||||
const std::array<float, 3> ahead = Rotate(lean, {0.0f, 0.0f, -distance});
|
|
||||||
screen.pose.orientation = {lean.x, lean.y, lean.z, lean.w};
|
|
||||||
screen.pose.position = {base[0] + ahead[0], base[1] + ahead[1], base[2] + ahead[2]};
|
|
||||||
screen.half_width_meters = 0.5f * width;
|
|
||||||
screen.half_height_meters = screen.half_width_meters / picture_aspect;
|
screen.half_height_meters = screen.half_width_meters / picture_aspect;
|
||||||
screen.valid = true;
|
screen.valid = true;
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame.presentation.mode != OpenXRFrameMode::VirtualScreen || frame.render_width[0] == 0 ||
|
if (frame.render_width[0] == 0 || frame.render_height[0] == 0 || !MenuScreenPose(frame, screen.pose)) {
|
||||||
frame.render_height[0] == 0) {
|
|
||||||
return screen;
|
|
||||||
}
|
|
||||||
if (frame.presentation.quad_anchored) {
|
|
||||||
screen.pose = frame.presentation.quad_pose;
|
|
||||||
} else if (position_usable) {
|
|
||||||
// Head-locked in the view space: straight ahead of the head.
|
|
||||||
const auto& head = xr_frame.views[0].pose.orientation;
|
|
||||||
const Quaternion orientation = Normalize({head.x, head.y, head.z, head.w});
|
|
||||||
const std::array<float, 3> center = CenterPosition(xr_frame);
|
|
||||||
const std::array<float, 3> ahead = Rotate(
|
|
||||||
orientation, {0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)});
|
|
||||||
screen.pose.orientation = {orientation.x, orientation.y, orientation.z, orientation.w};
|
|
||||||
screen.pose.position = {center[0] + ahead[0], center[1] + ahead[1], center[2] + ahead[2]};
|
|
||||||
} else {
|
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
const float eye_aspect =
|
const float eye_aspect =
|
||||||
@@ -1012,6 +977,81 @@ private:
|
|||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The settings panel's rectangle: centred on the same screen, a fixed
|
||||||
|
// fraction of its width, the way Aurora lays it over the eyes
|
||||||
|
// (aurora_imgui_set_stereo_overlay). Unlike the pointer's picture it is
|
||||||
|
// there in a race even when the 2D layer is not on the screen.
|
||||||
|
OpenXRPointerScreen SettingsPanelScreen(const OpenXRBackendFrame& frame, const MkwVRPolicySnapshot& policy,
|
||||||
|
bool immersive) const noexcept {
|
||||||
|
OpenXRPointerScreen screen{};
|
||||||
|
const float screen_width =
|
||||||
|
immersive ? policy.config.hud_width_meters : std::max(0.25f, frame.presentation.quad_width_meters);
|
||||||
|
if (!(screen_width > 0.0f) ||
|
||||||
|
!(immersive ? RaceScreenPose(frame, policy, screen.pose) : MenuScreenPose(frame, screen.pose))) {
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
|
const std::array<float, 2> extents = settings_panel::HalfExtents(screen_width);
|
||||||
|
screen.half_width_meters = extents[0];
|
||||||
|
screen.half_height_meters = extents[1];
|
||||||
|
screen.valid = true;
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centre of the race's 2D screen. ViewFromBase maps a point p of the
|
||||||
|
// recorded centre-eye space (in metres) to base + lean * p in the
|
||||||
|
// application space, so the screen Aurora hangs hud_distance_meters ahead
|
||||||
|
// sits at base + lean * (0, 0, -distance), turned by the lean.
|
||||||
|
bool RaceScreenPose(const OpenXRBackendFrame& frame, const MkwVRPolicySnapshot& policy,
|
||||||
|
XrPosef& pose) const noexcept {
|
||||||
|
const OpenXRFrame& xr_frame = frame.xr_frame;
|
||||||
|
const float distance = policy.config.hud_distance_meters;
|
||||||
|
if (!(distance > 0.0f)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::array<float, 3> base{};
|
||||||
|
if (base_position_valid_ && last_immersive_) {
|
||||||
|
base = base_position_;
|
||||||
|
} else if (xr_frame.views_valid &&
|
||||||
|
(xr_frame.view_state_flags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0 &&
|
||||||
|
(xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0) {
|
||||||
|
// BuildPublishedFrame latches exactly this for the frame.
|
||||||
|
base = CenterPosition(xr_frame);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const float half_angle = 0.5f * lean_back_degrees_.load(std::memory_order_relaxed) * kDegreesToRadians;
|
||||||
|
const Quaternion lean{std::sin(half_angle), 0.0f, 0.0f, std::cos(half_angle)};
|
||||||
|
const std::array<float, 3> ahead = Rotate(lean, {0.0f, 0.0f, -distance});
|
||||||
|
pose.orientation = {lean.x, lean.y, lean.z, lean.w};
|
||||||
|
pose.position = {base[0] + ahead[0], base[1] + ahead[1], base[2] + ahead[2]};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centre of the menu quad: where UpdateVirtualScreenPose anchored it, or
|
||||||
|
// its head-locked fallback straight ahead of the head.
|
||||||
|
bool MenuScreenPose(const OpenXRBackendFrame& frame, XrPosef& pose) const noexcept {
|
||||||
|
if (frame.presentation.mode != OpenXRFrameMode::VirtualScreen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frame.presentation.quad_anchored) {
|
||||||
|
pose = frame.presentation.quad_pose;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const OpenXRFrame& xr_frame = frame.xr_frame;
|
||||||
|
if (!xr_frame.views_valid || (xr_frame.view_state_flags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) == 0 ||
|
||||||
|
(xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& head = xr_frame.views[0].pose.orientation;
|
||||||
|
const Quaternion orientation = Normalize({head.x, head.y, head.z, head.w});
|
||||||
|
const std::array<float, 3> center = CenterPosition(xr_frame);
|
||||||
|
const std::array<float, 3> ahead =
|
||||||
|
Rotate(orientation, {0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)});
|
||||||
|
pose.orientation = {orientation.x, orientation.y, orientation.z, orientation.w};
|
||||||
|
pose.position = {center[0] + ahead[0], center[1] + ahead[1], center[2] + ahead[2]};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ApplyPendingReferenceSpaceChange(const OpenXRFrame& frame) noexcept {
|
void ApplyPendingReferenceSpaceChange(const OpenXRFrame& frame) noexcept {
|
||||||
if (runtime_->ConsumeAppSpaceChangesThrough(frame.predicted_display_time)) {
|
if (runtime_->ConsumeAppSpaceChangesThrough(frame.predicted_display_time)) {
|
||||||
ResetTrackingOrigin();
|
ResetTrackingOrigin();
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "vr/openxr_settings_panel.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace mkw::vr {
|
||||||
|
// Named rather than anonymous: runtime sources are unity-built in groups.
|
||||||
|
namespace settings_panel_bridge {
|
||||||
|
|
||||||
|
std::atomic_bool g_open{false};
|
||||||
|
|
||||||
|
struct PublishedPointer {
|
||||||
|
std::mutex mutex;
|
||||||
|
OpenXRSettingsPanelPointer pointer{};
|
||||||
|
};
|
||||||
|
|
||||||
|
PublishedPointer& Published() {
|
||||||
|
static PublishedPointer published;
|
||||||
|
return published;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace settings_panel_bridge
|
||||||
|
|
||||||
|
void OpenXRSetSettingsPanelOpen(bool open) noexcept {
|
||||||
|
settings_panel_bridge::g_open.store(open, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenXRSettingsPanelOpen() noexcept {
|
||||||
|
return settings_panel_bridge::g_open.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenXRPublishSettingsPanelPointer(bool valid, float x, float y, bool select, float wheel) noexcept {
|
||||||
|
auto& published = settings_panel_bridge::Published();
|
||||||
|
std::lock_guard lock(published.mutex);
|
||||||
|
published.pointer.valid = valid;
|
||||||
|
published.pointer.x = x;
|
||||||
|
published.pointer.y = y;
|
||||||
|
published.pointer.select = select;
|
||||||
|
published.pointer.wheel += wheel;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenXRSettingsPanelPointer OpenXRTakeSettingsPanelPointer() noexcept {
|
||||||
|
auto& published = settings_panel_bridge::Published();
|
||||||
|
std::lock_guard lock(published.mutex);
|
||||||
|
OpenXRSettingsPanelPointer pointer = published.pointer;
|
||||||
|
published.pointer.wheel = 0.0f;
|
||||||
|
return pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mkw::vr
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// The in-headset settings panel's controller handling, tested without a
|
||||||
|
// headset: the thumbstick chord that opens and closes it, the release latch that
|
||||||
|
// keeps a closing press out of the game, selection, scrolling, and where a hit
|
||||||
|
// lands on the panel's canvas.
|
||||||
|
|
||||||
|
#include "vr/openxr_settings_panel.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace mkw::vr;
|
||||||
|
using namespace mkw::vr::settings_panel;
|
||||||
|
|
||||||
|
int g_failures = 0;
|
||||||
|
|
||||||
|
void Check(bool condition, const char* what) {
|
||||||
|
if (!condition) {
|
||||||
|
++g_failures;
|
||||||
|
std::cerr << "FAILED: " << what << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckNear(float actual, float expected, const char* what, float tolerance = 1.0e-3f) {
|
||||||
|
if (!(std::fabs(actual - expected) <= tolerance)) {
|
||||||
|
++g_failures;
|
||||||
|
std::cerr << "FAILED: " << what << " (expected " << expected << ", got " << actual << ")\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr float kDt = 1.0f / 90.0f;
|
||||||
|
|
||||||
|
std::array<HandInputs, 2> Released() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<HandInputs, 2> Chord() {
|
||||||
|
std::array<HandInputs, 2> hands{};
|
||||||
|
hands[0].thumbstick_click = true;
|
||||||
|
hands[1].thumbstick_click = true;
|
||||||
|
return hands;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChordOpensAndClosesOnce() {
|
||||||
|
Controls controls;
|
||||||
|
bool open = false;
|
||||||
|
|
||||||
|
auto one = Released();
|
||||||
|
one[0].thumbstick_click = true;
|
||||||
|
Frame frame = controls.Update(one, open, kDt);
|
||||||
|
Check(!open && !frame.withheld, "one thumbstick click alone neither opens nor withholds");
|
||||||
|
|
||||||
|
frame = controls.Update(Chord(), open, kDt);
|
||||||
|
Check(open && frame.open && frame.withheld, "clicking both thumbsticks opens the panel");
|
||||||
|
frame = controls.Update(Chord(), open, kDt);
|
||||||
|
Check(open, "holding the chord does not toggle again");
|
||||||
|
frame = controls.Update(Released(), open, kDt);
|
||||||
|
Check(open && frame.withheld, "the panel stays open and keeps the controllers after the chord is released");
|
||||||
|
|
||||||
|
frame = controls.Update(Chord(), open, kDt);
|
||||||
|
Check(!open && !frame.open, "the chord closes the panel again");
|
||||||
|
Check(frame.withheld, "the closing chord is still withheld from the game");
|
||||||
|
frame = controls.Update(Released(), open, kDt);
|
||||||
|
Check(!frame.withheld, "the game gets the controllers back once everything is released");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuClosesAndItsPressStaysOutOfTheGame() {
|
||||||
|
Controls controls;
|
||||||
|
bool open = false;
|
||||||
|
controls.Update(Chord(), open, kDt);
|
||||||
|
controls.Update(Released(), open, kDt);
|
||||||
|
|
||||||
|
auto menu = Released();
|
||||||
|
menu[0].menu = true;
|
||||||
|
Frame frame = controls.Update(menu, open, kDt);
|
||||||
|
Check(!open, "the left menu button closes an open panel");
|
||||||
|
frame = controls.Update(menu, open, kDt);
|
||||||
|
Check(frame.withheld, "HOME held across the close does not reach the game");
|
||||||
|
Check(!open, "a held menu button does not reopen the panel");
|
||||||
|
frame = controls.Update(Released(), open, kDt);
|
||||||
|
Check(!frame.withheld, "released, the controllers go back to the game");
|
||||||
|
|
||||||
|
frame = controls.Update(menu, open, kDt);
|
||||||
|
Check(!open && !frame.withheld, "with the panel closed the menu button is the game's");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectWaitsForAReleaseAndFollowsTheTrigger() {
|
||||||
|
Controls controls;
|
||||||
|
bool open = false;
|
||||||
|
// Opened from the settings bar while a trigger is held for the game.
|
||||||
|
auto trigger = Released();
|
||||||
|
trigger[1].trigger = 1.0f;
|
||||||
|
controls.Update(trigger, open, kDt);
|
||||||
|
open = true;
|
||||||
|
Frame frame = controls.Update(trigger, open, kDt);
|
||||||
|
Check(frame.open && !frame.select, "a trigger held from before the panel opened does not click");
|
||||||
|
frame = controls.Update(Released(), open, kDt);
|
||||||
|
Check(!frame.select, "nothing held, nothing selected");
|
||||||
|
frame = controls.Update(trigger, open, kDt);
|
||||||
|
Check(frame.select && frame.pointing_hand == 1, "a fresh right trigger selects and points with the right hand");
|
||||||
|
|
||||||
|
auto left = Released();
|
||||||
|
left[0].trigger = 0.9f;
|
||||||
|
controls.Update(Released(), open, kDt);
|
||||||
|
frame = controls.Update(left, open, kDt);
|
||||||
|
Check(frame.select && frame.pointing_hand == 0, "pulling the left trigger hands the pointer to the left hand");
|
||||||
|
|
||||||
|
auto button = Released();
|
||||||
|
button[1].primary = true;
|
||||||
|
controls.Update(Released(), open, kDt);
|
||||||
|
frame = controls.Update(button, open, kDt);
|
||||||
|
Check(frame.select && frame.pointing_hand == 0, "A selects without moving the pointer to another hand");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThumbstickScrolls() {
|
||||||
|
Controls controls;
|
||||||
|
bool open = true;
|
||||||
|
controls.Update(Released(), open, kDt);
|
||||||
|
|
||||||
|
auto small = Released();
|
||||||
|
small[1].stick_y = 0.2f;
|
||||||
|
Check(controls.Update(small, open, kDt).wheel == 0.0f, "a resting thumbstick does not scroll");
|
||||||
|
|
||||||
|
auto up = Released();
|
||||||
|
up[1].stick_y = 1.0f;
|
||||||
|
CheckNear(controls.Update(up, open, 0.5f).wheel, kScrollStepsPerSecond * 0.1f,
|
||||||
|
"full deflection scrolls up at the full rate, with a long frame clamped");
|
||||||
|
auto down = Released();
|
||||||
|
down[0].stick_y = -1.0f;
|
||||||
|
down[1].stick_y = 0.3f;
|
||||||
|
Check(controls.Update(down, open, kDt).wheel < 0.0f, "the more deflected stick decides the direction");
|
||||||
|
|
||||||
|
bool closed = false;
|
||||||
|
Controls idle;
|
||||||
|
Check(idle.Update(up, closed, kDt).wheel == 0.0f, "a closed panel does not scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
void HitsMapOntoTheCanvas() {
|
||||||
|
wii_remote::ScreenHit hit{};
|
||||||
|
hit.valid = true;
|
||||||
|
hit.u = -1.0f;
|
||||||
|
hit.v = 1.0f;
|
||||||
|
auto point = CanvasPoint(hit);
|
||||||
|
CheckNear(point[0], 0.0f, "the panel's left edge is canvas x 0");
|
||||||
|
CheckNear(point[1], 0.0f, "the panel's top edge is canvas y 0");
|
||||||
|
hit.u = 1.0f;
|
||||||
|
hit.v = -1.0f;
|
||||||
|
point = CanvasPoint(hit);
|
||||||
|
CheckNear(point[0], kSettingsPanelWidthPixels, "the right edge is the canvas width");
|
||||||
|
CheckNear(point[1], kSettingsPanelHeightPixels, "the bottom edge is the canvas height");
|
||||||
|
|
||||||
|
const auto extents = HalfExtents(2.4f);
|
||||||
|
CheckNear(extents[0], 0.9f, "three quarters of a 2.4 m screen is 1.8 m across");
|
||||||
|
CheckNear(extents[1], 0.675f, "and keeps the canvas's 4:3");
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeAccumulatesWheelUntilTaken() {
|
||||||
|
OpenXRSetSettingsPanelOpen(true);
|
||||||
|
Check(OpenXRSettingsPanelOpen(), "the open flag is shared");
|
||||||
|
OpenXRPublishSettingsPanelPointer(true, 10.0f, 20.0f, false, 0.5f);
|
||||||
|
OpenXRPublishSettingsPanelPointer(true, 11.0f, 21.0f, true, 0.25f);
|
||||||
|
OpenXRSettingsPanelPointer pointer = OpenXRTakeSettingsPanelPointer();
|
||||||
|
Check(pointer.valid && pointer.select, "the latest pointer state is read");
|
||||||
|
CheckNear(pointer.x, 11.0f, "the latest x is read");
|
||||||
|
CheckNear(pointer.wheel, 0.75f, "wheel steps from every XR frame are kept");
|
||||||
|
pointer = OpenXRTakeSettingsPanelPointer();
|
||||||
|
CheckNear(pointer.wheel, 0.0f, "taking the pointer consumes its wheel steps");
|
||||||
|
OpenXRSetSettingsPanelOpen(false);
|
||||||
|
Check(!OpenXRSettingsPanelOpen(), "and can be cleared again");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
ChordOpensAndClosesOnce();
|
||||||
|
MenuClosesAndItsPressStaysOutOfTheGame();
|
||||||
|
SelectWaitsForAReleaseAndFollowsTheTrigger();
|
||||||
|
ThumbstickScrolls();
|
||||||
|
HitsMapOntoTheCanvas();
|
||||||
|
BridgeAccumulatesWheelUntilTaken();
|
||||||
|
if (g_failures != 0) {
|
||||||
|
std::cerr << g_failures << " check(s) failed\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::cout << "vr_settings_panel_tests: all checks passed\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user