mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 05:00:27 +02:00
Added Desktop view selector
This commit is contained in:
1 parent
a138670b37
commit
4cf9f60ee7
7 files changed
+260
-16
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user