Added Desktop view selector

This commit is contained in:
iChris4 committed 2026-09-09 00:18:41 +02:00
1 parent a138670b37
commit 4cf9f60ee7
7 files changed
+260 -16

No files matched your search

+9
View File
@@ -22,6 +22,7 @@ configuration and is created with the following defaults:
[vr] [vr]
enabled = false enabled = false
required = false required = false
mirror_view = "normal"
render_scale = 1.0 render_scale = 1.0
world_units_per_meter = 500.0 world_units_per_meter = 500.0
hud_distance_meters = 2.0 hud_distance_meters = 2.0
@@ -47,6 +48,14 @@ required.
or graphics-binding failure is logged and the game continues in ordinary desktop mode. Set it to or graphics-binding failure is logged and the game continues in ordinary desktop mode. Set it to
`true` only when a failed VR startup should stop the game with an error. `true` only when a failed VR startup should stop the game with an error.
`mirror_view` chooses what the desktop window shows while the headset is running: `"normal"`
keeps the ordinary desktop view, `"both"`, `"left"` and `"right"` mirror the headset's eyes, and
`"none"` blacks the window out. It is live and can be changed from the F10 settings bar, where it
sits directly under the enable switch as *Desktop view*. Menus reach the headset as a virtual
screen carrying the desktop image itself, so there is no separate eye view to mirror there and the
three eye choices show that same image; only `"none"` differs. The F10 bar is drawn over whichever
image is chosen, so the setting can always be changed back.
`render_scale` scales the per-eye size recommended by the OpenXR runtime. `render_scale` scales the per-eye size recommended by the OpenXR runtime.
`world_units_per_meter` controls the scale of headset translation in the game world. `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 `hud_distance_meters` and `hud_width_meters` place and size the virtual screen. They are read at
+22
View File
@@ -106,6 +106,28 @@ bool aurora_get_stereo_skip_copy_clears();
void aurora_set_stereo_hud_screen(bool enabled, float width, float distance); void aurora_set_stereo_hud_screen(bool enabled, float width, float distance);
bool aurora_get_stereo_hud_screen_enabled(); bool aurora_get_stereo_hud_screen_enabled();
// What the desktop window shows while a headset is being fed. NORMAL leaves the
// ordinary mono presentation untouched, the eye views mirror what the headset is
// actually displaying, and NONE presents a black window. Live, and only
// consulted while a stereo frame provider is supplying frames.
//
// A menu frame reaches the headset as a virtual screen carrying the very mono
// image the desktop already shows, so there is no distinct eye view to mirror:
// every eye choice presents that same image there, and only NONE differs.
//
// Interpolated presentation slots are encoded before the frame's eyes are
// rendered, so under an eye choice they mirror the previous frame's eyes while
// the real slot mirrors the current one.
typedef enum {
AURORA_STEREO_MIRROR_NORMAL = 0,
AURORA_STEREO_MIRROR_BOTH_EYES = 1,
AURORA_STEREO_MIRROR_LEFT_EYE = 2,
AURORA_STEREO_MIRROR_RIGHT_EYE = 3,
AURORA_STEREO_MIRROR_NONE = 4,
} AuroraStereoMirrorView;
void aurora_set_stereo_mirror_view(AuroraStereoMirrorView view);
AuroraStereoMirrorView aurora_get_stereo_mirror_view();
// Guest-RAM write tracking. `generation` changes whenever guest RAM covering a host range was // Guest-RAM write tracking. `generation` changes whenever guest RAM covering a host range was
// written (or returns AURORA_GUEST_WRITE_UNTRACKED); `notify` reports writes aurora made itself. // written (or returns AURORA_GUEST_WRITE_UNTRACKED); `notify` reports writes aurora made itself.
#define AURORA_GUEST_WRITE_UNTRACKED UINT64_MAX #define AURORA_GUEST_WRITE_UNTRACKED UINT64_MAX
+134 -14
View File
@@ -537,11 +537,24 @@ struct StereoEyeTarget {
uint32_t samples = 0; uint32_t samples = 0;
wgpu::TextureFormat colorFormat = wgpu::TextureFormat::Undefined; wgpu::TextureFormat colorFormat = wgpu::TextureFormat::Undefined;
wgpu::TextureFormat depthFormat = wgpu::TextureFormat::Undefined; wgpu::TextureFormat depthFormat = wgpu::TextureFormat::Undefined;
// Built on demand for the desktop mirror only, and dropped with the rest of
// the target when ensure_stereo_eye_target replaces the textures.
wgpu::BindGroup copyBindGroup;
const webgpu::TextureWithSampler& output() const noexcept { return resolvedColor.texture ? resolvedColor : color; } const webgpu::TextureWithSampler& output() const noexcept { return resolvedColor.texture ? resolvedColor : color; }
}; };
std::array<StereoEyeTarget, AURORA_STEREO_EYE_COUNT> g_stereoEyeTargets; std::array<StereoEyeTarget, AURORA_STEREO_EYE_COUNT> g_stereoEyeTargets;
// The eye targets outlive a frame, so the mirror samples them through a bind
// group cached beside them rather than one built per presentation slot.
wgpu::BindGroup stereo_eye_copy_bind_group(uint32_t eyeIndex) {
auto& target = g_stereoEyeTargets[eyeIndex];
if (!target.copyBindGroup && target.output().texture) {
target.copyBindGroup = webgpu::create_copy_bind_group(target.output());
}
return target.copyBindGroup;
}
void ensure_stereo_eye_target(uint32_t eyeIndex, uint32_t width, uint32_t height) { void ensure_stereo_eye_target(uint32_t eyeIndex, uint32_t width, uint32_t height) {
auto& target = g_stereoEyeTargets[eyeIndex]; auto& target = g_stereoEyeTargets[eyeIndex];
const uint32_t samples = webgpu::g_graphicsConfig.msaaSamples; const uint32_t samples = webgpu::g_graphicsConfig.msaaSamples;
@@ -1405,10 +1418,68 @@ void stop_presenter() noexcept {
g_presenterStarted.store(false, std::memory_order_release); g_presenterStarted.store(false, std::memory_order_release);
} }
// What a presentation slot draws under the image. Mono is the ordinary desktop
// view; the rest mirror the headset and are only ever chosen while a stereo
// provider is feeding one. ImGui is drawn over all of them alike, so the
// settings menu stays reachable even under Black.
enum class MirrorPlan {
Mono,
LeftEye,
RightEye,
BothEyes,
Black,
};
// A virtual-screen (menu) frame puts the desktop's own mono image on both eyes,
// so there is no separate eye view to mirror and the eye choices collapse onto
// Mono. Only Black still has something distinct to do there.
MirrorPlan resolve_mirror_plan(AuroraStereoMirrorView view, bool stereoOutput, bool immersiveReplay) noexcept {
if (!stereoOutput) {
return MirrorPlan::Mono;
}
switch (view) {
case AURORA_STEREO_MIRROR_NONE:
return MirrorPlan::Black;
case AURORA_STEREO_MIRROR_BOTH_EYES:
return immersiveReplay ? MirrorPlan::BothEyes : MirrorPlan::Mono;
case AURORA_STEREO_MIRROR_LEFT_EYE:
return immersiveReplay ? MirrorPlan::LeftEye : MirrorPlan::Mono;
case AURORA_STEREO_MIRROR_RIGHT_EYE:
return immersiveReplay ? MirrorPlan::RightEye : MirrorPlan::Mono;
case AURORA_STEREO_MIRROR_NORMAL:
break;
}
return MirrorPlan::Mono;
}
// Places one eye inside `bounds`, keeping the eye's own aspect ratio rather
// than the game's presented one: an eye is already the shape the headset asked
// for, so letterboxing it to the game's aspect would crop the compositor's view.
void draw_mirror_eye(const wgpu::RenderPassEncoder& pass, uint32_t eyeIndex, const webgpu::Viewport& bounds) {
const auto bindGroup = stereo_eye_copy_bind_group(eyeIndex);
const auto& output = g_stereoEyeTargets[eyeIndex].output();
if (!bindGroup || output.size.width == 0 || output.size.height == 0 || bounds.width <= 0.f ||
bounds.height <= 0.f) {
return;
}
const auto fitted = webgpu::calculate_present_viewport(static_cast<uint32_t>(bounds.width),
static_cast<uint32_t>(bounds.height), output.size.width,
output.size.height);
// A window too small to hold a half still rounds down to nothing here.
if (fitted.width <= 0.f || fitted.height <= 0.f) {
return;
}
pass.SetBindGroup(0, bindGroup, 0, nullptr);
pass.SetViewport(bounds.left + fitted.left, bounds.top + fitted.top, fitted.width, fitted.height, fitted.znear,
fitted.zfar);
pass.Draw(3);
}
// `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next // `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next
// gfx::begin_frame() may already have cleared the display-copy override. // gfx::begin_frame() may already have cleared the display-copy override.
void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, const webgpu::PresentSource& presentSource, void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, const webgpu::PresentSource& presentSource,
const PresentationImage& image, bool includeImGui) { const PresentationImage& image, bool includeImGui,
MirrorPlan plan = MirrorPlan::Mono) {
ZoneScoped; ZoneScoped;
auto viewport = webgpu::calculate_present_viewport(image.texture.size.width, image.texture.size.height, auto viewport = webgpu::calculate_present_viewport(image.texture.size.width, image.texture.size.height,
presentSource.size.width, presentSource.size.height); presentSource.size.width, presentSource.size.height);
@@ -1432,10 +1503,41 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, const web
.colorAttachments = attachments.data(), .colorAttachments = attachments.data(),
}; };
const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); const auto pass = encoder.BeginRenderPass(&renderPassDescriptor);
pass.SetPipeline(webgpu::g_CopyPipeline); const auto imageWidth = static_cast<float>(image.texture.size.width);
pass.SetBindGroup(0, presentBindGroup, 0, nullptr); const auto imageHeight = static_cast<float>(image.texture.size.height);
pass.SetViewport(viewport.left, viewport.top, viewport.width, viewport.height, viewport.znear, viewport.zfar); // Black needs nothing but the clear the attachment already performed.
pass.Draw(3); if (plan != MirrorPlan::Black) {
pass.SetPipeline(webgpu::g_CopyPipeline);
}
switch (plan) {
case MirrorPlan::Mono:
pass.SetBindGroup(0, presentBindGroup, 0, nullptr);
pass.SetViewport(viewport.left, viewport.top, viewport.width, viewport.height, viewport.znear, viewport.zfar);
pass.Draw(3);
break;
case MirrorPlan::LeftEye:
case MirrorPlan::RightEye:
draw_mirror_eye(pass, plan == MirrorPlan::LeftEye ? 0u : 1u,
{.left = 0.f, .top = 0.f, .width = imageWidth, .height = imageHeight, .znear = 0.f, .zfar = 1.f});
break;
case MirrorPlan::BothEyes: {
// Side by side in the window's two halves, in the order the compositor
// receives them, so the pair reads the way the headset is wearing it.
const float halfWidth = imageWidth * 0.5f;
for (uint32_t eye = 0; eye < AURORA_STEREO_EYE_COUNT; ++eye) {
draw_mirror_eye(pass, eye,
{.left = static_cast<float>(eye) * halfWidth,
.top = 0.f,
.width = halfWidth,
.height = imageHeight,
.znear = 0.f,
.zfar = 1.f});
}
break;
}
case MirrorPlan::Black:
break;
}
pass.End(); pass.End();
} }
if (includeImGui) { if (includeImGui) {
@@ -1679,6 +1781,11 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
std::vector<PresentationJob> presentationJobs; std::vector<PresentationJob> presentationJobs;
presentationJobs.reserve(presentationJobCount); presentationJobs.reserve(presentationJobCount);
std::optional<PendingStereoSink> pendingStereoSink; std::optional<PendingStereoSink> pendingStereoSink;
const bool stereoOutput = ctx.stereoReplay.has_value();
const bool immersiveReplay = stereoOutput && ctx.immersiveStereoPrepared;
// One choice for the whole group: a slot showing the mono view next to slots
// mirroring an eye would strobe between two different images.
const MirrorPlan mirrorPlan = resolve_mirror_plan(gfx::get_stereo_mirror_view(), stereoOutput, immersiveReplay);
// Each slot is submitted as soon as it is encoded, so the GPU starts slot 0 while slot 1 is still // Each slot is submitted as soon as it is encoded, so the GPU starts slot 0 while slot 1 is still
// recording. Queue order preserves the ordering the single batched buffer gave. // recording. Queue order preserves the ordering the single batched buffer gave.
@@ -1704,7 +1811,7 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
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);
auto image = acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); auto image = acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true); encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, mirrorPlan);
presentationJobs.push_back({ presentationJobs.push_back({
.image = std::move(image), .image = std::move(image),
.logicalFrame = ctx.logicalFrame, .logicalFrame = ctx.logicalFrame,
@@ -1718,8 +1825,6 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// A demanded CPU-visible EFB readback submits a prefix of the frame, so replaying the resumed // A demanded CPU-visible EFB readback submits a prefix of the frame, so replaying the resumed
// stream would mutate an already-rendered EFB. Render once, then duplicate into the slots. // stream would mutate an already-rendered EFB. Render once, then duplicate into the slots.
const bool stereoOutput = ctx.stereoReplay.has_value();
const bool immersiveReplay = stereoOutput && ctx.immersiveStereoPrepared;
gfx::render(sealedFrame, encoder, -1, !immersiveReplay); gfx::render(sealedFrame, encoder, -1, !immersiveReplay);
// The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder; // The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder;
// completion is harvested in gfx::after_submit, never waited on here. // completion is harvested in gfx::after_submit, never waited on here.
@@ -1727,7 +1832,7 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
if (!ctx.replayInterpolatedFrames) { if (!ctx.replayInterpolatedFrames) {
for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; ++interpolatedFrame) { for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; ++interpolatedFrame) {
auto image = acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); auto image = acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true); encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, mirrorPlan);
presentationJobs.push_back({ presentationJobs.push_back({
.image = std::move(image), .image = std::move(image),
.logicalFrame = ctx.logicalFrame, .logicalFrame = ctx.logicalFrame,
@@ -1739,18 +1844,28 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
encoder = g_device.CreateCommandEncoder(&encoderDescriptor); encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
} }
} }
auto finalImage = acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true);
// Keep both eye replays and the sink copy in the final submission. The // Keep both eye replays and the sink copy in the final submission. The
// duplicate-slot path above may submit and rotate the encoder several // duplicate-slot path above may submit and rotate the encoder several
// times, so encoding stereo before it would pair the post-submit callback // times, so encoding stereo before it would pair the post-submit callback
// with the wrong command buffer. // with the wrong command buffer. Within this last encoder the eyes come
// first, so a mirroring final slot samples this frame's eyes rather than the
// previous frame's; the interpolated slots above necessarily mirror the
// previous frame, having been encoded before this replay.
if (immersiveReplay) { if (immersiveReplay) {
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, eye + 1 == AURORA_STEREO_EYE_COUNT); gfx::render_stereo_eye(sealedFrame, encoder, *ctx.stereoReplay, eye, eye + 1 == AURORA_STEREO_EYE_COUNT);
} }
} else if (stereoOutput) { }
auto finalImage = acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight);
// A virtual-screen frame builds its eyes out of the completed mono snapshot,
// so that snapshot must hold the mono image whatever the desktop ends up
// showing. Black re-clears it below, once the eyes have taken their copy.
const bool virtualScreenNeedsMono = stereoOutput && !immersiveReplay;
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true,
virtualScreenNeedsMono ? MirrorPlan::Mono : mirrorPlan);
if (virtualScreenNeedsMono) {
// Use the completed mono snapshot so virtual-screen XR includes ImGui at // Use the completed mono snapshot so virtual-screen XR includes ImGui at
// the same scale and aspect as the desktop presentation. Rendering the // the same scale and aspect as the desktop presentation. Rendering the
// same ImGui draw data directly into differently-sized eye textures would // same ImGui draw data directly into differently-sized eye textures would
@@ -1764,6 +1879,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) {
encode_virtual_screen_eye(encoder, completedMono, eye); encode_virtual_screen_eye(encoder, completedMono, eye);
} }
if (mirrorPlan == MirrorPlan::Black) {
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, MirrorPlan::Black);
}
} }
if (stereoOutput) { if (stereoOutput) {
pendingStereoSink = run_stereo_sink(encoder, ctx.stereoFrameToken, ctx.logicalFrame, ctx.stereoFrameMode); pendingStereoSink = run_stereo_sink(encoder, ctx.stereoFrameToken, ctx.logicalFrame, ctx.stereoFrameMode);
@@ -2325,6 +2443,8 @@ void aurora_set_stereo_hud_screen(bool enabled, float width, float distance) {
aurora::gfx::set_stereo_hud_screen(enabled, width, distance); aurora::gfx::set_stereo_hud_screen(enabled, width, distance);
} }
bool aurora_get_stereo_hud_screen_enabled() { return aurora::gfx::get_stereo_hud_screen_enabled(); } bool aurora_get_stereo_hud_screen_enabled() { return aurora::gfx::get_stereo_hud_screen_enabled(); }
void aurora_set_stereo_mirror_view(AuroraStereoMirrorView view) { aurora::gfx::set_stereo_mirror_view(view); }
AuroraStereoMirrorView aurora_get_stereo_mirror_view() { return aurora::gfx::get_stereo_mirror_view(); }
void aurora_set_background_input(bool value) { void aurora_set_background_input(bool value) {
aurora::g_config.allowJoystickBackgroundEvents = value; aurora::g_config.allowJoystickBackgroundEvents = value;
aurora::window::set_background_input(value); aurora::window::set_background_input(value);
+22
View File
@@ -228,6 +228,28 @@ void set_stereo_hud_screen(bool enabled, float width, float distance) noexcept {
} }
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); }
// The desktop mirror choice. Normal is the ordinary mono presentation, so a
// build that never touches this setting presents exactly as it did before.
static std::atomic<AuroraStereoMirrorView> g_stereoMirrorView{AURORA_STEREO_MIRROR_NORMAL};
void set_stereo_mirror_view(AuroraStereoMirrorView value) noexcept {
switch (value) {
case AURORA_STEREO_MIRROR_NORMAL:
case AURORA_STEREO_MIRROR_BOTH_EYES:
case AURORA_STEREO_MIRROR_LEFT_EYE:
case AURORA_STEREO_MIRROR_RIGHT_EYE:
case AURORA_STEREO_MIRROR_NONE:
break;
default:
// An out-of-range value would otherwise black the window out with no way
// back from inside the game.
Log.warn("Ignoring unknown stereo mirror view {}", static_cast<int>(value));
return;
}
g_stereoMirrorView.store(value, std::memory_order_relaxed);
}
AuroraStereoMirrorView get_stereo_mirror_view() noexcept { return g_stereoMirrorView.load(std::memory_order_relaxed); }
// Recycle command storage: discarding passes used to free their command lists too, so each frame // Recycle command storage: discarding passes used to free their command lists too, so each frame
// rebuilt hundreds of KB from zero capacity. The passes themselves are cheap to recreate. // rebuilt hundreds of KB from zero capacity. The passes themselves are cheap to recreate.
using CommandListPool = std::vector<CommandList>; using CommandListPool = std::vector<CommandList>;
+5
View File
@@ -388,6 +388,11 @@ bool get_stereo_skip_copy_clears() noexcept;
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;
// What the desktop window presents while a stereo provider is feeding a
// headset. Live, and read once per presentation group by the frame worker.
void set_stereo_mirror_view(AuroraStereoMirrorView value) noexcept;
AuroraStereoMirrorView get_stereo_mirror_view() noexcept;
void begin_offscreen(uint32_t width, uint32_t height); void begin_offscreen(uint32_t width, uint32_t height);
void end_offscreen(); void end_offscreen();
bool is_offscreen() noexcept; bool is_offscreen() noexcept;
+32
View File
@@ -56,6 +56,7 @@ struct RuntimeUserConfig {
std::optional<bool> vrHudVirtualScreen; std::optional<bool> vrHudVirtualScreen;
std::optional<bool> vrStopAtDisplayCopy; std::optional<bool> vrStopAtDisplayCopy;
std::optional<bool> vrSkipCopyClears; std::optional<bool> vrSkipCopyClears;
std::optional<std::string> vrMirrorView;
std::optional<bool> vrFirstPerson; std::optional<bool> vrFirstPerson;
std::optional<float> vrFirstPersonUnitsPerMeter; std::optional<float> vrFirstPersonUnitsPerMeter;
std::optional<float> vrFirstPersonHeadUpMeters; std::optional<float> vrFirstPersonHeadUpMeters;
@@ -158,6 +159,15 @@ inline constexpr const char* kVrFirstPersonRotationDefault = "yaw";
inline bool IsSupportedVrFirstPersonRotation(std::string_view value) { inline bool IsSupportedVrFirstPersonRotation(std::string_view value) {
return value == "yaw" || value == "yaw_pitch" || value == "full"; return value == "yaw" || value == "yaw_pitch" || value == "full";
} }
// What the desktop window shows while the headset is running: "normal" leaves
// the ordinary desktop view alone, "both", "left" and "right" mirror the
// headset's eyes, and "none" blacks the window out. Matches
// AuroraStereoMirrorView.
inline constexpr const char* kVrMirrorViewDefault = "normal";
inline bool IsSupportedVrMirrorView(std::string_view value) {
return value == "normal" || value == "both" || value == "left" || value == "right" || value == "none";
}
// SDL scancode name, spelled the way SDL_GetScancodeName produces it. An // SDL scancode name, spelled the way SDL_GetScancodeName produces it. An
// empty string leaves the recenter hotkey unbound, menu button only. // empty string leaves the recenter hotkey unbound, menu button only.
inline constexpr std::string_view kVrRecenterKeyDefault = "F9"; inline constexpr std::string_view kVrRecenterKeyDefault = "F9";
@@ -360,6 +370,11 @@ inline void EnsureConfigFile() {
"# to the ordinary desktop renderer. These values are read at launch.\n" "# to the ordinary desktop renderer. These values are read at launch.\n"
"enabled = false\n" "enabled = false\n"
"required = false\n" "required = false\n"
"# What the desktop window shows while the headset is running:\n"
"# \"normal\" keeps the ordinary desktop view, \"both\", \"left\" and\n"
"# \"right\" mirror the headset's eyes, and \"none\" blacks the window\n"
"# out. Changeable live from the F10 menu.\n"
"mirror_view = \"normal\"\n"
"render_scale = 1.0\n" "render_scale = 1.0\n"
"world_units_per_meter = 500.0\n" "world_units_per_meter = 500.0\n"
"hud_distance_meters = 2.0\n" "hud_distance_meters = 2.0\n"
@@ -616,6 +631,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
value && IsSupportedVrFirstPersonRotation(*value)) { value && IsSupportedVrFirstPersonRotation(*value)) {
config.vrFirstPersonRotation = *value; config.vrFirstPersonRotation = *value;
} }
if (auto value = FindConfigValue<std::string>(document, "vr", "mirror_view");
value && IsSupportedVrMirrorView(*value)) {
config.vrMirrorView = *value;
}
if (auto value = FindConfigInt(document, "vr", "first_person_hidden_model"); if (auto value = FindConfigInt(document, "vr", "first_person_hidden_model");
value && *value >= -1 && *value <= 31) { value && *value >= -1 && *value <= 31) {
config.vrFirstPersonHiddenModel = static_cast<int32_t>(*value); config.vrFirstPersonHiddenModel = static_cast<int32_t>(*value);
@@ -909,6 +928,14 @@ inline bool SetVrFirstPersonHideDriver(bool value) {
return WriteSetting("vr", "first_person_hide_driver", value ? "true" : "false"); return WriteSetting("vr", "first_person_hide_driver", value ? "true" : "false");
} }
inline bool SetVrMirrorView(std::string value) {
if (!IsSupportedVrMirrorView(value)) {
return false;
}
Mutable().vrMirrorView = value;
return WriteSetting("vr", "mirror_view", FormatString(value));
}
inline bool SetVrFirstPersonRotation(std::string value) { inline bool SetVrFirstPersonRotation(std::string value) {
if (!IsSupportedVrFirstPersonRotation(value)) { if (!IsSupportedVrFirstPersonRotation(value)) {
return false; return false;
@@ -1236,6 +1263,11 @@ inline bool VrFirstPersonHideDriver(bool fallback = kVrFirstPersonHideDriverDefa
return Get().vrFirstPersonHideDriver.value_or(fallback); return Get().vrFirstPersonHideDriver.value_or(fallback);
} }
inline std::string VrMirrorView(std::string fallback = kVrMirrorViewDefault) {
const auto& value = Get().vrMirrorView;
return value && IsSupportedVrMirrorView(*value) ? *value : std::move(fallback);
}
inline std::string VrFirstPersonRotation(std::string fallback = kVrFirstPersonRotationDefault) { inline std::string VrFirstPersonRotation(std::string fallback = kVrFirstPersonRotationDefault) {
const auto& value = Get().vrFirstPersonRotation; const auto& value = Get().vrFirstPersonRotation;
return value && IsSupportedVrFirstPersonRotation(*value) ? *value : std::move(fallback); return value && IsSupportedVrFirstPersonRotation(*value) ? *value : std::move(fallback);
+36 -2
View File
@@ -118,6 +118,25 @@ float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMe
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters(); float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters();
bool g_vrFirstPersonHideDriver = RuntimeConfigFile::VrFirstPersonHideDriver(); bool g_vrFirstPersonHideDriver = RuntimeConfigFile::VrFirstPersonHideDriver();
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel(); int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
// Config spellings and menu labels for the desktop mirror, index-matched to
// AuroraStereoMirrorView so the combo selection converts to either directly.
constexpr std::array<const char*, 5> kVrMirrorViewNames{"normal", "both", "left", "right", "none"};
constexpr std::array<const char*, 5> kVrMirrorViewLabels{"Normal", "Both eyes", "Left eye", "Right eye", "None"};
static_assert(kVrMirrorViewNames.size() == kVrMirrorViewLabels.size());
static_assert(static_cast<int>(AURORA_STEREO_MIRROR_NORMAL) == 0);
static_assert(static_cast<int>(AURORA_STEREO_MIRROR_BOTH_EYES) == 1);
static_assert(static_cast<int>(AURORA_STEREO_MIRROR_LEFT_EYE) == 2);
static_assert(static_cast<int>(AURORA_STEREO_MIRROR_RIGHT_EYE) == 3);
static_assert(static_cast<int>(AURORA_STEREO_MIRROR_NONE) == 4);
int g_vrMirrorView = [] {
const std::string mode = RuntimeConfigFile::VrMirrorView();
for (size_t i = 0; i < kVrMirrorViewNames.size(); ++i) {
if (mode == kVrMirrorViewNames[i]) {
return static_cast<int>(i);
}
}
return 0;
}();
constexpr std::array<const char*, 3> kVrFirstPersonRotationNames{"yaw", "yaw_pitch", "full"}; constexpr std::array<const char*, 3> kVrFirstPersonRotationNames{"yaw", "yaw_pitch", "full"};
int g_vrFirstPersonRotation = [] { int g_vrFirstPersonRotation = [] {
const std::string mode = RuntimeConfigFile::VrFirstPersonRotation(); const std::string mode = RuntimeConfigFile::VrFirstPersonRotation();
@@ -891,9 +910,23 @@ void DrawVrSettings() {
RuntimeConfigFile::SetVrEnabled(g_vrEnabled); RuntimeConfigFile::SetVrEnabled(g_vrEnabled);
} }
ImGui::TextDisabled("OpenXR mode changes take effect after restarting the game."); ImGui::TextDisabled("OpenXR mode changes take effect after restarting the game.");
// Live, unlike the enable toggle above, so it is left usable either way:
// set before a restart it is simply what the next session starts on.
if (ImGui::Combo("Desktop view", &g_vrMirrorView, kVrMirrorViewLabels.data(),
static_cast<int>(kVrMirrorViewLabels.size()))) {
aurora_set_stereo_mirror_view(static_cast<AuroraStereoMirrorView>(g_vrMirrorView));
RuntimeConfigFile::SetVrMirrorView(kVrMirrorViewNames[static_cast<size_t>(g_vrMirrorView)]);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"What this window shows while the headset is running. Normal keeps the ordinary "
"desktop view, the eye choices mirror what you are actually seeing in the headset, "
"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.");
}
// Unlike the toggle above, these two apply to the very next frame, so they // Like the mirror above and unlike the enable toggle, these two apply to the
// can be compared against each other from inside a running race. // very next frame, so they can be compared from inside a running race.
ImGui::Separator(); ImGui::Separator();
ImGui::Text("VR eye replay (EFB)"); ImGui::Text("VR eye replay (EFB)");
if (ImGui::Checkbox("Stop eye at display copy", &g_vrStopAtDisplayCopy)) { if (ImGui::Checkbox("Stop eye at display copy", &g_vrStopAtDisplayCopy)) {
@@ -1331,6 +1364,7 @@ void InitializeRuntimeSettings() noexcept {
aurora_set_disable_copy_filter(g_disableCopyFilter); aurora_set_disable_copy_filter(g_disableCopyFilter);
aurora_set_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy); aurora_set_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy);
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears); aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
aurora_set_stereo_mirror_view(static_cast<AuroraStereoMirrorView>(g_vrMirrorView));
ApplyVrHudVirtualScreen(); ApplyVrHudVirtualScreen();
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines); aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings(); mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();