vr: add OpenXR headset output, VR tracker and VR controller input behind --vr (#1726)

This commit is contained in:
Leandro Bras authored and nmzik committed 2026-10-20 02:18:51 +02:00
1 parent 0cad4672e9
commit 742fe17173
17 files changed
+1637 -28

No files matched your search

+69 -8
View File
@@ -21,6 +21,7 @@
#include "graphics/presentation/renderDoc.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/hmd2.h"
#include "libs/libs.h"
#include "loader/systemContent.h"
@@ -51,6 +52,7 @@ constexpr int VIDEO_OUT_FALSE = 0;
constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0;
constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1;
constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2;
constexpr int VIDEO_OUT_BUS_TYPE_HMD2_MAIN = 32;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC = 1;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC_MULTI = 4;
constexpr int VIDEO_OUT_FLIP_MODE_SLAVE = 8;
@@ -66,6 +68,7 @@ constexpr uint64_t VIDEO_OUT_REFRESH_RATE_59_94HZ = 3;
constexpr uint64_t VIDEO_OUT_REFRESH_RATE_119_88HZ = 13;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2 = 0x10;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED = 32;
@@ -252,8 +255,9 @@ private:
struct VideoOutDriver::Impl {
public:
static constexpr std::array VIDEO_OUT_BUSES {
VIDEO_OUT_BUS_TYPE_MAIN, VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB};
static constexpr std::array VIDEO_OUT_BUSES {VIDEO_OUT_BUS_TYPE_MAIN,
VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB,
VIDEO_OUT_BUS_TYPE_HMD2_MAIN};
static constexpr int VIDEO_OUT_NUM_MAX = VIDEO_OUT_BUSES.size() + 1;
Impl(uint32_t width, uint32_t height, Graphics::Presenter& presenter)
@@ -516,6 +520,10 @@ static bool IsValidFlipMode(int mode) {
mode == VIDEO_OUT_FLIP_MODE_SLAVE || mode == VIDEO_OUT_FLIP_MODE_MASTER;
}
// Set once the headset port shows eye images. The window then shows them instead of the main port,
// so the picture is visible without a headset.
static std::atomic<bool> g_headset_mirror {false};
static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int index, int flip_mode,
int64_t flip_arg, FlipRequestSource source, uint64_t& request_id) {
auto* video_out = driver.Get(handle);
@@ -534,8 +542,10 @@ static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int inde
(video_out->slave_count != 0) != (flip_mode == VIDEO_OUT_FLIP_MODE_MASTER)) {
return VIDEO_OUT_ERROR_INVALID_FLIP_MODE;
}
// The headset port can flip before its reprojection buffers are registered; those show blank.
if (video_out->closing ||
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied())) {
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied() &&
video_out->bus != VIDEO_OUT_BUS_TYPE_HMD2_MAIN)) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
if (!driver.GetFlipQueue().Reserve(*video_out, index, flip_arg, source, request_id)) {
@@ -740,6 +750,9 @@ bool VideoOutDriver::Impl::Close(int handle) {
master->slave_count--;
}
m_flip_queue.Cancel(config);
if (config.bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN) {
g_headset_mirror = false;
}
m_presenter.ClearLayer(config.bus);
DeleteVideoOutEvents(flip_events, VideoOutEventKind::Flip);
DeleteVideoOutEvents(pre_vblank_events, VideoOutEventKind::PreVblankStart);
@@ -1066,16 +1079,19 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
index = request->index;
}
const bool special = IsSpecialBufferIndex(index);
bool special = IsSpecialBufferIndex(index);
Graphics::ImageInfo source_info;
uint32_t width = 0;
uint32_t height = 0;
bool current = false;
bool premultiplied_alpha = false;
bool hdr = false;
bool headset = false;
{
Common::LockGuard lock(cfg->mutex);
current = cfg->opened && !cfg->closing && cfg->generation == generation;
headset = cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
special = special || (headset && !cfg->buffers[index].Occupied());
if (current && special) {
width = cfg->width;
height = cfg->height;
@@ -1120,7 +1136,10 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
&buffer);
} else {
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr, headset);
if (headset) {
g_headset_mirror = true;
}
}
Common::LockGuard lock(m_mutex);
@@ -1239,6 +1258,8 @@ bool FlipQueue::Flip(uint32_t micros) {
[](const auto& a, const auto& b) { return a.cfg->bus < b.cfg->bus; });
bool due = true;
std::array<Graphics::Presenter::Layer, VideoOutDriver::Impl::VIDEO_OUT_BUSES.size()> layers {};
size_t layer_count = 0;
Graphics::Presenter::Frame* hidden = nullptr; // The main port behind the headset image.
for (size_t i = 0; i < count; i++) {
auto& r = requests[i];
r.cfg->mutex.Lock();
@@ -1246,10 +1267,22 @@ bool FlipQueue::Flip(uint32_t micros) {
if (r.id == group) {
due &= IsFlipDueLocked(*r.cfg, r.generation);
}
layers[i] = {r.frame, r.cfg->bus, r.premultiplied_alpha};
if (r.cfg->bus == VIDEO_OUT_BUS_TYPE_MAIN &&
g_headset_mirror.load(std::memory_order_relaxed)) {
hidden = r.frame;
} else {
const bool headset = r.cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
layers[layer_count++] = {r.frame, headset ? VIDEO_OUT_BUS_TYPE_MAIN : r.cfg->bus,
r.premultiplied_alpha};
}
}
if (due) {
m_presenter.Present(std::span(layers.data(), count));
if (hidden != nullptr) {
m_presenter.Discard(*hidden);
}
if (layer_count != 0) {
m_presenter.Present(std::span(layers.data(), layer_count));
}
if (requests[0].cfg->bus == 0) {
Graphics::PerformanceOverlayRecordFlip(requests[0].cfg->width, requests[0].cfg->height);
}
@@ -1461,6 +1494,33 @@ KYTY_SYSV_ABI int VideoOutRegisterBuffers2(int handle, int set_index, int buffer
if (option != nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
}
// The headset port takes reprojection buffers with a zeroed attribute. Each names its eye
// texture, which is registered and flipped like a regular surface.
const bool headset = ctx->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
if (headset != (category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2)) {
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
}
VideoOutBufferAttribute2 headset_attribute {};
std::array<VideoOutBuffers, VIDEO_OUT_BUFFER_NUM_MAX> headset_buffers {};
if (headset) {
for (int i = 0; i < buffer_num; i++) {
Hmd2::ReprojectionSurface surface {};
if (buffers[i].metadata != nullptr ||
!Hmd2::GetReprojectionSurface(buffers[i].data, surface) ||
(i != 0 && (surface.width != headset_attribute.width ||
surface.height != headset_attribute.height ||
surface.pixel_format != headset_attribute.pixel_format))) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
headset_attribute.pixel_format = surface.pixel_format;
headset_attribute.width = surface.width;
headset_attribute.height = surface.height;
headset_buffers[i].data = reinterpret_cast<const void*>(surface.address);
}
attribute = &headset_attribute;
buffers = headset_buffers.data();
category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED;
}
if (category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED &&
category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED) {
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
@@ -1871,7 +1931,8 @@ static int ValidateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO
}
}
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ) {
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ &&
mode != Hmd2::OUTPUT_MODE_89_91HZ && mode != Hmd2::OUTPUT_MODE_119_88HZ) {
return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE;
}