mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Implement render scale adjustments for OpenXR D3D12 and Vulkan backends
- Added SetRenderScale method to both D3D12 and Vulkan backends to adjust the render scale dynamically. - Updated eye size calculations to reflect the new render scale in both backends. - Implemented ResizeWritablePair method to handle resizing of swapchains based on the requested eye sizes. - Modified BeginFrame methods to ensure swapchains are resized appropriately before rendering. - Enhanced error handling to maintain current eye sizes when the requested sizes cannot be allocated. - Added tests to validate render scale functionality, ensuring correct behavior when scaling up and down, including handling of refused sizes.
This commit is contained in:
1 parent
f013f9d2f7
commit
e4d198ee13
22 files changed
+779
-48
No files matched your search
@@ -135,8 +135,26 @@ The old Eager Frame Heartbeat option has been removed and existing `eager_frame_
|
||||
settings are ignored. Completed rendering wakes the XR thread immediately. A 50 ms keep-alive
|
||||
still protects pauses and window dragging without eager repeats during rendering.
|
||||
|
||||
`render_scale` scales the per-eye size recommended by the OpenXR runtime. It defaults to 1.0 on PC
|
||||
and 0.8 on the Quest, whose mobile GPU needs the headroom.
|
||||
`render_scale` scales the per-eye size recommended by the OpenXR runtime (0.25 to 2, never above the
|
||||
runtime's maximum). It defaults to 1.0 on PC and 0.8 on the Quest, whose mobile GPU needs the
|
||||
headroom. It is live: **F10 → VR → Render resolution** (also on the headset panel's VR tab) sets it
|
||||
in percent, applies it when the slider is let go, and saves it. Below the slider, *Each eye* gives
|
||||
the left eye's size now and, while they differ, the size the slider's value gives.
|
||||
|
||||
A new scale never interrupts the picture. Each backend keeps two swapchain pairs, one on display and
|
||||
one Aurora writes next, and rebuilds only the second, at the start of the frame that writes it; the
|
||||
other follows a frame later, once it is the one written. The new
|
||||
swapchains are created before the old ones go, and Aurora first forgets the old images: the D3D12
|
||||
bridge waits for any copy still writing them, the Windows Vulkan bridge drains Dawn's queue and drops
|
||||
its wraps of the old `VkImage`s (the runtime may hand the same handles to the new swapchains), and
|
||||
the Quest also replaces its shared eye buffers, whose Dawn imports are released
|
||||
(`aurora_vulkan_forget_stereo_buffers`). Those buffers are kept as large as both pairs, growing with
|
||||
the first pair rebuilt larger and shrinking once the second has followed it down, and each copy moves
|
||||
only what fits the image it writes. If the runtime cannot allocate a size, the log says so, the eyes
|
||||
keep the size they had (a pair already rebuilt goes back to it), and that size is not tried again
|
||||
until the scale changes; the saved value is still what the next launch asks for.
|
||||
`mkw_openxr_replay_tests` and `mkw_openxr_vulkan_replay_tests` cover the rebuild, the display pair
|
||||
left alone, the images forgotten before their swapchains are destroyed, and both kinds of refusal.
|
||||
`world_units_per_meter` controls the scale of headset translation in the game world.
|
||||
`hud_distance_meters` and `hud_width_meters` place and size the virtual screen. They are read at
|
||||
launch and govern both the menu screen and the in-race 2D screen, so 2D content keeps its place
|
||||
|
||||
@@ -84,6 +84,15 @@ bool aurora_d3d12_set_stereo_targets_with_panel(uint64_t frameToken,
|
||||
*/
|
||||
bool aurora_d3d12_cancel_stereo_targets(uint64_t frameToken);
|
||||
|
||||
/**
|
||||
* The host is about to destroy these target resources (the eye swapchains, when
|
||||
* the render resolution changes): returns once no copy the bridge queued can
|
||||
* still write them, waiting for the queue if it has to. Call with no frame
|
||||
* token pending. False when the queue could not be fenced, in which case the
|
||||
* resources must be kept.
|
||||
*/
|
||||
bool aurora_d3d12_forget_stereo_targets(void* const* resources, uint32_t count);
|
||||
|
||||
/**
|
||||
* Removes the sink and drains bridge resources. The worker must be idle. Returns
|
||||
* false when queued work cannot be fenced; in that case the bridge is retained
|
||||
|
||||
@@ -132,6 +132,15 @@ bool aurora_vulkan_set_stereo_targets_with_panel(uint64_t frameToken,
|
||||
*/
|
||||
bool aurora_vulkan_cancel_stereo_targets(uint64_t frameToken);
|
||||
|
||||
/**
|
||||
* Releases Dawn's imports of buffers the caller is about to free (the eyes'
|
||||
* ring, when the render resolution changes), which would otherwise keep every
|
||||
* old buffer alive for the bridge's lifetime. Dawn's own reference to each
|
||||
* buffer lasts until its queue is done with it, so the caller need not wait for
|
||||
* Dawn. Call with no frame token pending; false if one is, releasing nothing.
|
||||
*/
|
||||
bool aurora_vulkan_forget_stereo_buffers(struct AHardwareBuffer* const* buffers, uint32_t count);
|
||||
|
||||
/**
|
||||
* Removes the sink and releases the cached Dawn imports. The worker must be
|
||||
* idle. Returns false only when Dawn could not be drained, in which case the
|
||||
|
||||
@@ -16,6 +16,10 @@ bool aurora_vulkan_win32_set_targets(uint64_t token, const AuroraD3D12StereoTarg
|
||||
bool aurora_vulkan_win32_set_targets_with_panel(uint64_t token, const AuroraD3D12StereoTarget* targets, uint32_t count,
|
||||
const AuroraD3D12StereoTarget* panel);
|
||||
bool aurora_vulkan_win32_cancel(uint64_t token);
|
||||
// As aurora_d3d12_forget_stereo_targets, for VkImages: waits for Dawn's queue,
|
||||
// then drops the textures wrapping them, since the runtime may hand the same
|
||||
// handles out again for the swapchains that replace them.
|
||||
bool aurora_vulkan_win32_forget_targets(void* const* images, uint32_t count);
|
||||
bool aurora_vulkan_win32_disable();
|
||||
void* aurora_vulkan_win32_lock_queue();
|
||||
void aurora_vulkan_win32_unlock_queue(void* guard);
|
||||
|
||||
@@ -319,6 +319,26 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// The command references keep a destroyed swapchain's resources alive, but
|
||||
// not necessarily the runtime's memory behind them, so a copy still writing
|
||||
// one is waited for. Usually the last one finished frames ago.
|
||||
bool ForgetTargets(void* const* resources, uint32_t count) noexcept {
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (m_framePending) {
|
||||
return false;
|
||||
}
|
||||
CollectCompletedCommandsLocked();
|
||||
const auto writesOne = [&](const InFlightCommand& command) {
|
||||
return std::any_of(command.destinations.begin(), command.destinations.end(), [&](const auto& destination) {
|
||||
return destination && std::find(resources, resources + count, destination.Get()) != resources + count;
|
||||
});
|
||||
};
|
||||
if (std::none_of(m_commands.begin(), m_commands.end(), writesOne)) {
|
||||
return true;
|
||||
}
|
||||
return WaitForGpuLocked();
|
||||
}
|
||||
|
||||
private:
|
||||
bool EnsureIntermediate(uint32_t eye, const stereo::EyeImage& source) noexcept {
|
||||
auto& intermediate = m_intermediates[eye];
|
||||
@@ -756,6 +776,14 @@ bool aurora_d3d12_cancel_stereo_targets(uint64_t frameToken) {
|
||||
return g_bridge && g_bridge->CancelBeforeEncode(frameToken);
|
||||
}
|
||||
|
||||
bool aurora_d3d12_forget_stereo_targets(void* const* resources, uint32_t count) {
|
||||
using namespace aurora::d3d12_interop;
|
||||
if (!g_bridge) {
|
||||
return true;
|
||||
}
|
||||
return resources != nullptr && g_bridge->ForgetTargets(resources, count);
|
||||
}
|
||||
|
||||
bool aurora_d3d12_disable_stereo_bridge() {
|
||||
using namespace aurora::d3d12_interop;
|
||||
if (!g_bridge) {
|
||||
@@ -798,6 +826,8 @@ bool aurora_d3d12_set_stereo_targets_with_panel(uint64_t, const AuroraD3D12Stere
|
||||
|
||||
bool aurora_d3d12_cancel_stereo_targets(uint64_t) { return false; }
|
||||
|
||||
bool aurora_d3d12_forget_stereo_targets(void* const*, uint32_t) { return true; }
|
||||
|
||||
bool aurora_d3d12_disable_stereo_bridge() { return true; }
|
||||
|
||||
#endif
|
||||
@@ -93,8 +93,9 @@ bool device_supports_bridge() noexcept {
|
||||
}
|
||||
|
||||
// One AHardwareBuffer imported into Dawn. The import is created on first use
|
||||
// and kept until the bridge is disabled; the ring the OpenXR side recycles is
|
||||
// tiny (two buffers per eye), so this never grows beyond a handful of entries.
|
||||
// and kept until the bridge is disabled or the OpenXR side replaces the buffer
|
||||
// (a new render resolution); the ring it recycles is tiny (two buffers per
|
||||
// eye), so this never grows beyond a handful of entries.
|
||||
struct Import {
|
||||
AHardwareBuffer* buffer = nullptr;
|
||||
wgpu::SharedTextureMemory memory;
|
||||
@@ -254,6 +255,30 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ForgetBuffers(AHardwareBuffer* const* buffers, uint32_t count) noexcept {
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (m_framePending) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const auto found = m_imports.find(buffers[i]);
|
||||
if (found == m_imports.end()) {
|
||||
continue;
|
||||
}
|
||||
auto& import = found->second;
|
||||
if (import.accessBegun) {
|
||||
// Only an abandoned frame leaves one open (see PrepareForDestruction).
|
||||
wgpu::SharedTextureMemoryEndAccessState end{};
|
||||
import.memory.EndAccess(import.texture, &end);
|
||||
}
|
||||
import.texture = nullptr;
|
||||
import.memory = nullptr;
|
||||
AHardwareBuffer_release(import.buffer);
|
||||
m_imports.erase(found);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
// An eye may be smaller than its buffer (the immersive window's eyes are the window only): it is
|
||||
// copied into the buffer's top-left corner, and the OpenXR side shows just that rectangle.
|
||||
@@ -629,6 +654,14 @@ bool aurora_vulkan_cancel_stereo_targets(uint64_t frameToken) {
|
||||
return g_bridge && g_bridge->CancelBeforeEncode(frameToken);
|
||||
}
|
||||
|
||||
bool aurora_vulkan_forget_stereo_buffers(AHardwareBuffer* const* buffers, uint32_t count) {
|
||||
using namespace aurora::vulkan_interop;
|
||||
if (!g_bridge) {
|
||||
return true;
|
||||
}
|
||||
return buffers != nullptr && g_bridge->ForgetBuffers(buffers, count);
|
||||
}
|
||||
|
||||
bool aurora_vulkan_disable_stereo_bridge() {
|
||||
using namespace aurora::vulkan_interop;
|
||||
if (!g_bridge) {
|
||||
@@ -668,6 +701,8 @@ bool aurora_vulkan_set_stereo_targets_with_panel(uint64_t, const AuroraVulkanSte
|
||||
|
||||
bool aurora_vulkan_cancel_stereo_targets(uint64_t) { return false; }
|
||||
|
||||
bool aurora_vulkan_forget_stereo_buffers(struct AHardwareBuffer* const*, uint32_t) { return true; }
|
||||
|
||||
bool aurora_vulkan_disable_stereo_bridge() { return true; }
|
||||
|
||||
#endif
|
||||
@@ -92,6 +92,22 @@ public:
|
||||
if (!guard.owns_lock() || encoded || !token || token != wanted) return false;
|
||||
token = 0; return true;
|
||||
}
|
||||
// Before the runtime destroys these images: the drain retires every copy into
|
||||
// them, and their wraps go because a new swapchain may get the same handles.
|
||||
bool Forget(void* const* images, uint32_t n) {
|
||||
{
|
||||
std::lock_guard guard(mutex);
|
||||
if (token) return false;
|
||||
}
|
||||
if (!api.drain(webgpu::g_device.Get())) return false;
|
||||
std::lock_guard guard(mutex);
|
||||
std::erase_if(imports, [&](const Import& entry) {
|
||||
return std::find(images, images + n, reinterpret_cast<void*>(static_cast<uintptr_t>(entry.image))) !=
|
||||
images + n;
|
||||
});
|
||||
active = {};
|
||||
return true;
|
||||
}
|
||||
bool Encode(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame) {
|
||||
std::lock_guard guard(mutex);
|
||||
if (!token || encoded || frame.frameToken != token) return false;
|
||||
@@ -187,6 +203,11 @@ bool aurora_vulkan_win32_cancel(uint64_t token) {
|
||||
using namespace aurora::vulkan_win32;
|
||||
return bridge && bridge->Cancel(token);
|
||||
}
|
||||
bool aurora_vulkan_win32_forget_targets(void* const* images, uint32_t count) {
|
||||
using namespace aurora::vulkan_win32;
|
||||
if (!bridge) return true;
|
||||
return images && bridge->Forget(images, count);
|
||||
}
|
||||
bool aurora_vulkan_win32_disable() {
|
||||
using namespace aurora::vulkan_win32;
|
||||
if (!bridge) return true;
|
||||
@@ -214,6 +235,7 @@ bool aurora_vulkan_win32_set_targets(uint64_t, const AuroraD3D12StereoTarget*, u
|
||||
bool aurora_vulkan_win32_set_targets_with_panel(uint64_t, const AuroraD3D12StereoTarget*, uint32_t,
|
||||
const AuroraD3D12StereoTarget*) { return false; }
|
||||
bool aurora_vulkan_win32_cancel(uint64_t) { return false; }
|
||||
bool aurora_vulkan_win32_forget_targets(void* const*, uint32_t) { return true; }
|
||||
bool aurora_vulkan_win32_disable() { return true; }
|
||||
void* aurora_vulkan_win32_lock_queue() { return nullptr; }
|
||||
void aurora_vulkan_win32_unlock_queue(void*) {}
|
||||
|
||||
+5
-3
@@ -294,8 +294,9 @@ seat, hand steering, tracked hands, lean back, render scale, VR interpolation,
|
||||
virtual screen size and distance),
|
||||
Graphics (resolution, widescreen, bloom, shader stutter), Controls (controller
|
||||
mode, vibration, the Wii Remote mapping), Audio, and About (paths, OpenXR
|
||||
logging). The launch-time geometry (`render_scale`, `hud_distance_meters`,
|
||||
`hud_width_meters`) is only reachable here, not from the in-headset panel.
|
||||
logging). The launch-time geometry (`hud_distance_meters`, `hud_width_meters`)
|
||||
is only reachable here, not from the in-headset panel; `render_scale` is also the
|
||||
panel's live **VR → Render resolution** slider (OPENXR.md).
|
||||
|
||||
**Patches**, between the two, is the PC launcher's mods page (`PatchesPage`,
|
||||
`ModLibrary`). Import takes one or more picked files, asks for a name and makes
|
||||
@@ -1113,7 +1114,8 @@ or `EndAccess` errors); a black mirror too points at Aurora itself.
|
||||
profiled on the XR2. The first run compiles every bundled pipeline recipe
|
||||
(about half a minute); later runs load Dawn's pipeline cache from `Cache/`
|
||||
next to `DATA`. `render_scale` defaults to 0.8 here (1.0 on
|
||||
PC); lower it further if the compositor reports missed frames.
|
||||
PC); lower it further if the compositor reports missed frames. It can be
|
||||
changed during a race from the headset panel (VR → Render resolution).
|
||||
Foveated rendering (above) is off by default: at `render_scale` 0.8 it saves
|
||||
nothing measurable, above that 8 to 22% of the eyes' GPU time.
|
||||
- **Lifecycle.** Backgrounding (the Quest menu, guardian) pauses the session
|
||||
|
||||
@@ -195,6 +195,10 @@ inline constexpr float kVrRenderScaleDefault = 0.8f;
|
||||
inline constexpr float kVrRenderScaleDefault = 1.0f;
|
||||
#define MKW_VR_RENDER_SCALE_DEFAULT_TEXT "1.0"
|
||||
#endif
|
||||
// The range the file accepts and the F10 bar's slider offers; the Quest
|
||||
// launcher's settings page (SettingsPage.kt) repeats it.
|
||||
inline constexpr float kVrRenderScaleMin = 0.25f;
|
||||
inline constexpr float kVrRenderScaleMax = 2.0f;
|
||||
|
||||
// First-person camera defaults and the range its head offsets accept, in one
|
||||
// place: the config getters, the on-disk template and the F10 bar's reset all
|
||||
@@ -773,7 +777,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.vrEnabled = FindConfigValue<bool>(document, "vr", "enabled");
|
||||
config.vrRequired = FindConfigValue<bool>(document, "vr", "required");
|
||||
if (auto value = FindConfigFloat(document, "vr", "render_scale");
|
||||
value && *value >= 0.25f && *value <= 2.0f) {
|
||||
value && *value >= kVrRenderScaleMin && *value <= kVrRenderScaleMax) {
|
||||
config.vrRenderScale = *value;
|
||||
}
|
||||
if (auto value = FindConfigFloat(document, "vr", "world_units_per_meter");
|
||||
@@ -1180,6 +1184,14 @@ inline bool SetVrRecenterKey(std::string value) {
|
||||
return WriteSetting("vr", "recenter_key", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrRenderScale(float value) {
|
||||
value = std::clamp(value, kVrRenderScaleMin, kVrRenderScaleMax);
|
||||
Mutable().vrRenderScale = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
return WriteSetting("vr", "render_scale", formatted.str());
|
||||
}
|
||||
|
||||
inline bool SetVrLeanBackDegrees(float value) {
|
||||
value = std::clamp(value, -kVrLeanBackDegreesLimit, kVrLeanBackDegreesLimit);
|
||||
Mutable().vrLeanBackDegrees = value;
|
||||
@@ -1578,7 +1590,7 @@ inline bool VrRequired(bool fallback = false) {
|
||||
}
|
||||
|
||||
inline float VrRenderScale(float fallback = kVrRenderScaleDefault) {
|
||||
return std::clamp(Get().vrRenderScale.value_or(fallback), 0.25f, 2.0f);
|
||||
return std::clamp(Get().vrRenderScale.value_or(fallback), kVrRenderScaleMin, kVrRenderScaleMax);
|
||||
}
|
||||
|
||||
inline float VrWorldUnitsPerMeter(float fallback = 500.0f) {
|
||||
|
||||
@@ -54,6 +54,14 @@ public:
|
||||
bool QueryGraphicsRequirements(OpenXRRuntime& runtime);
|
||||
bool BindAurora(OpenXRRuntime& runtime);
|
||||
|
||||
// The eyes' resolution, as a scale of the size the runtime recommends (the
|
||||
// session starts at OpenXRConfig::resolution_scale). A new one applies from
|
||||
// the next BeginFrame or PreparePacket: each swapchain pair is rebuilt at it
|
||||
// the next time it is the pair Aurora writes, so the pair on display is
|
||||
// never touched. A size the runtime cannot allocate keeps the one before,
|
||||
// until the scale changes again.
|
||||
void SetRenderScale(float scale);
|
||||
|
||||
OpenXRD3D12BeginStatus BeginFrame(const OpenXRD3D12Presentation& presentation,
|
||||
OpenXRD3D12Frame& frame);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace mkw::vr {
|
||||
@@ -74,6 +75,24 @@ void OpenXRSetPassthrough(bool enabled) noexcept;
|
||||
// altogether, makes it moot.
|
||||
void OpenXRSetImmersiveWindow(bool enabled) noexcept;
|
||||
|
||||
// The headset's eye resolution, as a scale of the size the OpenXR runtime
|
||||
// recommends (clamped to RuntimeConfigFile::kVrRenderScaleMin..Max). Callable
|
||||
// from any thread. The pacing thread rebuilds the eye swapchains at it over the
|
||||
// next two frames, never touching the one on display; a size the runtime
|
||||
// cannot allocate keeps the eyes as they are.
|
||||
void OpenXRSetRenderScale(float scale) noexcept;
|
||||
|
||||
// The left eye's image size: width x height is what the headset is shown now,
|
||||
// scaled_width x scaled_height what `scale` gives on this headset. All zero
|
||||
// while no OpenXR session runs.
|
||||
struct OpenXREyeResolution {
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t scaled_width = 0;
|
||||
uint32_t scaled_height = 0;
|
||||
};
|
||||
OpenXREyeResolution OpenXRGetEyeResolution(float scale) noexcept;
|
||||
|
||||
// Live scene interpolation at the headset's own display deadlines.
|
||||
// 0 = Off, 1 = Auto, otherwise 72/90/120 as a rendering-rate ceiling.
|
||||
void OpenXRSetFrameInterpolationFps(uint32_t target) noexcept;
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
#include "vr/openxr_config.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -41,6 +43,28 @@ struct OpenXRViewConfiguration {
|
||||
uint32_t render_height = 0;
|
||||
};
|
||||
|
||||
struct OpenXREyeSize {
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
|
||||
bool operator==(const OpenXREyeSize& other) const noexcept {
|
||||
return width == other.width && height == other.height;
|
||||
}
|
||||
bool operator!=(const OpenXREyeSize& other) const noexcept { return !(*this == other); }
|
||||
};
|
||||
|
||||
// A view's image at `scale` of the size the runtime recommends, never below one
|
||||
// pixel or above the runtime's maximum. The size the session starts at, and the
|
||||
// one a backend rebuilds its eyes at when the render scale changes live.
|
||||
inline OpenXREyeSize OpenXRScaledEyeSize(const XrViewConfigurationView& view, float scale) noexcept {
|
||||
const auto scaled = [scale](uint32_t recommended, uint32_t maximum) {
|
||||
const double value = std::round(static_cast<double>(recommended) * static_cast<double>(scale));
|
||||
return static_cast<uint32_t>(std::clamp(value, 1.0, static_cast<double>(std::max(maximum, 1u))));
|
||||
};
|
||||
return {scaled(view.recommendedImageRectWidth, view.maxImageRectWidth),
|
||||
scaled(view.recommendedImageRectHeight, view.maxImageRectHeight)};
|
||||
}
|
||||
|
||||
struct OpenXRRuntimeInfo {
|
||||
std::string runtime_name;
|
||||
XrVersion runtime_version = 0;
|
||||
|
||||
@@ -51,6 +51,10 @@ public:
|
||||
bool QueryGraphicsRequirements(OpenXRRuntime& runtime);
|
||||
bool BindAurora(OpenXRRuntime& runtime);
|
||||
|
||||
// As OpenXRD3D12Backend::SetRenderScale. The shared eye buffers are
|
||||
// reallocated along with the first pair rebuilt at the new size.
|
||||
void SetRenderScale(float scale);
|
||||
|
||||
OpenXRBeginStatus BeginFrame(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame);
|
||||
OpenXRSubmissionStatus WaitForSubmission(const OpenXRBackendFrame& frame,
|
||||
uint32_t timeout_ms = UINT32_MAX);
|
||||
|
||||
@@ -51,6 +51,9 @@ public:
|
||||
bool QueryGraphicsRequirements(OpenXRRuntime& runtime);
|
||||
bool BindAurora(OpenXRRuntime& runtime);
|
||||
|
||||
// As OpenXRD3D12Backend::SetRenderScale.
|
||||
void SetRenderScale(float scale);
|
||||
|
||||
OpenXRWindowsVulkanBeginStatus BeginFrame(const OpenXRWindowsVulkanPresentation& presentation,
|
||||
OpenXRWindowsVulkanFrame& frame);
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ public:
|
||||
bool BindAurora(OpenXRRuntime& runtime) {
|
||||
return std::visit([&](auto& backend) -> bool { return backend->BindAurora(runtime); }, backend_);
|
||||
}
|
||||
void SetRenderScale(float scale) {
|
||||
std::visit([&](auto& backend) { backend->SetRenderScale(scale); }, backend_);
|
||||
}
|
||||
OpenXRBeginStatus BeginFrame(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame) {
|
||||
return std::visit([&](auto& backend) -> OpenXRBeginStatus { return backend->BeginFrame(presentation, frame); }, backend_);
|
||||
}
|
||||
|
||||
@@ -238,6 +238,8 @@ SDL_Scancode g_vrRecenterScancode = [] {
|
||||
}();
|
||||
bool g_vrRecenterRebinding = false;
|
||||
float g_vrLeanBackDegrees = RuntimeConfigFile::VrLeanBackDegrees();
|
||||
// [vr] render_scale as the slider shows it, in percent of the headset's recommended size.
|
||||
int g_vrRenderScalePercent = static_cast<int>(std::lround(RuntimeConfigFile::VrRenderScale() * 100.0f));
|
||||
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths();
|
||||
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
|
||||
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
|
||||
@@ -1419,6 +1421,35 @@ void DrawVrSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// Live, and usable without a session like the mirror above. It applies when the slider is let
|
||||
// go rather than at every step of a drag, since each new size means new eye swapchains.
|
||||
ImGui::SliderInt("Render resolution", &g_vrRenderScalePercent,
|
||||
static_cast<int>(RuntimeConfigFile::kVrRenderScaleMin * 100.0f),
|
||||
static_cast<int>(RuntimeConfigFile::kVrRenderScaleMax * 100.0f), "%d%%",
|
||||
ImGuiSliderFlags_AlwaysClamp);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
const float scale = static_cast<float>(g_vrRenderScalePercent) / 100.0f;
|
||||
mkw::vr::OpenXRSetRenderScale(scale);
|
||||
RuntimeConfigFile::SetVrRenderScale(scale);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"How many pixels each eye is rendered with, as a share of what your headset's OpenXR "
|
||||
"runtime recommends. Lower is faster, higher is sharper, and the GPU's work grows with the "
|
||||
"pixel count: 150%% draws about 2.25 times as many as 100%%.\n"
|
||||
"Applies when you let go of the slider, without restarting; the headset keeps showing "
|
||||
"the game while the eyes change size.");
|
||||
}
|
||||
if (const auto eyes = mkw::vr::OpenXRGetEyeResolution(static_cast<float>(g_vrRenderScalePercent) / 100.0f);
|
||||
eyes.width != 0) {
|
||||
if (eyes.width == eyes.scaled_width && eyes.height == eyes.scaled_height) {
|
||||
ImGui::TextDisabled("Each eye: %u x %u", eyes.width, eyes.height);
|
||||
} else {
|
||||
ImGui::TextDisabled("Each eye: %u x %u now, %u x %u at %d%%", eyes.width, eyes.height, eyes.scaled_width,
|
||||
eyes.scaled_height, g_vrRenderScalePercent);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::Combo("VR frame interpolation (experimental)", &g_vrFrameInterpolationMode,
|
||||
kVrInterpolationLabels.data(), static_cast<int>(kVrInterpolationLabels.size()))) {
|
||||
const auto target = kVrInterpolationFps[static_cast<size_t>(g_vrFrameInterpolationMode)];
|
||||
|
||||
@@ -195,6 +195,11 @@ public:
|
||||
}
|
||||
owns_session_ = true;
|
||||
aurora_format_ = static_cast<DXGI_FORMAT>(handles.colorDxgiFormat);
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const auto& view = runtime.ViewConfiguration()[eye];
|
||||
eye_size_[eye] = {view.render_width, view.render_height};
|
||||
}
|
||||
requested_eye_size_ = eye_size_;
|
||||
|
||||
if (!SelectSwapchainFormat() || !CreateSwapchains()) {
|
||||
DestroySwapchains();
|
||||
@@ -226,6 +231,21 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetRenderScale(float scale) {
|
||||
if (runtime_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> requested{};
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
requested[eye] = OpenXRScaledEyeSize(runtime_->ViewConfiguration()[eye].properties, scale);
|
||||
}
|
||||
// Only a change: a refused size stays refused while it is still the one asked for.
|
||||
if (requested != requested_eye_size_) {
|
||||
requested_eye_size_ = requested;
|
||||
eye_size_ = requested;
|
||||
}
|
||||
}
|
||||
|
||||
OpenXRD3D12BeginStatus BeginFrame(const OpenXRD3D12Presentation& presentation,
|
||||
OpenXRD3D12Frame& frame) {
|
||||
frame = {};
|
||||
@@ -238,6 +258,9 @@ public:
|
||||
Fail("BeginFrame called while another OpenXR frame is active");
|
||||
return OpenXRD3D12BeginStatus::Error;
|
||||
}
|
||||
if (!ResizeWritablePair()) {
|
||||
return OpenXRD3D12BeginStatus::Error;
|
||||
}
|
||||
|
||||
const OpenXRFrameStatus status = runtime_->WaitFrame(frame.xr_frame);
|
||||
if (status != OpenXRFrameStatus::Ready) {
|
||||
@@ -372,6 +395,7 @@ public:
|
||||
const auto status = KeepAliveCycle();
|
||||
if (status != OpenXRBeginStatus::Ready) return status;
|
||||
}
|
||||
if (!ResizeWritablePair()) return OpenXRBeginStatus::Error;
|
||||
packet.xr_frame.serial = next_packet_serial_++;
|
||||
packet.xr_frame.predicted_display_time = last_display_time_ + 2 * last_display_period_;
|
||||
packet.xr_frame.predicted_display_period = last_display_period_;
|
||||
@@ -814,8 +838,7 @@ private:
|
||||
|
||||
bool CreateSwapchainPair(std::array<EyeSwapchain, kOpenXREyeCount>& pair) {
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const auto& view = runtime_->ViewConfiguration()[eye];
|
||||
if (!CreateSwapchain(pair[eye], view.render_width, view.render_height,
|
||||
if (!CreateSwapchain(pair[eye], eye_size_[eye].width, eye_size_[eye].height,
|
||||
eye == 0 ? "left eye" : "right eye")) {
|
||||
return false;
|
||||
}
|
||||
@@ -823,6 +846,53 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rebuilds the pair Aurora writes next at eye_size_ when it is another size. Only that pair,
|
||||
// with none of its images acquired, never the one on display. The new swapchains are made
|
||||
// before the old ones go, so a size the runtime cannot allocate leaves the pair as it was.
|
||||
// False only on a failure that ends the session.
|
||||
bool ResizeWritablePair() {
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> current{};
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
if (eye_swapchains_[eye].acquired) {
|
||||
return true;
|
||||
}
|
||||
current[eye] = {eye_swapchains_[eye].width, eye_swapchains_[eye].height};
|
||||
}
|
||||
if (current == eye_size_) {
|
||||
return true;
|
||||
}
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> replacement{};
|
||||
if (!CreateSwapchainPair(replacement)) {
|
||||
DestroySwapchainPair(replacement);
|
||||
std::ostringstream message;
|
||||
message << "OpenXR D3D12 eyes stay " << current[0].width << 'x' << current[0].height
|
||||
<< ": the runtime could not make " << eye_size_[0].width << 'x' << eye_size_[0].height
|
||||
<< " swapchains (" << last_error_ << ')';
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
ClearError();
|
||||
// Back to this pair's size, which also returns the other pair to it if it was rebuilt.
|
||||
eye_size_ = current;
|
||||
return true;
|
||||
}
|
||||
std::vector<void*> retired;
|
||||
for (const EyeSwapchain& swapchain : eye_swapchains_) {
|
||||
for (const auto& image : swapchain.images) {
|
||||
retired.push_back(image.texture);
|
||||
}
|
||||
}
|
||||
if (!aurora_d3d12_forget_stereo_targets(retired.data(), static_cast<uint32_t>(retired.size()))) {
|
||||
DestroySwapchainPair(replacement);
|
||||
return Fail("Aurora could not retire its copies into the old D3D12 eye swapchains");
|
||||
}
|
||||
DestroySwapchainPair(eye_swapchains_);
|
||||
eye_swapchains_ = std::move(replacement);
|
||||
std::ostringstream message;
|
||||
message << "OpenXR D3D12 eyes resized: " << eye_size_[0].width << 'x' << eye_size_[0].height << " / "
|
||||
<< eye_size_[1].width << 'x' << eye_size_[1].height;
|
||||
Log(OpenXRLogLevel::Info, message.str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CreateSwapchain(EyeSwapchain& swapchain, uint32_t width, uint32_t height, const char* what) {
|
||||
swapchain.width = width;
|
||||
swapchain.height = height;
|
||||
@@ -1063,6 +1133,10 @@ private:
|
||||
OpenXRD3D12GraphicsRequirements requirements_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> eye_swapchains_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> retained_swapchains_{};
|
||||
// The size a rebuilt eye pair gets, and the one the render scale last asked
|
||||
// for, which differ while a size the runtime refused is being kept.
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> eye_size_{};
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> requested_eye_size_{};
|
||||
// The settings panel's layer: written like the eyes into panel_swapchain_,
|
||||
// shown from retained_panel_swapchain_ (see FinishFrame).
|
||||
EyeSwapchain panel_swapchain_{};
|
||||
@@ -1119,6 +1193,8 @@ bool OpenXRD3D12Backend::BindAurora(OpenXRRuntime& runtime) {
|
||||
return m_impl->BindAurora(runtime);
|
||||
}
|
||||
|
||||
void OpenXRD3D12Backend::SetRenderScale(float scale) { m_impl->SetRenderScale(scale); }
|
||||
|
||||
OpenXRD3D12BeginStatus OpenXRD3D12Backend::BeginFrame(
|
||||
const OpenXRD3D12Presentation& presentation, OpenXRD3D12Frame& frame) {
|
||||
return m_impl->BeginFrame(presentation, frame);
|
||||
|
||||
@@ -474,6 +474,13 @@ public:
|
||||
ResetPreparedObjects();
|
||||
return false;
|
||||
}
|
||||
{
|
||||
const OpenXRViewConfiguration& left = runtime_->ViewConfiguration()[0];
|
||||
std::lock_guard lock(eye_view_mutex_);
|
||||
eye_view_ = left.properties;
|
||||
eye_width_.store(left.render_width, std::memory_order_relaxed);
|
||||
eye_height_.store(left.render_height, std::memory_order_relaxed);
|
||||
}
|
||||
input_ = std::make_unique<OpenXRInput>(logger_);
|
||||
if (!input_->Create(*runtime_)) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR controller input unavailable: " << input_->LastError()
|
||||
@@ -563,6 +570,12 @@ public:
|
||||
rendered_fps_.store(0, std::memory_order_relaxed);
|
||||
interpolation_available_.store(false, std::memory_order_release);
|
||||
hand_tracking_available_.store(false, std::memory_order_release);
|
||||
{
|
||||
std::lock_guard lock(eye_view_mutex_);
|
||||
eye_view_ = {XR_TYPE_VIEW_CONFIGURATION_VIEW};
|
||||
eye_width_.store(0, std::memory_order_relaxed);
|
||||
eye_height_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
ResetTrackingOrigin();
|
||||
applied_session_run_serial_ = 0;
|
||||
session_was_active_ = false;
|
||||
@@ -605,6 +618,23 @@ public:
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void SetRenderScale(float scale) noexcept {
|
||||
render_scale_.store(ClampRenderScale(scale), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
OpenXREyeResolution EyeResolution(float scale) const noexcept {
|
||||
OpenXREyeResolution resolution{};
|
||||
std::lock_guard lock(eye_view_mutex_);
|
||||
resolution.width = eye_width_.load(std::memory_order_relaxed);
|
||||
resolution.height = eye_height_.load(std::memory_order_relaxed);
|
||||
if (resolution.width != 0) {
|
||||
const OpenXREyeSize scaled = OpenXRScaledEyeSize(eye_view_, ClampRenderScale(scale));
|
||||
resolution.scaled_width = scaled.width;
|
||||
resolution.scaled_height = scaled.height;
|
||||
}
|
||||
return resolution;
|
||||
}
|
||||
|
||||
void ServiceProducerFrameBoundary() noexcept {
|
||||
if (teardown_requested_.load(std::memory_order_acquire)) {
|
||||
Shutdown();
|
||||
@@ -631,6 +661,17 @@ private:
|
||||
// at the headset's refresh rate.
|
||||
static constexpr uint32_t kMaxConsecutiveSkips = 300;
|
||||
|
||||
static float ClampRenderScale(float scale) noexcept {
|
||||
return std::clamp(scale, RuntimeConfigFile::kVrRenderScaleMin, RuntimeConfigFile::kVrRenderScaleMax);
|
||||
}
|
||||
|
||||
// The size the eyes are rendered at now, for the settings: the pair being written, before the
|
||||
// immersive window may aim its eyes through the window.
|
||||
void NoteEyeSize(const OpenXRBackendFrame& frame) noexcept {
|
||||
eye_width_.store(frame.render_width[0], std::memory_order_relaxed);
|
||||
eye_height_.store(frame.render_height[0], std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool BackendMatchesConfiguredGraphicsApi(const AuroraConfig& aurora_config) {
|
||||
#if defined(_WIN32)
|
||||
kRequiredAuroraBackend = aurora_config.desiredBackend == BACKEND_VULKAN ? BACKEND_VULKAN : BACKEND_D3D12;
|
||||
@@ -927,6 +968,8 @@ private:
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR " << kGraphicsBackendName << " pacing: "
|
||||
<< (render_first ? "render-first" : "frame-first (VR interpolation)") << std::endl;
|
||||
}
|
||||
// A new scale rebuilds the eyes as the backend next prepares them.
|
||||
backend_->SetRenderScale(render_scale_.load(std::memory_order_relaxed));
|
||||
if (render_first) {
|
||||
if (!RenderFirstCycle(presentation, policy, immersive, consecutive_skips,
|
||||
immersive_submission_logged)) {
|
||||
@@ -949,6 +992,7 @@ private:
|
||||
fatal = true;
|
||||
break;
|
||||
}
|
||||
NoteEyeSize(frame);
|
||||
|
||||
UpdateFrameTiming(frame.xr_frame);
|
||||
if (diagnostics::Enabled()) {
|
||||
@@ -1140,6 +1184,7 @@ private:
|
||||
SetError(backend_->LastError());
|
||||
return false;
|
||||
}
|
||||
NoteEyeSize(packet);
|
||||
// The head pose this packet was located with places the screens and aims the pointer.
|
||||
ServiceRecenterRequest();
|
||||
UpdateVirtualScreenPose(packet);
|
||||
@@ -1917,6 +1962,13 @@ private:
|
||||
std::atomic_bool teardown_requested_{false};
|
||||
std::atomic_bool recenter_requested_{false};
|
||||
std::atomic<float> lean_back_degrees_{RuntimeConfigFile::VrLeanBackDegrees()};
|
||||
std::atomic<float> render_scale_{RuntimeConfigFile::VrRenderScale()};
|
||||
// The left eye for OpenXRGetEyeResolution: the runtime's description of it, set while a
|
||||
// session runs, and the size it is rendered at now (0 without a session).
|
||||
mutable std::mutex eye_view_mutex_;
|
||||
XrViewConfigurationView eye_view_{XR_TYPE_VIEW_CONFIGURATION_VIEW};
|
||||
std::atomic_uint32_t eye_width_{0};
|
||||
std::atomic_uint32_t eye_height_{0};
|
||||
std::atomic_bool passthrough_{RuntimeConfigFile::VrPassthrough()};
|
||||
std::atomic_bool immersive_window_{RuntimeConfigFile::VrImmersiveWindow()};
|
||||
std::atomic_uint32_t frame_interpolation_fps_{RuntimeConfigFile::VrFrameInterpolationFps()};
|
||||
@@ -2049,6 +2101,23 @@ void OpenXRSetImmersiveWindow(bool enabled) noexcept {
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenXRSetRenderScale(float scale) noexcept {
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
OpenXRIntegration::Get().SetRenderScale(scale);
|
||||
#else
|
||||
(void)scale;
|
||||
#endif
|
||||
}
|
||||
|
||||
OpenXREyeResolution OpenXRGetEyeResolution(float scale) noexcept {
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
return OpenXRIntegration::Get().EyeResolution(scale);
|
||||
#else
|
||||
(void)scale;
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenXRSetFrameInterpolationFps(uint32_t target) noexcept {
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
OpenXRIntegration::Get().SetFrameInterpolationFps(target);
|
||||
|
||||
@@ -48,14 +48,6 @@ bool IsFinitePositive(float value) noexcept {
|
||||
(bits & 0x7f800000u) != 0x7f800000u;
|
||||
}
|
||||
|
||||
uint32_t ScaledDimension(uint32_t recommended, uint32_t maximum, float scale) {
|
||||
const double scaled = std::round(static_cast<double>(recommended) *
|
||||
static_cast<double>(scale));
|
||||
const double clamped = std::clamp(
|
||||
scaled, 1.0, static_cast<double>(std::max(maximum, 1u)));
|
||||
return static_cast<uint32_t>(clamped);
|
||||
}
|
||||
|
||||
const char* ReferenceSpaceName(XrReferenceSpaceType type) {
|
||||
switch (type) {
|
||||
case XR_REFERENCE_SPACE_TYPE_VIEW:
|
||||
@@ -309,12 +301,9 @@ bool OpenXRRuntime::EnumerateViewConfiguration() {
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
OpenXRViewConfiguration& destination = m_view_configuration[eye];
|
||||
destination.properties = properties[eye];
|
||||
destination.render_width = ScaledDimension(
|
||||
properties[eye].recommendedImageRectWidth,
|
||||
properties[eye].maxImageRectWidth, m_config.resolution_scale);
|
||||
destination.render_height = ScaledDimension(
|
||||
properties[eye].recommendedImageRectHeight,
|
||||
properties[eye].maxImageRectHeight, m_config.resolution_scale);
|
||||
const OpenXREyeSize size = OpenXRScaledEyeSize(properties[eye], m_config.resolution_scale);
|
||||
destination.render_width = size.width;
|
||||
destination.render_height = size.height;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,9 @@ public:
|
||||
// The settings panel's layer image follows the eyes.
|
||||
bool panel = false;
|
||||
std::array<VkImage, kMaxCopies> swapchain_images{};
|
||||
// Each image's size. A shared buffer can be larger than the image while
|
||||
// the render resolution is changing, and a copy moves only what fits.
|
||||
std::array<VkExtent2D, kMaxCopies> swapchain_extents{};
|
||||
|
||||
uint32_t Count() const noexcept { return target_count + (panel ? 1u : 0u); }
|
||||
};
|
||||
@@ -291,6 +294,11 @@ public:
|
||||
return Fail("OpenXR rejected the Vulkan device binding");
|
||||
}
|
||||
owns_session_ = true;
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const auto& view = runtime.ViewConfiguration()[eye];
|
||||
eye_size_[eye] = {view.render_width, view.render_height};
|
||||
}
|
||||
requested_eye_size_ = eye_size_;
|
||||
|
||||
if (!SelectSwapchainFormat() || !CreateSwapchains() || !AllocateSlots()) {
|
||||
DestroySwapchains();
|
||||
@@ -329,6 +337,21 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetRenderScale(float scale) {
|
||||
if (runtime_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> requested{};
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
requested[eye] = OpenXRScaledEyeSize(runtime_->ViewConfiguration()[eye].properties, scale);
|
||||
}
|
||||
// Only a change: a refused size stays refused while it is still the one asked for.
|
||||
if (requested != requested_eye_size_) {
|
||||
requested_eye_size_ = requested;
|
||||
eye_size_ = requested;
|
||||
}
|
||||
}
|
||||
|
||||
OpenXRBeginStatus BeginFrame(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame) {
|
||||
frame = {};
|
||||
frame.presentation = presentation;
|
||||
@@ -340,6 +363,9 @@ public:
|
||||
Fail("BeginFrame called while another OpenXR frame is active");
|
||||
return OpenXRBeginStatus::Error;
|
||||
}
|
||||
if (!ResizeWritablePair()) {
|
||||
return OpenXRBeginStatus::Error;
|
||||
}
|
||||
|
||||
const OpenXRFrameStatus status = runtime_->WaitFrame(frame.xr_frame);
|
||||
if (status != OpenXRFrameStatus::Ready) {
|
||||
@@ -423,14 +449,7 @@ public:
|
||||
panel_target = SlotTargetLocked(panel_slots_[slot]);
|
||||
}
|
||||
pending_copy_ = {frame.xr_frame.serial, slot, target_count, panel, {}};
|
||||
for (uint32_t eye = 0; eye < target_count; ++eye) {
|
||||
pending_copy_.swapchain_images[eye] =
|
||||
eye_swapchains_[eye].images[eye_swapchains_[eye].acquired_index].image;
|
||||
}
|
||||
if (panel) {
|
||||
pending_copy_.swapchain_images[target_count] =
|
||||
panel_swapchain_.images[panel_swapchain_.acquired_index].image;
|
||||
}
|
||||
RecordCopyImagesLocked(target_count, panel);
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(submission_mutex_);
|
||||
@@ -543,6 +562,9 @@ public:
|
||||
return primed;
|
||||
}
|
||||
}
|
||||
if (!ResizeWritablePair()) {
|
||||
return OpenXRBeginStatus::Error;
|
||||
}
|
||||
packet.xr_frame.serial = next_packet_serial_++;
|
||||
// The eyes are ready after at most one game frame plus the encode and show at the first
|
||||
// display slot after that: two periods past the last predicted display time.
|
||||
@@ -774,14 +796,7 @@ public:
|
||||
diagnostics::OnSwapchainAcquire(acquire_timer);
|
||||
{
|
||||
std::lock_guard lock(vk_mutex_);
|
||||
for (uint32_t eye = 0; eye < target_count; ++eye) {
|
||||
pending_copy_.swapchain_images[eye] =
|
||||
eye_swapchains_[eye].images[eye_swapchains_[eye].acquired_index].image;
|
||||
}
|
||||
if (panel) {
|
||||
pending_copy_.swapchain_images[target_count] =
|
||||
panel_swapchain_.images[panel_swapchain_.acquired_index].image;
|
||||
}
|
||||
RecordCopyImagesLocked(target_count, panel);
|
||||
}
|
||||
return OpenXRBeginStatus::Ready;
|
||||
}
|
||||
@@ -1568,6 +1583,20 @@ private:
|
||||
self->submission_cv_.notify_all();
|
||||
}
|
||||
|
||||
// The acquired compositor images the pending copy writes: the eyes', then the panel's.
|
||||
void RecordCopyImagesLocked(uint32_t target_count, bool panel) noexcept {
|
||||
const auto record = [this](uint32_t n, const EyeSwapchain& swapchain) {
|
||||
pending_copy_.swapchain_images[n] = swapchain.images[swapchain.acquired_index].image;
|
||||
pending_copy_.swapchain_extents[n] = {swapchain.width, swapchain.height};
|
||||
};
|
||||
for (uint32_t eye = 0; eye < target_count; ++eye) {
|
||||
record(eye, eye_swapchains_[eye]);
|
||||
}
|
||||
if (panel) {
|
||||
record(target_count, panel_swapchain_);
|
||||
}
|
||||
}
|
||||
|
||||
// The slot image `n` of a copy reads: an eye's, or after the eyes the panel's.
|
||||
EyeSlot& CopySlotLocked(const PendingCopy& copy, uint32_t n) noexcept {
|
||||
return n < copy.target_count ? slots_[n][copy.slot] : panel_slots_[copy.slot];
|
||||
@@ -1670,7 +1699,8 @@ private:
|
||||
VkImageCopy region{};
|
||||
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.extent = {slot.width, slot.height, 1};
|
||||
region.extent = {std::min(slot.width, copy.swapchain_extents[n].width),
|
||||
std::min(slot.height, copy.swapchain_extents[n].height), 1};
|
||||
vkCmdCopyImage(cmd, slot.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, copy.swapchain_images[n],
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
||||
|
||||
@@ -1804,8 +1834,7 @@ private:
|
||||
|
||||
bool CreateSwapchainPair(std::array<EyeSwapchain, kOpenXREyeCount>& pair) {
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const auto& view = runtime_->ViewConfiguration()[eye];
|
||||
if (!CreateSwapchain(pair[eye], view.render_width, view.render_height,
|
||||
if (!CreateSwapchain(pair[eye], eye_size_[eye].width, eye_size_[eye].height,
|
||||
eye == 0 ? "left eye" : "right eye")) {
|
||||
return false;
|
||||
}
|
||||
@@ -1813,6 +1842,121 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
// As the D3D12 backend's, with the shared eye buffers, which hold whichever pair is written
|
||||
// and so are kept as large as both pairs: they grow with the first pair rebuilt larger and
|
||||
// shrink once the second has followed it down (each copy moves only what fits the image it
|
||||
// writes). Everything new is allocated before anything old goes, so a size that cannot be had
|
||||
// leaves the eyes as they were.
|
||||
bool ResizeWritablePair() {
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> current{};
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
if (eye_swapchains_[eye].acquired) {
|
||||
return true;
|
||||
}
|
||||
current[eye] = {eye_swapchains_[eye].width, eye_swapchains_[eye].height};
|
||||
}
|
||||
if (current == eye_size_) {
|
||||
return true;
|
||||
}
|
||||
const auto keep_current = [&](const char* what) {
|
||||
std::ostringstream message;
|
||||
message << "OpenXR Vulkan eyes stay " << current[0].width << 'x' << current[0].height
|
||||
<< ": could not make " << eye_size_[0].width << 'x' << eye_size_[0].height << ' ' << what
|
||||
<< " (" << last_error_ << ')';
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
ClearError();
|
||||
// Back to this pair's size, which also returns the other pair to it if it was rebuilt.
|
||||
eye_size_ = current;
|
||||
};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> replacement{};
|
||||
if (!CreateSwapchainPair(replacement)) {
|
||||
DestroySwapchainPair(replacement);
|
||||
keep_current("swapchains");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::lock_guard lock(vk_mutex_);
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> needed{};
|
||||
std::array<bool, kOpenXREyeCount> new_slots{};
|
||||
bool must_grow = false;
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
needed[eye] = {std::max(eye_size_[eye].width, retained_swapchains_[eye].width),
|
||||
std::max(eye_size_[eye].height, retained_swapchains_[eye].height)};
|
||||
const OpenXREyeSize held{slots_[eye][0].width, slots_[eye][0].height};
|
||||
new_slots[eye] = needed[eye] != held;
|
||||
must_grow |= needed[eye].width > held.width || needed[eye].height > held.height;
|
||||
}
|
||||
decltype(slots_) slots{};
|
||||
const auto discard_new_slots = [&] {
|
||||
for (auto& eye : slots) {
|
||||
for (EyeSlot& slot : eye) {
|
||||
DestroySlotLocked(slot);
|
||||
}
|
||||
}
|
||||
new_slots = {};
|
||||
};
|
||||
bool allocated = true;
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
if (new_slots[eye]) {
|
||||
for (EyeSlot& slot : slots[eye]) {
|
||||
allocated = allocated && AllocateSlot(slot, needed[eye].width, needed[eye].height);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!allocated) {
|
||||
discard_new_slots();
|
||||
if (must_grow) {
|
||||
DestroySwapchainPair(replacement);
|
||||
keep_current("shared buffers");
|
||||
return true;
|
||||
}
|
||||
// Smaller buffers would only give memory back; the larger ones still hold both pairs.
|
||||
Log(OpenXRLogLevel::Warning, "OpenXR Vulkan keeps its larger shared eye buffers: " + last_error_);
|
||||
ClearError();
|
||||
}
|
||||
// This device's copies out of the old buffers and into the old images are done after this.
|
||||
const VkResult idle = vkDeviceWaitIdle(vk_device_);
|
||||
if (idle != VK_SUCCESS) {
|
||||
discard_new_slots();
|
||||
DestroySwapchainPair(replacement);
|
||||
return Fail(VkFailure("vkDeviceWaitIdle failed before resizing the eyes", idle));
|
||||
}
|
||||
std::vector<AHardwareBuffer*> retired;
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
if (new_slots[eye]) {
|
||||
for (EyeSlot& slot : slots_[eye]) {
|
||||
retired.push_back(slot.buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!retired.empty()) {
|
||||
if (!aurora_vulkan_forget_stereo_buffers(retired.data(), static_cast<uint32_t>(retired.size()))) {
|
||||
discard_new_slots();
|
||||
DestroySwapchainPair(replacement);
|
||||
return Fail("Aurora could not release the old shared eye buffers");
|
||||
}
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
if (!new_slots[eye]) {
|
||||
continue;
|
||||
}
|
||||
for (uint32_t slot = 0; slot < kSlotCount; ++slot) {
|
||||
DestroySlotLocked(slots_[eye][slot]);
|
||||
slots_[eye][slot] = slots[eye][slot];
|
||||
}
|
||||
}
|
||||
}
|
||||
DestroySwapchainPair(eye_swapchains_);
|
||||
eye_swapchains_ = std::move(replacement);
|
||||
std::ostringstream message;
|
||||
message << "OpenXR Vulkan eyes resized: " << eye_size_[0].width << 'x' << eye_size_[0].height << " / "
|
||||
<< eye_size_[1].width << 'x' << eye_size_[1].height;
|
||||
if (!retired.empty()) {
|
||||
message << ", shared buffers " << slots_[0][0].width << 'x' << slots_[0][0].height;
|
||||
}
|
||||
Log(OpenXRLogLevel::Info, message.str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CreateSwapchain(EyeSwapchain& swapchain, uint32_t width, uint32_t height, const char* what) {
|
||||
swapchain.width = width;
|
||||
swapchain.height = height;
|
||||
@@ -2030,6 +2174,9 @@ private:
|
||||
OpenXRVulkanGraphicsRequirements requirements_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> eye_swapchains_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> retained_swapchains_{};
|
||||
// As the D3D12 backend's.
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> eye_size_{};
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> requested_eye_size_{};
|
||||
// The settings panel's layer: written like the eyes into panel_swapchain_,
|
||||
// shown from retained_panel_swapchain_ (see FinishFrame).
|
||||
EyeSwapchain panel_swapchain_{};
|
||||
@@ -2110,6 +2257,8 @@ bool OpenXRVulkanBackend::BindAurora(OpenXRRuntime& runtime) {
|
||||
return m_impl->BindAurora(runtime);
|
||||
}
|
||||
|
||||
void OpenXRVulkanBackend::SetRenderScale(float scale) { m_impl->SetRenderScale(scale); }
|
||||
|
||||
OpenXRBeginStatus OpenXRVulkanBackend::BeginFrame(const OpenXRPresentation& presentation,
|
||||
OpenXRBackendFrame& frame) {
|
||||
return m_impl->BeginFrame(presentation, frame);
|
||||
|
||||
@@ -216,6 +216,11 @@ public:
|
||||
if (!runtime.CreateSession(&binding)) return Fail("OpenXR rejected Dawn's Vulkan device binding");
|
||||
owns_session_ = true;
|
||||
aurora_format_ = static_cast<VkFormat>(format);
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const auto& view = runtime.ViewConfiguration()[eye];
|
||||
eye_size_[eye] = {view.render_width, view.render_height};
|
||||
}
|
||||
requested_eye_size_ = eye_size_;
|
||||
const auto abandon_session = [&] {
|
||||
DestroySwapchains();
|
||||
runtime.DestroySession();
|
||||
@@ -248,6 +253,21 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetRenderScale(float scale) {
|
||||
if (runtime_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> requested{};
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
requested[eye] = OpenXRScaledEyeSize(runtime_->ViewConfiguration()[eye].properties, scale);
|
||||
}
|
||||
// Only a change: a refused size stays refused while it is still the one asked for.
|
||||
if (requested != requested_eye_size_) {
|
||||
requested_eye_size_ = requested;
|
||||
eye_size_ = requested;
|
||||
}
|
||||
}
|
||||
|
||||
OpenXRWindowsVulkanBeginStatus BeginFrame(const OpenXRWindowsVulkanPresentation& presentation,
|
||||
OpenXRWindowsVulkanFrame& frame) {
|
||||
frame = {};
|
||||
@@ -260,6 +280,9 @@ public:
|
||||
Fail("BeginFrame called while another OpenXR frame is active");
|
||||
return OpenXRWindowsVulkanBeginStatus::Error;
|
||||
}
|
||||
if (!ResizeWritablePair()) {
|
||||
return OpenXRWindowsVulkanBeginStatus::Error;
|
||||
}
|
||||
|
||||
const OpenXRFrameStatus status = runtime_->WaitFrame(frame.xr_frame);
|
||||
if (status != OpenXRFrameStatus::Ready) {
|
||||
@@ -394,6 +417,7 @@ public:
|
||||
const auto status = KeepAliveCycle();
|
||||
if (status != OpenXRBeginStatus::Ready) return status;
|
||||
}
|
||||
if (!ResizeWritablePair()) return OpenXRBeginStatus::Error;
|
||||
packet.xr_frame.serial = next_packet_serial_++;
|
||||
packet.xr_frame.predicted_display_time = last_display_time_ + 2 * last_display_period_;
|
||||
packet.xr_frame.predicted_display_period = last_display_period_;
|
||||
@@ -841,8 +865,7 @@ private:
|
||||
|
||||
bool CreateSwapchainPair(std::array<EyeSwapchain, kOpenXREyeCount>& pair) {
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const auto& view = runtime_->ViewConfiguration()[eye];
|
||||
if (!CreateSwapchain(pair[eye], view.render_width, view.render_height,
|
||||
if (!CreateSwapchain(pair[eye], eye_size_[eye].width, eye_size_[eye].height,
|
||||
eye == 0 ? "left eye" : "right eye")) {
|
||||
return false;
|
||||
}
|
||||
@@ -850,6 +873,51 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
// As the D3D12 backend's. Aurora's bridge wraps each swapchain VkImage for Dawn, and the
|
||||
// runtime may hand the old handles out again, so the wraps go with the swapchains.
|
||||
bool ResizeWritablePair() {
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> current{};
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
if (eye_swapchains_[eye].acquired) {
|
||||
return true;
|
||||
}
|
||||
current[eye] = {eye_swapchains_[eye].width, eye_swapchains_[eye].height};
|
||||
}
|
||||
if (current == eye_size_) {
|
||||
return true;
|
||||
}
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> replacement{};
|
||||
if (!CreateSwapchainPair(replacement)) {
|
||||
DestroySwapchainPair(replacement);
|
||||
std::ostringstream message;
|
||||
message << "OpenXR Vulkan eyes stay " << current[0].width << 'x' << current[0].height
|
||||
<< ": the runtime could not make " << eye_size_[0].width << 'x' << eye_size_[0].height
|
||||
<< " swapchains (" << last_error_ << ')';
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
ClearError();
|
||||
// Back to this pair's size, which also returns the other pair to it if it was rebuilt.
|
||||
eye_size_ = current;
|
||||
return true;
|
||||
}
|
||||
std::vector<void*> retired;
|
||||
for (const EyeSwapchain& swapchain : eye_swapchains_) {
|
||||
for (const auto& image : swapchain.images) {
|
||||
retired.push_back(reinterpret_cast<void*>(image.image));
|
||||
}
|
||||
}
|
||||
if (!aurora_vulkan_win32_forget_targets(retired.data(), static_cast<uint32_t>(retired.size()))) {
|
||||
DestroySwapchainPair(replacement);
|
||||
return Fail("Aurora could not retire its copies into the old Vulkan eye swapchains");
|
||||
}
|
||||
DestroySwapchainPair(eye_swapchains_);
|
||||
eye_swapchains_ = std::move(replacement);
|
||||
std::ostringstream message;
|
||||
message << "OpenXR Vulkan eyes resized: " << eye_size_[0].width << 'x' << eye_size_[0].height << " / "
|
||||
<< eye_size_[1].width << 'x' << eye_size_[1].height;
|
||||
Log(OpenXRLogLevel::Info, message.str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CreateSwapchain(EyeSwapchain& swapchain, uint32_t width, uint32_t height, const char* what) {
|
||||
swapchain.width = width;
|
||||
swapchain.height = height;
|
||||
@@ -1102,6 +1170,9 @@ private:
|
||||
OpenXRWindowsVulkanGraphicsRequirements requirements_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> eye_swapchains_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> retained_swapchains_{};
|
||||
// As the D3D12 backend's.
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> eye_size_{};
|
||||
std::array<OpenXREyeSize, kOpenXREyeCount> requested_eye_size_{};
|
||||
// The settings panel's layer: written like the eyes into panel_swapchain_,
|
||||
// shown from retained_panel_swapchain_ (see FinishFrame).
|
||||
EyeSwapchain panel_swapchain_{};
|
||||
@@ -1158,6 +1229,8 @@ bool OpenXRWindowsVulkanBackend::BindAurora(OpenXRRuntime& runtime) {
|
||||
return m_impl->BindAurora(runtime);
|
||||
}
|
||||
|
||||
void OpenXRWindowsVulkanBackend::SetRenderScale(float scale) { m_impl->SetRenderScale(scale); }
|
||||
|
||||
OpenXRWindowsVulkanBeginStatus OpenXRWindowsVulkanBackend::BeginFrame(
|
||||
const OpenXRWindowsVulkanPresentation& presentation, OpenXRWindowsVulkanFrame& frame) {
|
||||
return m_impl->BeginFrame(presentation, frame);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#define aurora_d3d12_set_stereo_targets aurora_vulkan_win32_set_targets
|
||||
#define aurora_d3d12_set_stereo_targets_with_panel aurora_vulkan_win32_set_targets_with_panel
|
||||
#define aurora_d3d12_cancel_stereo_targets aurora_vulkan_win32_cancel
|
||||
#define aurora_d3d12_forget_stereo_targets aurora_vulkan_win32_forget_targets
|
||||
#define aurora_d3d12_disable_stereo_bridge aurora_vulkan_win32_disable
|
||||
#else
|
||||
#define CINTERFACE
|
||||
@@ -32,6 +33,7 @@
|
||||
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -51,6 +53,8 @@ void RequireAt(bool condition, int line, const char* expression) {
|
||||
struct Image { uint64_t content = 0; };
|
||||
struct Swapchain {
|
||||
Image image;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
bool acquired = false;
|
||||
bool waited = false;
|
||||
bool released = false;
|
||||
@@ -74,6 +78,11 @@ uint64_t displayed_content = 0;
|
||||
uint32_t layer_count = 0;
|
||||
uint32_t releases = 0;
|
||||
uint32_t live_swapchains = 0;
|
||||
uint32_t create_attempts = 0;
|
||||
int creates_before_failure = -1; // xrCreateSwapchain fails once this reaches 0; -1 never
|
||||
bool bridge_enabled = false;
|
||||
std::vector<void*> forgotten; // images the backend had Aurora forget
|
||||
int32_t shown_width = 0; // the scene layer's image width
|
||||
XrTime display_time = 0;
|
||||
XrStructureType layer_type = XR_TYPE_UNKNOWN;
|
||||
XrPosef quad_pose{};
|
||||
@@ -160,6 +169,7 @@ bool aurora_d3d12_get_native_handles(AuroraD3D12NativeHandles* handles) {
|
||||
bool aurora_d3d12_enable_stereo_bridge(AuroraD3D12StereoSubmittedCallback cb, void* data) {
|
||||
callback = cb;
|
||||
callback_data = data;
|
||||
bridge_enabled = true;
|
||||
return true;
|
||||
}
|
||||
bool aurora_d3d12_set_stereo_targets_with_panel(uint64_t token, const AuroraD3D12StereoTarget* data,
|
||||
@@ -180,14 +190,26 @@ bool aurora_d3d12_cancel_stereo_targets(uint64_t token) {
|
||||
pending_token = 0;
|
||||
return true;
|
||||
}
|
||||
bool aurora_d3d12_forget_stereo_targets(void* const* resources, uint32_t count) {
|
||||
Require(pending_token == 0); // Never while Aurora may be writing a target.
|
||||
forgotten.insert(forgotten.end(), resources, resources + count);
|
||||
return true;
|
||||
}
|
||||
bool aurora_d3d12_disable_stereo_bridge() {
|
||||
pending_token = 0;
|
||||
encoded = false;
|
||||
bridge_enabled = false;
|
||||
return true; // Simulate a successful queue drain.
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL xrCreateSwapchain(XrSession, const XrSwapchainCreateInfo*, XrSwapchain* out) {
|
||||
*out = reinterpret_cast<XrSwapchain>(new Swapchain);
|
||||
XrResult XRAPI_CALL xrCreateSwapchain(XrSession, const XrSwapchainCreateInfo* info, XrSwapchain* out) {
|
||||
++create_attempts;
|
||||
if (creates_before_failure == 0) return XR_ERROR_OUT_OF_MEMORY;
|
||||
if (creates_before_failure > 0) --creates_before_failure;
|
||||
auto* chain = new Swapchain;
|
||||
chain->width = info->width;
|
||||
chain->height = info->height;
|
||||
*out = reinterpret_cast<XrSwapchain>(chain);
|
||||
++live_swapchains;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -236,6 +258,10 @@ XrResult XRAPI_CALL xrReleaseSwapchainImage(XrSwapchain handle,
|
||||
XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain handle) {
|
||||
auto* chain = reinterpret_cast<Swapchain*>(handle);
|
||||
Require(!chain->acquired);
|
||||
// An image Aurora has written goes only once Aurora forgot it, or with the whole bridge.
|
||||
if (bridge_enabled && chain->released) {
|
||||
Require(std::find(forgotten.begin(), forgotten.end(), static_cast<void*>(&chain->image)) != forgotten.end());
|
||||
}
|
||||
delete chain;
|
||||
--live_swapchains;
|
||||
return XR_SUCCESS;
|
||||
@@ -250,6 +276,10 @@ OpenXRRuntime::OpenXRRuntime(OpenXRLogCallback) {
|
||||
m_swapchain_formats = {DXGI_FORMAT_R8G8B8A8_UNORM_SRGB};
|
||||
#endif
|
||||
for (auto& view : m_view_configuration) {
|
||||
view.properties.recommendedImageRectWidth = 100;
|
||||
view.properties.recommendedImageRectHeight = 80;
|
||||
view.properties.maxImageRectWidth = 400;
|
||||
view.properties.maxImageRectHeight = 320;
|
||||
view.render_width = 100;
|
||||
view.render_height = 80;
|
||||
}
|
||||
@@ -325,11 +355,16 @@ bool OpenXRRuntime::EndFrame(const OpenXRFrame& frame,
|
||||
const auto check_image = [](const XrSwapchainSubImage& subimage) {
|
||||
const auto& chain = *reinterpret_cast<Swapchain*>(subimage.swapchain);
|
||||
Require(chain.released && !chain.acquired && chain.image.content != 0);
|
||||
// The layer shows its own image's size, whichever size the other pair has.
|
||||
Require(subimage.imageRect.offset.x == 0 && subimage.imageRect.offset.y == 0);
|
||||
Require(subimage.imageRect.extent.width == static_cast<int32_t>(chain.width));
|
||||
Require(subimage.imageRect.extent.height == static_cast<int32_t>(chain.height));
|
||||
return chain.image.content;
|
||||
};
|
||||
if (layer_type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
|
||||
auto& projection = *reinterpret_cast<const XrCompositionLayerProjection*>(layers[0]);
|
||||
Require(projection.viewCount == 2);
|
||||
shown_width = projection.views[0].subImage.imageRect.extent.width;
|
||||
for (const auto& view : {projection.views[0], projection.views[1]}) {
|
||||
displayed_content = check_image(view.subImage);
|
||||
// Detect a new image paired with an old pose, or a repeated image
|
||||
@@ -341,6 +376,7 @@ bool OpenXRRuntime::EndFrame(const OpenXRFrame& frame,
|
||||
Require(layer_type == XR_TYPE_COMPOSITION_LAYER_QUAD);
|
||||
const auto& quad = *reinterpret_cast<const XrCompositionLayerQuad*>(layers[0]);
|
||||
displayed_content = check_image(quad.subImage);
|
||||
shown_width = quad.subImage.imageRect.extent.width;
|
||||
quad_pose = quad.pose;
|
||||
}
|
||||
if (count == 2) {
|
||||
@@ -584,6 +620,111 @@ void TestPanelLayer() {
|
||||
display_time = 0;
|
||||
}
|
||||
|
||||
// A new render scale rebuilds each swapchain pair the next time it is the one Aurora writes: the
|
||||
// pair on display is never touched, Aurora forgets the old images before they go (checked by
|
||||
// xrDestroySwapchain), and a size the runtime refuses keeps the eyes as they were.
|
||||
void TestRenderScale() {
|
||||
display_time = 0;
|
||||
forgotten.clear();
|
||||
OpenXRRuntime runtime;
|
||||
OpenXRD3D12Backend backend;
|
||||
Require(backend.QueryGraphicsRequirements(runtime) && backend.BindAurora(runtime));
|
||||
OpenXRPresentation presentation;
|
||||
OpenXRBackendFrame frame;
|
||||
const auto begin = [&](uint32_t width, uint32_t height) {
|
||||
Require(backend.BeginFrame(presentation, frame) == OpenXRBeginStatus::Ready);
|
||||
Require(frame.render_width[0] == width && frame.render_height[1] == height && targets.size() == 2);
|
||||
for (const auto& target : targets) Require(target.width == width && target.height == height);
|
||||
Require(live_swapchains == 4);
|
||||
};
|
||||
const auto finish = [&] {
|
||||
Complete();
|
||||
Require(backend.WaitForSubmission(frame, 0) == OpenXRSubmissionStatus::Success);
|
||||
Require(backend.FinishFrame(frame, true));
|
||||
Require(layer_count == 1 && displayed_content == frame.xr_frame.serial);
|
||||
};
|
||||
|
||||
backend.SetRenderScale(1.0f); // The session's own scale rebuilds nothing.
|
||||
begin(100, 80);
|
||||
finish();
|
||||
Require(forgotten.empty() && shown_width == 100);
|
||||
|
||||
backend.SetRenderScale(1.5f);
|
||||
begin(150, 120); // The pair written next, while the one on display still shows the last frame.
|
||||
Require(forgotten.size() == 2);
|
||||
Require(backend.RepeatFrame(frame) && layer_count == 1 && shown_width == 100);
|
||||
finish();
|
||||
Require(shown_width == 150);
|
||||
begin(150, 120); // Then the other pair, once it is the one written.
|
||||
Require(forgotten.size() == 4);
|
||||
finish();
|
||||
begin(150, 120); // Both are the new size: nothing more is rebuilt.
|
||||
finish();
|
||||
Require(forgotten.size() == 4);
|
||||
|
||||
backend.SetRenderScale(5.0f); // Clamped to the runtime's maximum.
|
||||
begin(400, 320);
|
||||
finish();
|
||||
begin(400, 320);
|
||||
finish();
|
||||
backend.SetRenderScale(1.5f);
|
||||
begin(150, 120);
|
||||
finish();
|
||||
begin(150, 120);
|
||||
finish();
|
||||
const size_t forgotten_before_refusal = forgotten.size();
|
||||
|
||||
// A size the runtime cannot make keeps the eyes, and is not retried while it is still asked for.
|
||||
creates_before_failure = 1; // One eye's swapchain is made, then the other's is refused.
|
||||
const uint32_t attempts = create_attempts;
|
||||
backend.SetRenderScale(3.0f);
|
||||
begin(150, 120);
|
||||
finish();
|
||||
backend.SetRenderScale(3.0f);
|
||||
begin(150, 120);
|
||||
finish();
|
||||
Require(create_attempts == attempts + 2 && forgotten.size() == forgotten_before_refusal);
|
||||
creates_before_failure = -1;
|
||||
backend.SetRenderScale(0.5f); // A new size is tried again.
|
||||
begin(50, 40);
|
||||
finish();
|
||||
begin(50, 40);
|
||||
finish();
|
||||
|
||||
// Refused for the second pair only: the first follows it back to the size it kept.
|
||||
creates_before_failure = 2;
|
||||
backend.SetRenderScale(2.0f);
|
||||
begin(200, 160);
|
||||
finish();
|
||||
Require(shown_width == 200);
|
||||
begin(50, 40);
|
||||
finish();
|
||||
Require(shown_width == 50);
|
||||
creates_before_failure = -1;
|
||||
begin(50, 40);
|
||||
finish();
|
||||
const size_t forgotten_settled = forgotten.size();
|
||||
begin(50, 40);
|
||||
finish();
|
||||
Require(forgotten.size() == forgotten_settled);
|
||||
|
||||
// Render-first pacing rebuilds the pair its packet will be rendered into.
|
||||
expect_render_first = true;
|
||||
backend.SetRenderScale(1.0f);
|
||||
OpenXRBackendFrame packet;
|
||||
Require(backend.PreparePacket(presentation, packet) == OpenXRBeginStatus::Ready);
|
||||
Require(packet.render_width[0] == 100 && targets.size() == 2 && targets[0].width == 100);
|
||||
Complete();
|
||||
Require(backend.WaitForSubmission(packet, 0) == OpenXRSubmissionStatus::Success);
|
||||
Require(backend.BeginFrameForPacket(packet, frame) == OpenXRBeginStatus::Ready);
|
||||
Require(backend.CopyRenderedEyes(frame) == OpenXRSubmissionStatus::Success);
|
||||
Require(backend.FinishFrame(frame, true));
|
||||
Require(layer_count == 1 && shown_width == 100);
|
||||
expect_render_first = false;
|
||||
Require(backend.Shutdown() && live_swapchains == 0);
|
||||
display_time = 0;
|
||||
}
|
||||
|
||||
bool SameRect(const XrRect2Di& rect, int32_t x, int32_t y, int32_t width, int32_t height) {
|
||||
return rect.offset.x == x && rect.offset.y == y && rect.extent.width == width && rect.extent.height == height;
|
||||
}
|
||||
@@ -610,6 +751,7 @@ void TestVirtualScreenContentRect() {
|
||||
int main() {
|
||||
TestRenderFirst();
|
||||
TestPanelLayer();
|
||||
TestRenderScale();
|
||||
TestVirtualScreenContentRect();
|
||||
OpenXRRuntime runtime;
|
||||
OpenXRD3D12Backend backend;
|
||||
|
||||
Reference in new issue
Block a user