Compare commits

...

1 Commits

Author SHA1 Message Date
Leandro Bras 742fe17173 vr: add OpenXR headset output, VR tracker and VR controller input behind --vr (#1726) 2026-10-20 02:18:51 +02:00
17 changed files with 1637 additions and 28 deletions
+17
View File
@@ -25,6 +25,23 @@ FetchContent_MakeAvailable(opus)
set(BUILD_TESTING "${kyty_build_testing_saved}")
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
# Headsets are driven through OpenXR. Only the static loader is built; the runtime that serves it
# (for example SteamVR, Monado or Virtual Desktop) comes from the user's system.
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_SYSTEM_JSONCPP OFF CACHE BOOL "" FORCE)
FetchContent_Declare(openxr
GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git
GIT_TAG f2448a8797c85814aa892efc1ab8707900fbcc78 # release-1.1.63
GIT_SHALLOW FALSE)
FetchContent_MakeAvailable(openxr)
# Keep the loader's headers and library out of the installed package.
set_property(DIRECTORY "${openxr_SOURCE_DIR}" PROPERTY EXCLUDE_FROM_ALL TRUE)
# The loader uses exceptions internally.
if(NOT KYTY_CLANG_CL)
target_compile_options(openxr_loader PRIVATE -fexceptions)
endif()
install(FILES "${openxr_SOURCE_DIR}/LICENSE" DESTINATION "licenses/openxr")
# The guest instruction patcher needs a decoder/encoder and a small
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+1 -1
View File
@@ -336,7 +336,7 @@ if (KYTY_CLANG_CL)
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
endif()
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus OpenXR::openxr_loader)
# Linux system libraries.
if(LINUX)
+762
View File
@@ -0,0 +1,762 @@
#include "graphics/presentation/openxrHeadset.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "common/threads.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/presentation/vrInput.h"
#include "libs/hmd2.h"
#define XR_USE_GRAPHICS_API_VULKAN
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fmt/format.h>
#include <mutex>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#include <string>
#include <thread>
namespace Libs::Vr {
namespace {
using namespace Libs::Graphics;
enum Action { TRIGGER, SQUEEZE, STICK, STICK_CLICK, PRIMARY, SECONDARY, MENU, GRIP, ACTION_COUNT };
// Touch controller bindings: the path under /user/hand/<side>/input/ per hand, or null.
constexpr struct {
const char* name;
XrActionType type;
const char* path[2];
} ACTIONS[ACTION_COUNT] = {
{"trigger", XR_ACTION_TYPE_FLOAT_INPUT, {"trigger/value", "trigger/value"}},
{"squeeze", XR_ACTION_TYPE_FLOAT_INPUT, {"squeeze/value", "squeeze/value"}},
{"thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT, {"thumbstick", "thumbstick"}},
{"thumbstick_click", XR_ACTION_TYPE_BOOLEAN_INPUT, {"thumbstick/click", "thumbstick/click"}},
{"primary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"x/click", "a/click"}},
{"secondary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"y/click", "b/click"}},
{"menu", XR_ACTION_TYPE_BOOLEAN_INPUT, {"menu/click", nullptr}},
{"grip_pose", XR_ACTION_TYPE_POSE_INPUT, {"grip/pose", "grip/pose"}},
};
constexpr const char* HANDS[2] = {"left", "right"};
// Eye images move from the render thread to the headset thread through these. One is being written,
// one is the latest recorded frame and one may be held by the headset thread until the GPU is done.
constexpr int STAGING_COUNT = 3;
struct Staging {
VulkanImage image;
uint64_t tick = 0;
VrPose head; // The head pose the game last read, so the picture can be placed in the world.
};
struct State {
XrInstance instance = XR_NULL_HANDLE;
XrSystemId system = XR_NULL_SYSTEM_ID;
XrSession session = XR_NULL_HANDLE;
XrSpace view_space = XR_NULL_HANDLE;
XrSpace local_space = XR_NULL_HANDLE;
XrSwapchain swapchain = XR_NULL_HANDLE; // Two array layers, one per eye.
std::vector<XrSwapchainImageVulkanKHR> swapchain_images;
vk::Extent2D eye {};
std::vector<std::string> extensions[2]; // Vulkan instance and device extensions.
PFN_xrGetVulkanGraphicsDeviceKHR get_graphics_device = nullptr;
GraphicContext* graphics = nullptr;
std::thread thread;
std::atomic_bool running {false};
// Headset thread only.
XrSessionState session_state = XR_SESSION_STATE_UNKNOWN;
bool session_running = false;
uint64_t shown_tick = 0;
VrPose shown_head;
vk::CommandPool pool;
vk::CommandBuffer command;
vk::Fence fence;
std::mutex mutex;
Staging staging[STAGING_COUNT];
int latest = -1;
int held = -1;
MasterSemaphore* semaphore = nullptr;
XrActionSet action_set = XR_NULL_HANDLE;
XrAction actions[ACTION_COUNT] {};
XrPath hand_path[2] {};
XrSpace grip_space[2] {};
std::mutex input_mutex;
VrControllerInput input[2];
VrPose head;
VrPose last_head_read;
};
State g;
bool Ok(XrResult result, const char* what) {
if (XR_SUCCEEDED(result)) {
return true;
}
char text[XR_MAX_RESULT_STRING_SIZE] = {};
xrResultToString(g.instance, result, text);
Log::WriteToConsoleAndLog(fmt::format("OpenXR: {} failed: {}\n", what, text));
return false;
}
// Destroying the instance also destroys the session, spaces, swapchain and actions.
void DestroyInstance(const char* reason) {
if (reason != nullptr) {
Log::WriteToConsoleAndLog(
fmt::format("OpenXR: {}; showing the eye image in the window only\n", reason));
}
if (g.instance != XR_NULL_HANDLE) {
xrDestroyInstance(g.instance);
}
g.instance = XR_NULL_HANDLE;
g.session = XR_NULL_HANDLE;
}
struct Access {
vk::PipelineStageFlags2 stage;
vk::AccessFlags2 access;
};
constexpr Access NO_ACCESS {vk::PipelineStageFlagBits2::eNone, vk::AccessFlagBits2::eNone};
constexpr Access TRANSFER {vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone};
constexpr Access TRANSFER_READ {vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferRead};
constexpr Access TRANSFER_WRITE {vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite};
constexpr Access COLOR_ATTACHMENT {vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::AccessFlagBits2::eColorAttachmentRead |
vk::AccessFlagBits2::eColorAttachmentWrite};
void Barrier(vk::CommandBuffer command, vk::Image image, vk::ImageLayout from, vk::ImageLayout to,
uint32_t layers, Access src, Access dst) {
vk::ImageMemoryBarrier2 barrier {};
barrier.srcStageMask = src.stage;
barrier.srcAccessMask = src.access;
barrier.dstStageMask = dst.stage;
barrier.dstAccessMask = dst.access;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, layers};
vk::DependencyInfo dependency {};
dependency.imageMemoryBarrierCount = 1;
dependency.pImageMemoryBarriers = &barrier;
command.pipelineBarrier2(dependency);
}
XrQuaternionf Multiply(const XrQuaternionf& a, const XrQuaternionf& b) {
return {a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z};
}
XrVector3f Rotate(const XrQuaternionf& q, const XrVector3f& v) {
const XrVector3f t {2.0f * (q.y * v.z - q.z * v.y), 2.0f * (q.z * v.x - q.x * v.z),
2.0f * (q.x * v.y - q.y * v.x)};
return {v.x + q.w * t.x + (q.y * t.z - q.z * t.y), v.y + q.w * t.y + (q.z * t.x - q.x * t.z),
v.z + q.w * t.z + (q.x * t.y - q.y * t.x)};
}
VrPose Locate(XrSpace space, XrTime time) {
VrPose pose;
XrSpaceLocation location {XR_TYPE_SPACE_LOCATION};
if (XR_SUCCEEDED(xrLocateSpace(space, g.local_space, time, &location)) &&
(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0) {
const auto& [q, p] = location.pose;
pose = {true, {q.x, q.y, q.z, q.w}, {p.x, p.y, p.z}};
}
return pose;
}
// The swapchain stores the eyes in a gamma-encoded format, so a raw copy of the display-encoded
// game image comes out right. Staging uses the same layout with UNORM, which a blit writes from the
// raw UNORM or float presentation frame without conversion.
bool CreateSwapchain() {
uint32_t count = 0;
xrEnumerateSwapchainFormats(g.session, 0, &count, nullptr);
std::vector<int64_t> formats(count);
xrEnumerateSwapchainFormats(g.session, count, &count, formats.data());
constexpr std::pair<vk::Format, vk::Format> candidates[] = {
{vk::Format::eR8G8B8A8Srgb, vk::Format::eR8G8B8A8Unorm},
{vk::Format::eB8G8R8A8Srgb, vk::Format::eB8G8R8A8Unorm},
{vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm},
{vk::Format::eB8G8R8A8Unorm, vk::Format::eB8G8R8A8Unorm},
};
const auto* format =
std::find_if(std::begin(candidates), std::end(candidates), [&](const auto& c) {
return std::find(formats.begin(), formats.end(), static_cast<int64_t>(c.first)) !=
formats.end();
});
if (format == std::end(candidates)) {
return false;
}
XrSwapchainCreateInfo info {XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
info.format = static_cast<int64_t>(format->first);
info.sampleCount = 1;
info.width = g.eye.width;
info.height = g.eye.height;
info.faceCount = 1;
info.arraySize = 2;
info.mipCount = 1;
if (!Ok(xrCreateSwapchain(g.session, &info, &g.swapchain), "xrCreateSwapchain")) {
return false;
}
xrEnumerateSwapchainImages(g.swapchain, 0, &count, nullptr);
g.swapchain_images.assign(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR});
xrEnumerateSwapchainImages(
g.swapchain, count, &count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(g.swapchain_images.data()));
vk::ImageCreateInfo staging {};
staging.imageType = vk::ImageType::e2D;
staging.format = format->second;
staging.extent = vk::Extent3D {g.eye.width * 2, g.eye.height, 1};
staging.mipLevels = 1;
staging.arrayLayers = 1;
staging.samples = vk::SampleCountFlagBits::e1;
staging.tiling = vk::ImageTiling::eOptimal;
staging.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst;
for (auto& slot: g.staging) {
if (!g.graphics->CreateImage(staging, slot.image)) {
return false;
}
}
Log::WriteToConsoleAndLog(fmt::format("OpenXR: eye swapchain {}x{} {}\n", g.eye.width,
g.eye.height, vk::to_string(format->first)));
return true;
}
bool CreateInput() {
XrActionSetCreateInfo set {XR_TYPE_ACTION_SET_CREATE_INFO};
std::strcpy(set.actionSetName, "gameplay");
std::strcpy(set.localizedActionSetName, "Gameplay");
if (!Ok(xrCreateActionSet(g.instance, &set, &g.action_set), "xrCreateActionSet")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
xrStringToPath(g.instance, fmt::format("/user/hand/{}", HANDS[hand]).c_str(),
&g.hand_path[hand]);
}
std::vector<XrActionSuggestedBinding> bindings;
for (int i = 0; i < ACTION_COUNT; i++) {
XrActionCreateInfo info {XR_TYPE_ACTION_CREATE_INFO};
info.actionType = ACTIONS[i].type;
info.countSubactionPaths = 2;
info.subactionPaths = g.hand_path;
std::strcpy(info.actionName, ACTIONS[i].name);
std::strcpy(info.localizedActionName, ACTIONS[i].name);
if (!Ok(xrCreateAction(g.action_set, &info, &g.actions[i]), "xrCreateAction")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
if (ACTIONS[i].path[hand] != nullptr) {
XrPath path = XR_NULL_PATH;
xrStringToPath(
g.instance,
fmt::format("/user/hand/{}/input/{}", HANDS[hand], ACTIONS[i].path[hand])
.c_str(),
&path);
bindings.push_back({g.actions[i], path});
}
}
}
XrInteractionProfileSuggestedBinding suggested {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
xrStringToPath(g.instance, "/interaction_profiles/oculus/touch_controller",
&suggested.interactionProfile);
suggested.countSuggestedBindings = static_cast<uint32_t>(bindings.size());
suggested.suggestedBindings = bindings.data();
XrSessionActionSetsAttachInfo attach {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
attach.countActionSets = 1;
attach.actionSets = &g.action_set;
if (!Ok(xrSuggestInteractionProfileBindings(g.instance, &suggested), "suggest bindings") ||
!Ok(xrAttachSessionActionSets(g.session, &attach), "attach action sets")) {
return false;
}
for (int hand = 0; hand < 2; hand++) {
XrActionSpaceCreateInfo space {XR_TYPE_ACTION_SPACE_CREATE_INFO};
space.action = g.actions[GRIP];
space.subactionPath = g.hand_path[hand];
space.poseInActionSpace = {{0, 0, 0, 1}, {0, 0, 0}};
if (!Ok(xrCreateActionSpace(g.session, &space, &g.grip_space[hand]), "grip space")) {
return false;
}
}
return true;
}
// The head is tracked while the frame loop runs; the controllers only while the session has focus.
void PollInput(XrTime time) {
const auto head = Locate(g.view_space, time);
VrControllerInput input[2];
XrActiveActionSet active {g.action_set, XR_NULL_PATH};
XrActionsSyncInfo sync {XR_TYPE_ACTIONS_SYNC_INFO};
sync.countActiveActionSets = 1;
sync.activeActionSets = &active;
if (g.action_set != XR_NULL_HANDLE && g.session_state == XR_SESSION_STATE_FOCUSED &&
XR_SUCCEEDED(xrSyncActions(g.session, &sync))) {
for (int hand = 0; hand < 2; hand++) {
const auto get = [&](Action action, auto read, auto state) {
XrActionStateGetInfo info {XR_TYPE_ACTION_STATE_GET_INFO};
info.action = g.actions[action];
info.subactionPath = g.hand_path[hand];
read(g.session, &info, &state);
return state;
};
const auto value = [&](Action action) {
return get(action, xrGetActionStateFloat,
XrActionStateFloat {XR_TYPE_ACTION_STATE_FLOAT})
.currentState;
};
const auto flag = [&](Action action) {
return get(action, xrGetActionStateBoolean,
XrActionStateBoolean {XR_TYPE_ACTION_STATE_BOOLEAN})
.currentState == XR_TRUE;
};
const auto stick = get(STICK, xrGetActionStateVector2f,
XrActionStateVector2f {XR_TYPE_ACTION_STATE_VECTOR2F});
auto& out = input[hand];
out.trigger = value(TRIGGER);
out.squeeze = value(SQUEEZE);
out.stick_x = stick.currentState.x;
out.stick_y = stick.currentState.y;
out.stick_click = flag(STICK_CLICK);
out.primary = flag(PRIMARY);
out.secondary = flag(SECONDARY);
out.menu = flag(MENU);
out.pose = Locate(g.grip_space[hand], time);
out.connected =
get(GRIP, xrGetActionStatePose, XrActionStatePose {XR_TYPE_ACTION_STATE_POSE})
.isActive == XR_TRUE;
}
}
std::lock_guard lock(g.input_mutex);
g.head = head;
g.input[0] = input[0];
g.input[1] = input[1];
}
void HandleEvents() {
for (XrEventDataBuffer event {XR_TYPE_EVENT_DATA_BUFFER};
xrPollEvent(g.instance, &event) == XR_SUCCESS; event = {XR_TYPE_EVENT_DATA_BUFFER}) {
if (event.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) {
g.running = false;
continue;
}
if (event.type != XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
continue;
}
g.session_state = reinterpret_cast<const XrEventDataSessionStateChanged&>(event).state;
LOGF("OpenXR: session state %d\n", static_cast<int>(g.session_state));
switch (g.session_state) {
case XR_SESSION_STATE_READY: {
XrSessionBeginInfo begin {XR_TYPE_SESSION_BEGIN_INFO};
begin.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
g.session_running = Ok(xrBeginSession(g.session, &begin), "xrBeginSession");
break;
}
case XR_SESSION_STATE_STOPPING: {
xrEndSession(g.session);
g.session_running = false;
std::lock_guard lock(g.input_mutex);
g.head = {};
g.input[0] = {};
g.input[1] = {};
break;
}
case XR_SESSION_STATE_EXITING:
case XR_SESSION_STATE_LOSS_PENDING: g.running = false; break;
default: break;
}
}
}
bool CopyToSwapchain(const VulkanImage& staging) {
uint32_t image = 0;
XrSwapchainImageAcquireInfo acquire {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
if (!Ok(xrAcquireSwapchainImage(g.swapchain, &acquire, &image), "acquire")) {
return false;
}
}
XrSwapchainImageWaitInfo wait {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait.timeout = XR_INFINITE_DURATION;
bool copied = Ok(xrWaitSwapchainImage(g.swapchain, &wait), "wait swapchain image");
if (copied) {
const auto target = g.swapchain_images[image].image;
(void)g.command.reset();
vk::CommandBufferBeginInfo begin {};
begin.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
(void)g.command.begin(&begin);
Barrier(g.command, target, vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal, 2, NO_ACCESS, TRANSFER_WRITE);
vk::ImageCopy copies[2] {};
for (uint32_t eye = 0; eye < 2; eye++) {
copies[eye].srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
copies[eye].srcOffset = vk::Offset3D {static_cast<int32_t>(eye * g.eye.width), 0, 0};
copies[eye].dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, eye, 1};
copies[eye].extent = vk::Extent3D {g.eye.width, g.eye.height, 1};
}
g.command.copyImage(staging.image, vk::ImageLayout::eTransferSrcOptimal, target,
vk::ImageLayout::eTransferDstOptimal, 2, copies);
Barrier(g.command, target, vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eColorAttachmentOptimal, 2, TRANSFER_WRITE, COLOR_ATTACHMENT);
(void)g.command.end();
vk::SubmitInfo submit {};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &g.command;
(void)g.graphics->device.resetFences(1, &g.fence);
vk::Result result {};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
result = g.graphics->queue.submit(1, &submit, g.fence);
}
copied = result == vk::Result::eSuccess &&
g.graphics->device.waitForFences(1, &g.fence, VK_TRUE, UINT64_MAX) ==
vk::Result::eSuccess;
}
XrSwapchainImageReleaseInfo release {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
Common::LockGuard queue_lock(g.graphics->queue_mutex);
return Ok(xrReleaseSwapchainImage(g.swapchain, &release), "release") && copied;
}
// Holds the newest recorded eye image until the GPU has finished it, without waiting, then copies
// it into the swapchain. The swapchain keeps the last copy, so frames in between show it again.
bool UpdateSwapchain() {
int index = -1;
MasterSemaphore* semaphore = nullptr;
{
std::lock_guard lock(g.mutex);
if (g.held < 0 && g.latest >= 0 && g.staging[g.latest].tick != g.shown_tick) {
g.held = g.latest;
}
index = g.held;
semaphore = g.semaphore;
}
if (index >= 0) {
const auto& slot = g.staging[index];
semaphore->Refresh();
if (semaphore->IsFree(slot.tick)) {
if (CopyToSwapchain(slot.image)) {
g.shown_tick = slot.tick;
g.shown_head = slot.head;
}
std::lock_guard lock(g.mutex);
g.held = -1;
}
}
return g.shown_tick != 0;
}
// The game drew the picture for the head pose it read, so it is placed in the world at that pose
// and the runtime reprojects it as the head moves. Eye offsets come from the runtime.
void PlaceViews(XrTime time, XrCompositionLayerProjection& layer,
XrCompositionLayerProjectionView (&views)[2]) {
XrViewLocateInfo locate {XR_TYPE_VIEW_LOCATE_INFO};
locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
locate.displayTime = time;
locate.space = g.view_space;
XrViewState state {XR_TYPE_VIEW_STATE};
XrView located[2] {{XR_TYPE_VIEW}, {XR_TYPE_VIEW}};
uint32_t count = 0;
const bool valid =
XR_SUCCEEDED(xrLocateViews(g.session, &locate, &state, 2, &count, located)) &&
(state.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
const auto& head = g.shown_head;
const XrQuaternionf head_q {head.orientation[0], head.orientation[1], head.orientation[2],
head.orientation[3]};
const XrVector3f head_p {head.position[0], head.position[1], head.position[2]};
// The outer side of each eye is its temporal side: left for the left eye.
const float out = std::atan(Hmd2::FOV_TAN_OUT);
const float in = std::atan(Hmd2::FOV_TAN_IN);
const float top = std::atan(Hmd2::FOV_TAN_TOP);
const float bottom = std::atan(Hmd2::FOV_TAN_BOTTOM);
for (int eye = 0; eye < 2; eye++) {
const float side = eye == 0 ? -Hmd2::EYE_OFFSET_M : Hmd2::EYE_OFFSET_M;
auto& view = views[eye];
view.pose = valid ? located[eye].pose : XrPosef {{0, 0, 0, 1}, {side, 0, 0}};
if (head.valid) {
const auto offset = Rotate(head_q, view.pose.position);
view.pose.position = {head_p.x + offset.x, head_p.y + offset.y, head_p.z + offset.z};
view.pose.orientation = Multiply(head_q, view.pose.orientation);
}
view.fov = {eye == 0 ? -out : -in, eye == 0 ? in : out, top, -bottom};
view.subImage.swapchain = g.swapchain;
view.subImage.imageRect = {
{0, 0}, {static_cast<int32_t>(g.eye.width), static_cast<int32_t>(g.eye.height)}};
view.subImage.imageArrayIndex = static_cast<uint32_t>(eye);
}
layer.space = head.valid ? g.local_space : g.view_space;
layer.viewCount = 2;
layer.views = views;
}
void ThreadMain() {
while (g.running) {
HandleEvents();
XrFrameWaitInfo wait {XR_TYPE_FRAME_WAIT_INFO};
XrFrameState frame {XR_TYPE_FRAME_STATE};
if (!g.session_running || !Ok(xrWaitFrame(g.session, &wait, &frame), "xrWaitFrame")) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
PollInput(frame.predictedDisplayTime);
XrFrameBeginInfo begin {XR_TYPE_FRAME_BEGIN_INFO};
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
if (!Ok(xrBeginFrame(g.session, &begin), "xrBeginFrame")) {
continue;
}
}
XrCompositionLayerProjection layer {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
XrCompositionLayerProjectionView views[2] {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
const auto* layers = reinterpret_cast<const XrCompositionLayerBaseHeader*>(&layer);
XrFrameEndInfo end {XR_TYPE_FRAME_END_INFO};
end.displayTime = frame.predictedDisplayTime;
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
if (frame.shouldRender == XR_TRUE && UpdateSwapchain()) {
PlaceViews(frame.predictedDisplayTime, layer, views);
end.layerCount = 1;
end.layers = &layers;
}
Common::LockGuard queue_lock(g.graphics->queue_mutex);
(void)Ok(xrEndFrame(g.session, &end), "xrEndFrame");
}
}
} // namespace
void OpenXrInit() {
if (!Config::VrEnabled()) {
return;
}
if (const char* env = std::getenv("KYTY_OPENXR");
env != nullptr && std::strcmp(env, "0") == 0) {
return;
}
const char* extensions[] = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
XrInstanceCreateInfo info {XR_TYPE_INSTANCE_CREATE_INFO};
std::strcpy(info.applicationInfo.applicationName, "KytyPS5");
std::strcpy(info.applicationInfo.engineName, "Kyty");
info.applicationInfo.apiVersion = XR_API_VERSION_1_0;
info.enabledExtensionCount = 1;
info.enabledExtensionNames = extensions;
if (XR_FAILED(xrCreateInstance(&info, &g.instance))) {
DestroyInstance("no runtime available");
return;
}
XrSystemGetInfo system {XR_TYPE_SYSTEM_GET_INFO};
system.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
if (XR_FAILED(xrGetSystem(g.instance, &system, &g.system))) {
DestroyInstance("no headset available");
return;
}
PFN_xrGetVulkanGraphicsRequirementsKHR get_requirements = nullptr;
PFN_xrGetVulkanInstanceExtensionsKHR get_extensions[2] {};
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsRequirementsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_requirements));
xrGetInstanceProcAddr(g.instance, "xrGetVulkanInstanceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[0]));
// Both queries share one signature.
xrGetInstanceProcAddr(g.instance, "xrGetVulkanDeviceExtensionsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[1]));
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsDeviceKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&g.get_graphics_device));
XrGraphicsRequirementsVulkanKHR requirements {XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
if (get_requirements == nullptr || get_extensions[0] == nullptr ||
get_extensions[1] == nullptr || g.get_graphics_device == nullptr ||
XR_FAILED(get_requirements(g.instance, g.system, &requirements))) {
DestroyInstance("the runtime does not support XR_KHR_vulkan_enable");
return;
}
for (int i = 0; i < 2; i++) {
uint32_t size = 0;
get_extensions[i](g.instance, g.system, 0, &size, nullptr);
std::string names(size, '\0');
get_extensions[i](g.instance, g.system, size, &size, names.data());
g.extensions[i] = Common::Split(names.c_str(), ' ');
}
XrViewConfigurationView views[2] {{XR_TYPE_VIEW_CONFIGURATION_VIEW},
{XR_TYPE_VIEW_CONFIGURATION_VIEW}};
uint32_t count = 0;
xrEnumerateViewConfigurationViews(g.instance, g.system,
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 2, &count, views);
g.eye = {views[0].recommendedImageRectWidth, views[0].recommendedImageRectHeight};
XrSystemProperties properties {XR_TYPE_SYSTEM_PROPERTIES};
xrGetSystemProperties(g.instance, g.system, &properties);
Log::WriteToConsoleAndLog(fmt::format("OpenXR: headset '{}' ready\n", properties.systemName));
}
void OpenXrAppendExtensions(std::vector<const char*>& out,
const std::vector<vk::ExtensionProperties>& available, bool device) {
if (g.instance == XR_NULL_HANDLE) {
return;
}
for (const auto& name: g.extensions[device ? 1 : 0]) {
const auto same = [&](const char* other) { return name == other; };
if (std::any_of(available.begin(), available.end(),
[&](const auto& extension) { return same(extension.extensionName); }) &&
std::none_of(out.begin(), out.end(), same)) {
out.push_back(name.c_str());
}
}
}
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out) {
VkPhysicalDevice device = VK_NULL_HANDLE;
if (g.instance == XR_NULL_HANDLE ||
XR_FAILED(g.get_graphics_device(g.instance, g.system, static_cast<VkInstance>(instance),
&device))) {
return false;
}
out = vk::PhysicalDevice(device);
return true;
}
void OpenXrStartSession(GraphicContext& graphics) {
if (g.instance == XR_NULL_HANDLE) {
return;
}
// The runtime must be asked for its GPU before a session is created.
vk::PhysicalDevice required;
if (!OpenXrPhysicalDevice(graphics.instance, required) ||
required != graphics.physical_device) {
DestroyInstance("the headset runs on another GPU; select it with --gpu-index");
return;
}
g.graphics = &graphics;
XrGraphicsBindingVulkanKHR binding {XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR};
binding.instance = static_cast<VkInstance>(graphics.instance);
binding.physicalDevice = static_cast<VkPhysicalDevice>(graphics.physical_device);
binding.device = static_cast<VkDevice>(graphics.device);
binding.queueFamilyIndex = graphics.queue_family;
binding.queueIndex = 0;
XrSessionCreateInfo info {XR_TYPE_SESSION_CREATE_INFO};
info.next = &binding;
info.systemId = g.system;
XrReferenceSpaceCreateInfo space {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
space.poseInReferenceSpace = {{0, 0, 0, 1}, {0, 0, 0}};
const auto reference = [&](XrReferenceSpaceType type, XrSpace& out) {
space.referenceSpaceType = type;
return Ok(xrCreateReferenceSpace(g.session, &space, &out), "xrCreateReferenceSpace");
};
if (!Ok(xrCreateSession(g.instance, &info, &g.session), "xrCreateSession") ||
!reference(XR_REFERENCE_SPACE_TYPE_VIEW, g.view_space) ||
!reference(XR_REFERENCE_SPACE_TYPE_LOCAL, g.local_space) || !CreateSwapchain()) {
DestroyInstance("the headset session could not be created");
return;
}
if (!CreateInput()) {
g.action_set = XR_NULL_HANDLE;
}
vk::CommandPoolCreateInfo pool {};
pool.queueFamilyIndex = graphics.queue_family;
pool.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
(void)graphics.device.createCommandPool(&pool, nullptr, &g.pool);
vk::CommandBufferAllocateInfo allocate {};
allocate.commandPool = g.pool;
allocate.level = vk::CommandBufferLevel::ePrimary;
allocate.commandBufferCount = 1;
(void)graphics.device.allocateCommandBuffers(&allocate, &g.command);
vk::FenceCreateInfo fence {};
(void)graphics.device.createFence(&fence, nullptr, &g.fence);
g.running = true;
g.thread = std::thread(ThreadMain);
}
void OpenXrShutdown() {
g.running = false;
if (g.thread.joinable()) {
g.thread.join();
}
DestroyInstance(nullptr);
if (g.graphics == nullptr) {
return;
}
{
Common::LockGuard queue_lock(g.graphics->queue_mutex);
(void)g.graphics->device.waitIdle();
}
for (auto& slot: g.staging) {
if (slot.image.image != nullptr) {
g.graphics->DeleteImage(slot.image);
}
}
g.graphics->device.destroyFence(g.fence, nullptr);
g.graphics->device.destroyCommandPool(g.pool, nullptr);
g.graphics = nullptr;
}
VrPose OpenXrGetHeadPose() {
if (!g.running) {
return {};
}
std::lock_guard lock(g.input_mutex);
if (g.head.valid) {
g.last_head_read = g.head;
}
return g.head;
}
VrControllerInput OpenXrGetController(int hand) {
if (!g.running) {
return {};
}
std::lock_guard lock(g.input_mutex);
return g.input[hand];
}
void OpenXrQueueEyeSource(CommandBuffer& command, const VulkanImage& source) {
if (!g.running) {
return;
}
std::lock_guard lock(g.mutex);
int index = 0;
while (index == g.held || index == g.latest) {
index++;
}
auto& slot = g.staging[index];
const auto vk_command = command.Handle();
Barrier(vk_command, slot.image.image, vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal, 1, TRANSFER, TRANSFER_WRITE);
vk::ImageBlit blit {};
blit.srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
blit.srcOffsets[1] = vk::Offset3D {static_cast<int32_t>(source.extent.width),
static_cast<int32_t>(source.extent.height), 1};
blit.dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
blit.dstOffsets[1] = vk::Offset3D {static_cast<int32_t>(slot.image.extent.width),
static_cast<int32_t>(slot.image.extent.height), 1};
vk_command.blitImage(source.image, vk::ImageLayout::eTransferSrcOptimal, slot.image.image,
vk::ImageLayout::eTransferDstOptimal, 1, &blit, vk::Filter::eLinear);
Barrier(vk_command, slot.image.image, vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eTransferSrcOptimal, 1, TRANSFER_WRITE, TRANSFER_READ);
auto& scheduler = command.GetContext().GetCommandScheduler();
slot.tick = scheduler.CurrentTick();
g.semaphore = &scheduler.GetMasterSemaphore();
g.latest = index;
std::lock_guard input_lock(g.input_mutex);
slot.head = g.last_head_read;
}
} // namespace Libs::Vr
+41
View File
@@ -0,0 +1,41 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
#include "graphics/host_gpu/vulkanCommon.h"
#include <vector>
namespace Libs::Graphics {
struct GraphicContext;
struct VulkanImage;
class CommandBuffer;
} // namespace Libs::Graphics
// Output to a real headset through OpenXR. Everything here is inert unless the virtual headset is
// enabled (--vr) and an OpenXR runtime with a headset is available; KYTY_OPENXR=0 turns it off.
namespace Libs::Vr {
// Creates the OpenXR instance. Call before the Vulkan instance exists, because the runtime names
// the Vulkan extensions and the GPU it needs.
void OpenXrInit();
// Appends the Vulkan instance or device extensions the runtime needs, when available.
void OpenXrAppendExtensions(std::vector<const char*>& out,
const std::vector<vk::ExtensionProperties>& available, bool device);
// The physical device the runtime renders with. False when OpenXR is not in use.
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out);
// Creates the session and starts the headset thread. Call once the device and queue exist.
void OpenXrStartSession(Graphics::GraphicContext& graphics);
// Stops the headset thread and destroys the session. Call before the device is destroyed.
void OpenXrShutdown();
// Render thread, headset flips only: scales the side-by-side eye image into a staging image for the
// headset thread. The source is in transfer-source layout.
void OpenXrQueueEyeSource(Graphics::CommandBuffer& command, const Graphics::VulkanImage& source);
} // namespace Libs::Vr
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
+1 -1
View File
@@ -27,7 +27,7 @@ public:
KYTY_CLASS_NO_COPY(Presenter);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
bool hdr = false);
bool hdr = false, bool headset = false);
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
CommandBuffer* producer = nullptr);
[[nodiscard]] bool PresentLastFrame();
+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;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
namespace Libs::Vr {
// A pose in the tracking space: metres, right-handed, +y up, -z forward (the OpenXR convention).
struct VrPose {
bool valid = false;
float orientation[4] = {0.0f, 0.0f, 0.0f, 1.0f}; // x, y, z, w
float position[3] = {0.0f, 0.0f, 0.0f};
};
// One headset controller. Sticks are -1..1 with +y up, the trigger and squeeze 0..1.
struct VrControllerInput {
bool connected = false;
VrPose pose;
float trigger = 0.0f;
float squeeze = 0.0f;
float stick_x = 0.0f;
float stick_y = 0.0f;
bool primary = false; // X on the left controller, A on the right one.
bool secondary = false; // Y on the left controller, B on the right one.
bool stick_click = false;
bool menu = false;
};
// Latest head pose; not valid, at the origin, while no headset session tracks the head.
[[nodiscard]] VrPose OpenXrGetHeadPose();
// Latest state of the left (0) or right (1) controller. While it is not connected the caller keeps
// using the keyboard or gamepad.
[[nodiscard]] VrControllerInput OpenXrGetController(int hand);
} // namespace Libs::Vr
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
@@ -11,6 +11,7 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/openxrHeadset.h"
#include "graphics/presentation/performanceOverlay.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/systemOverlay.h"
@@ -918,7 +919,8 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
Presenter::~Presenter() = default;
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr,
bool headset) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
auto frame_format = info.pixel_format;
@@ -938,6 +940,12 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
frame->Configure(m_impl->window.graphic_ctx,
{image.backing.extent.width, image.backing.extent.height}, frame_format);
frame->CopyFrom(buffer, image);
if (headset) {
// The frame holds the raw display-encoded pixels the headset needs.
frame->Transit(buffer.Handle(), vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead);
Vr::OpenXrQueueEyeSource(buffer, frame->image);
}
return *frame;
}
@@ -1,6 +1,3 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
@@ -14,6 +11,7 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/openxrHeadset.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/videoOut.h"
@@ -23,6 +21,8 @@
#include "libs/controller.h"
#include "loader/systemContent.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
@@ -170,6 +170,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
LOGF("Vulkan GPU index %d is unavailable; selecting automatically\n",
Config::GetGpuIndex());
}
} else if (vk::PhysicalDevice headset; Vr::OpenXrPhysicalDevice(instance, headset)) {
devices = {headset}; // The headset runtime renders on this GPU.
}
vk::PhysicalDevice best_device = nullptr;
@@ -876,8 +878,11 @@ void WindowContext::CreateVulkan() {
}
VULKAN_HPP_DEFAULT_DISPATCHER.init(get_instance_proc_addr);
Vr::OpenXrInit();
VulkanExtensions r;
VulkanGetExtensions(r);
Vr::OpenXrAppendExtensions(r.required_extensions, r.available_extensions, false);
VulkanCheckInstanceVersion();
vk::ApplicationInfo app_info {};
@@ -1051,6 +1056,7 @@ void WindowContext::CreateVulkan() {
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME);
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME);
}
Vr::OpenXrAppendExtensions(device_extensions, available_extensions, true);
}
graphic_ctx.device = VulkanCreateDevice(graphic_ctx, device_extensions);
@@ -1068,6 +1074,7 @@ void WindowContext::CreateVulkan() {
render_context = std::make_unique<RenderContext>(graphic_ctx);
LibKernel::Memory::InstallGpuResources(render_context.get());
presenter = std::make_unique<Presenter>(*this);
Vr::OpenXrStartSession(graphic_ctx);
}
void WindowContext::RefreshSurfaceCapabilities() {
@@ -1090,6 +1097,7 @@ void WindowContext::RecreateSurface() {
}
WindowContext::~WindowContext() {
Vr::OpenXrShutdown();
ShutdownSystemOverlayInput();
presenter.reset();
LibKernel::Memory::InstallGpuResources(nullptr);
+24 -9
View File
@@ -25,12 +25,8 @@ namespace Libs::Controller {
LIB_NAME("Pad", "Pad");
constexpr int PAD_ERROR_INVALID_ARG = -2137915391; /* 0x80920001 */
constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; /* 0x80920003 */
constexpr int PAD_ERROR_ALREADY_OPENED = -2137915388; /* 0x80920004 */
constexpr int PAD_ERROR_NOT_INITIALIZED = -2137915387; /* 0x80920005 */
constexpr int PAD_ERROR_NO_HANDLE = -2137915384; /* 0x80920008 */
constexpr int PAD_HANDLE = 1;
constexpr int PAD_HANDLE = 1;
static_assert(PAD_HANDLE + Config::MAX_PLAYERS <= PAD_HANDLE_VR_LEFT);
constexpr uint32_t RUMBLE_DURATION_MS = 0xffff;
constexpr uint32_t RELEASE_FLUSH_MS = 50;
@@ -215,9 +211,11 @@ static GameController* PlayerForDevice(int id) {
}
static GameController* PlayerForHandle(int handle) {
return handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS
? g_players[handle - PAD_HANDLE].get()
: nullptr;
if (handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS) {
return g_players[handle - PAD_HANDLE].get();
}
// The Sense controller ports belong to the initial user.
return PadVrHand(handle) >= 0 ? g_players[0].get() : nullptr;
}
static void pad_fill_data(PadData* data, const ControllerState& state, bool connected,
@@ -1204,10 +1202,18 @@ int PadCheckHandle(int handle) {
if (g_players[0] == nullptr) {
return PAD_ERROR_NOT_INITIALIZED;
}
if (PadVrHand(handle) >= 0) {
return PadVrCheckHandle(handle);
}
auto* player = PlayerForHandle(handle);
return player != nullptr ? player->CheckPortHandle(handle) : PAD_ERROR_INVALID_HANDLE;
}
// Handle 0 is never valid, so this only reports whether scePadInit has run.
bool PadInitialized() {
return g_players[0] != nullptr && g_players[0]->CheckPortHandle(0) != PAD_ERROR_NOT_INITIALIZED;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
for (const auto& player: g_players) {
@@ -1225,6 +1231,9 @@ int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
"\t param = 0x%016" PRIx64 "\n",
user_id, type, index, reinterpret_cast<uint64_t>(param));
if (const int hand = PadVrPortHand(type); hand >= 0) {
return PadVrPort(user_id, hand, index, true);
}
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->OpenPort(user_id, type, index) : PAD_ERROR_INVALID_ARG;
}
@@ -1237,6 +1246,9 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
"\t index = %d\n",
user_id, type, index);
if (const int hand = PadVrPortHand(type); hand >= 0) {
return PadVrPort(user_id, hand, index, false);
}
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->GetPortHandle(user_id, type, index)
: PAD_ERROR_NO_HANDLE;
@@ -1245,6 +1257,9 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
int KYTY_SYSV_ABI PadClose(int handle) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
if (PadVrHand(handle) >= 0) {
return PadVrClose(handle);
}
auto* player = PlayerForHandle(handle);
if (player == nullptr) {
return PAD_ERROR_INVALID_HANDLE;
+32 -1
View File
@@ -18,6 +18,12 @@ struct Lifecycle {
constexpr int HOST_INPUT_CONTROLLER_ID = -1000;
constexpr int PAD_ERROR_INVALID_ARG = -2137915391; /* 0x80920001 */
constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; /* 0x80920003 */
constexpr int PAD_ERROR_ALREADY_OPENED = -2137915388; /* 0x80920004 */
constexpr int PAD_ERROR_NOT_INITIALIZED = -2137915387; /* 0x80920005 */
constexpr int PAD_ERROR_NO_HANDLE = -2137915384; /* 0x80920008 */
constexpr uint32_t PAD_BUTTON_L3 = 0x00000002;
constexpr uint32_t PAD_BUTTON_R3 = 0x00000004;
constexpr uint32_t PAD_BUTTON_OPTIONS = 0x00000008;
@@ -86,7 +92,32 @@ enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity }
void CycleSetting(Setting setting);
float GetSettingScale(Setting setting);
int PadCheckHandle(int handle);
int PadCheckHandle(int handle);
bool PadInitialized();
// Virtual headset only (padVr.cpp): the Sense controller ports of the initial user. Their handles
// stay outside the per-player range and otherwise act on the first player.
constexpr int PAD_HANDLE_VR_LEFT = 0x10; // The right controller follows it.
// 0 for the left and 1 for the right Sense controller handle, -1 for any other handle.
constexpr int PadVrHand(int handle) {
return handle == PAD_HANDLE_VR_LEFT || handle == PAD_HANDLE_VR_LEFT + 1
? handle - PAD_HANDLE_VR_LEFT
: -1;
}
// Hand of a Sense controller port type, -1 for other types or without the virtual headset.
int PadVrPortHand(int type);
// scePadOpen (open) or scePadGetHandle for a Sense controller port.
int PadVrPort(int user_id, int hand, int index, bool open);
int PadVrCheckHandle(int handle);
int PadVrClose(int handle);
struct PadVrControllerData;
int KYTY_SYSV_ABI PadVrControllerReadState(int handle, PadVrControllerData* data);
int KYTY_SYSV_ABI PadVrControllerSetTriggerEffects(int left_handle, int right_handle,
const PadTriggerEffectParam* param);
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
+29
View File
@@ -12,6 +12,17 @@ inline constexpr uint64_t OUTPUT_MODE_89_91HZ = 0x2000c;
inline constexpr uint64_t OUTPUT_MODE_119_88HZ = 0x2000f;
inline constexpr int32_t ERROR_REPROJECTION_NOT_INITIALIZED = -1972240373; // 0x8a72000b
// Virtual ER15 field of view per eye: 55/42.5/51.2/51.2 degrees. "Out" is the temporal side and
// "in" the nasal side. These tangents describe that geometry, not bit-exact firmware output.
inline constexpr float FOV_TAN_OUT = 1.4281480312f;
inline constexpr float FOV_TAN_IN = 0.9163311720f;
inline constexpr float FOV_TAN_TOP = 1.2437491417f;
inline constexpr float FOV_TAN_BOTTOM = 1.2437491417f;
// Each eye sits half the lens separation to the side of the head.
inline constexpr uint8_t LENS_SEPARATION_MM = 63;
inline constexpr float EYE_OFFSET_M = LENS_SEPARATION_MM / 2000.0f;
struct ReprojectionState {
std::atomic<uint64_t> output_mode {0};
std::atomic<uint32_t> timing_us {3000};
@@ -19,6 +30,24 @@ struct ReprojectionState {
[[nodiscard]] ReprojectionState* GetReprojectionState();
struct SceFVector3 {
float x;
float y;
float z;
};
// The headset video-out port registers SceHmd2ReprojectionBuffer objects. The eye texture of their
// first layer is the surface the game flips; both eyes sit side by side in it.
struct ReprojectionSurface {
uint64_t address;
uint32_t width;
uint32_t height;
uint64_t pixel_format; // SceVideoOutPixelFormat
};
[[nodiscard]] bool GetReprojectionSurface(const void* reprojection_buffer,
ReprojectionSurface& out);
} // namespace Libs::Hmd2
#endif // EMULATOR_SRC_LIBS_HMD2_H_
+215 -4
View File
@@ -1,15 +1,20 @@
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "graphics/guest_gpu/tile.h"
#include "graphics/host_gpu/renderer/image/imageInfo.h"
#include "graphics/shader/shaderBindings.h"
#include "kernel/pthread.h"
#include "libs/agc.h"
#include "libs/errno.h"
#include "libs/hmd2.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <new>
@@ -21,10 +26,19 @@ namespace Hmd2 {
constexpr int32_t ERROR_ALREADY_INITIALIZED = -1972240383; // 0x8a720001
constexpr int32_t ERROR_NOT_INITIALIZED = -1972240382; // 0x8a720002
constexpr int32_t ERROR_HANDLE_INVALID = -1972240381; // 0x8a720003
constexpr int32_t ERROR_PARAMETER_NULL = -1972240376; // 0x8a720008
constexpr int32_t ERROR_PARAMETER_INVALID = -1972240375; // 0x8a720009
constexpr int32_t ERROR_REPROJECTION_ALREADY_INITIALIZED = -1972240372; // 0x8a72000c
constexpr int32_t ERROR_REPROJECTION_IN_VR_MODE = -1972240356; // 0x8a72001c
// Any non-negative value is a valid headset handle.
constexpr int32_t HMD_HANDLE = 0x0f000001;
constexpr uint32_t REPROJECTION_LAYER_TYPE_VR3D = 0x10;
constexpr uint32_t REPROJECTION_LAYER_TYPE_VR2D = 0x12;
constexpr uint32_t REPROJECTION_MAX_SOURCE_COUNT = 5;
constexpr int32_t GAZE_TRACKING_STATUS_DISABLE = -1;
struct SceHmd2InitializeParam {
void* reserved0;
uint8_t reserved[8];
@@ -69,6 +83,58 @@ struct SceHmd2ReprojectionInitializeParam {
uint32_t reserved[5];
};
struct SceHmd2ReprojectionSource {
uint32_t layerType;
uint32_t data[73]; // VR3D: srcL texture, srcR texture, ...; VR2D: src texture, ...
const void* option;
};
struct SceHmd2ReprojectionBuffer {
const SceHmd2ReprojectionSource* src;
uint32_t srcCount;
};
struct SceHmd2ReprojectionRenderConfig {
uint32_t syncMode;
uint32_t phase;
uint32_t reserved[2];
};
struct SceHmd2ReprojectionParam {
const void* settings;
int32_t settingCount;
const void* option;
};
struct SceHmd2GazeGetResultParam {
uint32_t sizeOfThis;
int32_t handle;
int64_t flipArg;
};
struct SceHmd2GazeEyeResult {
int32_t isPupilDiameterValid;
float pupilDiameter;
int32_t isBlink;
};
struct SceHmd2GazeResult {
uint32_t sizeOfThis;
int32_t status;
uint64_t timestamp;
int32_t isGazeValid;
SceFVector3 gazeDirection;
SceHmd2GazeEyeResult left;
SceHmd2GazeEyeResult right;
};
struct SceHmd2GazeResultForFoveatedRendering {
uint32_t sizeOfThis;
int32_t status;
SceFVector3 gazeDirection;
uint32_t reserved;
};
static_assert(sizeof(SceHmd2InitializeParam) == 16);
static_assert(offsetof(SceHmd2InitializeParam, reserved) == 8);
static_assert(sizeof(SceHmd2DeviceInformation) == 28);
@@ -84,6 +150,19 @@ static_assert(offsetof(SceHmd2ReprojectionInitializeParam, type) == 32);
static_assert(offsetof(SceHmd2ReprojectionInitializeParam, pSeeThroughBuff) == 40);
static_assert(offsetof(SceHmd2ReprojectionInitializeParam, reprojectionTiming) == 48);
static_assert(offsetof(SceHmd2ReprojectionInitializeParam, reserved) == 52);
static_assert(sizeof(SceHmd2ReprojectionSource) == 304);
static_assert(offsetof(SceHmd2ReprojectionSource, option) == 296);
static_assert(sizeof(SceHmd2ReprojectionBuffer) == 16);
static_assert(sizeof(SceHmd2ReprojectionRenderConfig) == 16);
static_assert(sizeof(SceHmd2ReprojectionParam) == 24);
static_assert(offsetof(SceHmd2ReprojectionParam, option) == 16);
static_assert(sizeof(SceHmd2GazeGetResultParam) == 16);
static_assert(offsetof(SceHmd2GazeGetResultParam, flipArg) == 8);
static_assert(sizeof(SceHmd2GazeResult) == 56);
static_assert(offsetof(SceHmd2GazeResult, gazeDirection) == 20);
static_assert(offsetof(SceHmd2GazeResult, right) == 44);
static_assert(sizeof(SceHmd2GazeResultForFoveatedRendering) == 24);
static_assert(offsetof(SceHmd2GazeResultForFoveatedRendering, gazeDirection) == 8);
static std::atomic<bool> g_initialized = false;
static std::mutex g_reprojection_mutex;
@@ -127,7 +206,7 @@ static int32_t KYTY_SYSV_ABI Hmd2GetDeviceInformation(SceHmd2DeviceInformation*
if (Config::VrEnabled()) {
// Virtual headset defaults: READY, MOUNT, 63 mm lens separation.
info->hmuMount = 1;
info->lensSeparationDistance = 63;
info->lensSeparationDistance = LENS_SEPARATION_MM;
// Headset characteristics: 4000x2040 panel and 2 m virtual image distance.
info->deviceInfo.panelResolution = {PANEL_WIDTH, PANEL_HEIGHT};
info->virtualImageDistance = 2.0f;
@@ -149,9 +228,7 @@ static int32_t KYTY_SYSV_ABI Hmd2GetFieldOfViewWithoutHandle(SceHmd2FieldOfView*
if (fov == nullptr) {
return ERROR_PARAMETER_NULL;
}
// Virtual ER15 field of view: 55/42.5/51.2/51.2 degrees.
// These tangents describe that geometry, not bit-exact firmware output.
*fov = {1.4281480312f, 0.9163311720f, 1.2437491417f, 1.2437491417f};
*fov = {FOV_TAN_OUT, FOV_TAN_IN, FOV_TAN_TOP, FOV_TAN_BOTTOM};
LOGF("Hmd2: FOV tangents out=%f in=%f top=%f bottom=%f\n", fov->tanOut, fov->tanIn,
fov->tanTop, fov->tanBottom);
return OK;
@@ -212,6 +289,133 @@ static int32_t KYTY_SYSV_ABI Hmd2ReprojectionEnableVrMode(uint64_t output_mode)
return OK;
}
static int32_t KYTY_SYSV_ABI Hmd2Open(int32_t user_id, int32_t type, int32_t index,
const void* param) {
PRINT_NAME();
if (!g_initialized.load()) {
return ERROR_NOT_INITIALIZED;
}
if (user_id != Config::GetUserId() || type != 0 || index != 0 || param != nullptr) {
return ERROR_PARAMETER_INVALID;
}
return HMD_HANDLE;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionBeginFrame(int64_t flip_arg) {
PRINT_NAME();
LOGF("\t flip_arg = %" PRId64 "\n", flip_arg);
return OK;
}
// Microseconds of process time, the clock VrTracker2 predicts poses for. The headset shows a frame
// one refresh after it begins.
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionGetPredictedDisplayTime(int64_t flip_arg,
uint64_t* timestamp) {
PRINT_NAME();
if (timestamp == nullptr) {
return ERROR_PARAMETER_NULL;
}
const auto* state = GetReprojectionState();
const bool fast = state != nullptr && state->output_mode.load() == OUTPUT_MODE_119_88HZ;
*timestamp = LibKernel::KernelGetProcessTime() + (fast ? 8342 : 11122);
LOGF("\t flip_arg = %" PRId64 ", time = %" PRIu64 "\n", flip_arg, *timestamp);
return OK;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionSetRenderConfig(SceHmd2ReprojectionRenderConfig config,
const void* option) {
PRINT_NAME();
LOGF("\t sync_mode = %u, phase = %u\n", config.syncMode, config.phase);
if (GetReprojectionState() == nullptr) {
return ERROR_REPROJECTION_NOT_INITIALIZED;
}
return option != nullptr ? ERROR_PARAMETER_INVALID : OK;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionSetParam(int32_t index, int64_t flip_arg,
const SceHmd2ReprojectionParam* param,
const void* option) {
PRINT_NAME();
if (GetReprojectionState() == nullptr) {
return ERROR_REPROJECTION_NOT_INITIALIZED;
}
if (param == nullptr || param->settings == nullptr) {
return ERROR_PARAMETER_NULL;
}
LOGF("\t index = %d, flip_arg = %" PRId64 ", layers = %d\n", index, flip_arg,
param->settingCount);
return option != nullptr ? ERROR_PARAMETER_INVALID : OK;
}
// No eye tracker: report DISABLE, which a PS5 also returns when the user turned eye tracking off.
template <typename Result>
static int32_t GazeResult(const SceHmd2GazeGetResultParam* param, Result* result) {
if (param == nullptr || result == nullptr) {
return ERROR_PARAMETER_NULL;
}
if (param->sizeOfThis != sizeof(*param) || result->sizeOfThis != sizeof(*result)) {
return ERROR_PARAMETER_INVALID;
}
if (param->handle != HMD_HANDLE) {
return ERROR_HANDLE_INVALID;
}
*result = {};
result->sizeOfThis = sizeof(*result);
result->status = GAZE_TRACKING_STATUS_DISABLE;
result->gazeDirection = {0.0f, 0.0f,
-1.0f}; // The display center; +z points away from the lens.
return OK;
}
static int32_t KYTY_SYSV_ABI Hmd2GazeGetResult(const SceHmd2GazeGetResultParam* param,
SceHmd2GazeResult* result) {
PRINT_NAME();
const auto error = GazeResult(param, result);
if (error == OK) {
result->timestamp = LibKernel::KernelGetProcessTime();
}
return error;
}
static int32_t KYTY_SYSV_ABI Hmd2GazeGetResultForFoveatedRendering(
const SceHmd2GazeGetResultParam* param, SceHmd2GazeResultForFoveatedRendering* result) {
PRINT_NAME();
return GazeResult(param, result);
}
bool GetReprojectionSurface(const void* reprojection_buffer, ReprojectionSurface& out) {
const auto* buffer = static_cast<const SceHmd2ReprojectionBuffer*>(reprojection_buffer);
if (buffer == nullptr || buffer->src == nullptr || buffer->srcCount == 0 ||
buffer->srcCount > REPROJECTION_MAX_SOURCE_COUNT ||
(buffer->src->layerType != REPROJECTION_LAYER_TYPE_VR3D &&
buffer->src->layerType != REPROJECTION_LAYER_TYPE_VR2D)) {
return false;
}
Graphics::ShaderTextureResource texture;
std::memcpy(texture.fields, buffer->src->data, sizeof(texture.fields));
// Eye textures are RGBA ordered render targets; flip them through the matching SDR format.
const auto& policies = Graphics::VIDEO_OUT_FORMAT_POLICIES;
const auto policy = std::find_if(policies.begin(), policies.end(), [&](const auto& p) {
return p.info.guest_format == texture.Format() &&
(p.pixel_format & 0xff000000u) == 0x22000000u &&
!Graphics::IsHdrVideoOutPixelFormat(p.pixel_format);
});
if (texture.IsNull() || texture.TileMode() != Graphics::Prospero::TileMode::kRenderTarget ||
policy == policies.end()) {
LOGF("Hmd2: unsupported eye texture format=%u tile=%u\n",
static_cast<uint32_t>(texture.Format()), static_cast<uint32_t>(texture.TileMode()));
return false;
}
if (buffer->src->layerType == REPROJECTION_LAYER_TYPE_VR3D &&
std::memcmp(buffer->src->data, buffer->src->data + std::size(texture.fields),
sizeof(texture.fields)) != 0) {
Log::WriteToConsoleAndLog(
"Hmd2: separate eye textures are not supported; showing the left one\n");
}
out = {texture.Base40(), texture.Width5() + 1u, texture.Height5() + 1u, policy->pixel_format};
return true;
}
static int32_t KYTY_SYSV_ABI Hmd2ReprojectionSetTiming(int32_t timing) {
auto* state = GetReprojectionState();
if (state == nullptr) {
@@ -235,6 +439,13 @@ LIB_DEFINE(InitHmd2_1) {
LIB_FUNC("C0rPwER-yxg", Hmd2::Hmd2ReprojectionInitialize);
LIB_FUNC("VVvFh51o20s", Hmd2::Hmd2ReprojectionEnableVrMode);
LIB_FUNC("FkQX7rjFomk", Hmd2::Hmd2ReprojectionSetTiming);
LIB_FUNC("f3kPeoTZnIE", Hmd2::Hmd2Open);
LIB_FUNC("Ocf081WpBpA", Hmd2::Hmd2ReprojectionBeginFrame);
LIB_FUNC("SVEG+1D7qHA", Hmd2::Hmd2ReprojectionGetPredictedDisplayTime);
LIB_FUNC("hA9LshbSkzw", Hmd2::Hmd2ReprojectionSetRenderConfig);
LIB_FUNC("xMo9ENEu2E0", Hmd2::Hmd2ReprojectionSetParam);
LIB_FUNC("lAoFUedcfqA", Hmd2::Hmd2GazeGetResult);
LIB_FUNC("retc+-uRMhk", Hmd2::Hmd2GazeGetResultForFoveatedRendering);
}
} // namespace Libs
+2
View File
@@ -189,6 +189,8 @@ LIB_DEFINE(InitPad_1) {
LIB_FUNC("RR4novUEENY", Controller::PadSetLightBar);
LIB_FUNC("6ncge5+l5Qs", Controller::PadClose);
LIB_FUNC("n3kSX62fgNo", PadUnknownN3kSX62fgNo);
LIB_FUNC("iA-DdobUen8", Controller::PadVrControllerReadState);
LIB_FUNC("v8P+9PRqg10", Controller::PadVrControllerSetTriggerEffects);
}
namespace LibMouse {
+228
View File
@@ -0,0 +1,228 @@
#include "common/abi.h"
#include "common/logging/log.h"
#include "graphics/presentation/vrInput.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/hmd2.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace Libs {
LIB_VERSION("VrTracker2", 1, "VrTracker2", 1, 1);
namespace VrTracker2 {
constexpr int32_t ERROR_NOT_INIT = -1972109311; // 0x8a740001
constexpr int32_t ERROR_ALREADY_INITIALIZED = -1972109310; // 0x8a740002
constexpr int32_t ERROR_ARGUMENT_INVALID = -1972109306; // 0x8a740006
constexpr uint32_t MEMORY_SIZE = 32u << 20u;
constexpr uint32_t MEMORY_ALIGNMENT = 0x10000;
// Device types: the headset, then the right and the left controller.
constexpr int32_t DEVICE_TYPE_HMD = 0;
constexpr int32_t DEVICE_TYPE_VR_CONTROLLER_LEFT = 2;
constexpr uint32_t STATUS_TRACKING = 1;
constexpr uint32_t QUALITY_FULL = 9;
constexpr uint8_t MOTION_SENSOR_DATA_VALID = 3; // Velocity and acceleration.
struct SceFQuaternion {
float x;
float y;
float z;
float w;
};
struct SceVrTracker2QueryMemoryResult {
uint32_t sizeOfThis;
uint32_t memorySize;
uint32_t memoryAlignment;
uint32_t reserved[5];
};
struct SceVrTracker2InitializeParam {
uint32_t sizeOfThis;
uint32_t profile;
int32_t highThreadPriority;
int32_t lowThreadPriority;
uint64_t highThreadAffinity;
uint64_t lowThreadAffinity;
void* memoryPointer;
uint8_t options[32]; // Memory size and tracking options; nothing here depends on them.
};
struct SceVrTracker2GetResultParam {
uint32_t sizeOfThis;
int32_t handle;
uint32_t reserved00;
uint64_t predictionTime;
uint32_t reserved01;
int64_t flipArg;
uint32_t reserved02[2];
};
struct SceVrTracker2PoseData {
Hmd2::SceFVector3 position;
uint32_t reserved00;
SceFQuaternion orientation;
uint32_t reserved01[8];
};
struct SceVrTracker2ResultData {
int32_t handle;
uint32_t connected;
uint32_t playAreaUpdateCounter;
uint32_t playAreaMode;
uint64_t timestamp;
uint32_t status;
uint32_t positionQuality;
uint32_t orientationQuality;
Hmd2::SceFVector3 velocity;
Hmd2::SceFVector3 acceleration;
Hmd2::SceFVector3 angularVelocity;
uint32_t reserved01[3];
uint8_t motionSensorDataValidFlags;
uint8_t reserved010[3];
int64_t flipArg;
union {
struct {
SceVrTracker2PoseData devicePose;
SceVrTracker2PoseData leftEyePose;
SceVrTracker2PoseData rightEyePose;
SceVrTracker2PoseData headPose;
SceVrTracker2PoseData gazeOriginPose;
} hmdInfo;
struct {
SceVrTracker2PoseData devicePose;
uint64_t lastValidVisionTimestamp;
uint64_t lastValidSensorTimestamp;
uint32_t reserved[60];
} vrControllerInfo;
};
uint32_t reserved02;
};
static_assert(sizeof(SceVrTracker2QueryMemoryResult) == 0x20);
static_assert(sizeof(SceVrTracker2InitializeParam) == 0x48);
static_assert(offsetof(SceVrTracker2InitializeParam, memoryPointer) == 0x20);
static_assert(sizeof(SceVrTracker2GetResultParam) == 0x30);
static_assert(offsetof(SceVrTracker2GetResultParam, flipArg) == 0x20);
static_assert(sizeof(SceVrTracker2PoseData) == 0x40);
static_assert(sizeof(SceVrTracker2ResultData) == 0x1a8);
static_assert(offsetof(SceVrTracker2ResultData, status) == 0x18);
static_assert(offsetof(SceVrTracker2ResultData, motionSensorDataValidFlags) == 0x54);
static_assert(offsetof(SceVrTracker2ResultData, flipArg) == 0x58);
static_assert(offsetof(SceVrTracker2ResultData, hmdInfo.devicePose) == 0x60);
static_assert(offsetof(SceVrTracker2ResultData, hmdInfo.gazeOriginPose) == 0x160);
static_assert(offsetof(SceVrTracker2ResultData, vrControllerInfo.lastValidVisionTimestamp) == 0xa0);
static std::atomic<bool> g_initialized = false;
// Handle registered for each device type.
static std::atomic<int32_t> g_devices[DEVICE_TYPE_VR_CONTROLLER_LEFT + 1] = {-1, -1, -1};
// Writes a tracking-space pose moved along its own x axis by shift metres. w stays non-negative so
// that a rotation always has one representation, whatever sign the runtime reports.
static void SetPose(SceVrTracker2PoseData& out, const Vr::VrPose& pose, float shift = 0.0f) {
const auto [x, y, z, w] = pose.orientation;
const float s = w < 0.0f ? -1.0f : 1.0f;
out.position = {pose.position[0] + shift * (1.0f - 2.0f * (y * y + z * z)),
pose.position[1] + shift * 2.0f * (x * y + w * z),
pose.position[2] + shift * 2.0f * (x * z - w * y)};
out.orientation = {s * x, s * y, s * z, s * w};
}
static int32_t KYTY_SYSV_ABI QueryMemory(const void* param,
SceVrTracker2QueryMemoryResult* result) {
PRINT_NAME();
if (param == nullptr || result == nullptr) {
return ERROR_ARGUMENT_INVALID;
}
result->memorySize = MEMORY_SIZE;
result->memoryAlignment = MEMORY_ALIGNMENT;
return OK;
}
static int32_t KYTY_SYSV_ABI Initialize(const SceVrTracker2InitializeParam* param) {
PRINT_NAME();
if (param == nullptr || param->memoryPointer == nullptr) {
return ERROR_ARGUMENT_INVALID;
}
return g_initialized.exchange(true) ? ERROR_ALREADY_INITIALIZED : OK;
}
static int32_t KYTY_SYSV_ABI RegisterDevice(int32_t device_type, int32_t handle) {
PRINT_NAME();
LOGF("\t device_type = %d\n"
"\t handle = %d\n",
device_type, handle);
if (!g_initialized.load()) {
return ERROR_NOT_INIT;
}
if (device_type < DEVICE_TYPE_HMD || device_type > DEVICE_TYPE_VR_CONTROLLER_LEFT ||
handle < 0) {
return ERROR_ARGUMENT_INVALID;
}
g_devices[device_type].store(handle);
return OK;
}
// Without a headset runtime every device reports the identity pose at the tracking origin.
static int32_t KYTY_SYSV_ABI GetResult(const SceVrTracker2GetResultParam* param,
SceVrTracker2ResultData* result) {
PRINT_NAME();
if (param == nullptr || result == nullptr) {
return ERROR_ARGUMENT_INVALID;
}
if (!g_initialized.load()) {
return ERROR_NOT_INIT;
}
std::memset(result, 0, sizeof(*result));
result->handle = param->handle;
result->flipArg = param->flipArg;
result->timestamp =
param->predictionTime != 0 ? param->predictionTime : LibKernel::KernelGetProcessTime();
int32_t type = DEVICE_TYPE_HMD;
while (type <= DEVICE_TYPE_VR_CONTROLLER_LEFT && g_devices[type].load() != param->handle) {
type++;
}
if (param->handle < 0 || type > DEVICE_TYPE_VR_CONTROLLER_LEFT) {
return OK; // Not registered: not connected, tracking not started.
}
result->connected = 1;
result->status = STATUS_TRACKING;
result->positionQuality = QUALITY_FULL;
result->orientationQuality = QUALITY_FULL;
result->motionSensorDataValidFlags = MOTION_SENSOR_DATA_VALID;
if (type == DEVICE_TYPE_HMD) {
const auto pose = Vr::OpenXrGetHeadPose();
auto& hmd = result->hmdInfo;
SetPose(hmd.devicePose, pose);
SetPose(hmd.leftEyePose, pose, -Hmd2::EYE_OFFSET_M);
SetPose(hmd.rightEyePose, pose, Hmd2::EYE_OFFSET_M);
hmd.headPose = hmd.devicePose;
hmd.gazeOriginPose = hmd.devicePose;
return OK;
}
auto& controller = result->vrControllerInfo;
SetPose(controller.devicePose,
Vr::OpenXrGetController(type == DEVICE_TYPE_VR_CONTROLLER_LEFT ? 0 : 1).pose);
controller.lastValidVisionTimestamp = result->timestamp;
controller.lastValidSensorTimestamp = result->timestamp;
return OK;
}
} // namespace VrTracker2
LIB_DEFINE(InitVrTracker2_1) {
LIB_FUNC("TwqZnaIjWv4", VrTracker2::QueryMemory);
LIB_FUNC("6Jy73SRfG-o", VrTracker2::Initialize);
LIB_FUNC("Dog+g25QYjw", VrTracker2::RegisterDevice);
LIB_FUNC("J4Vh3VVX0iU", VrTracker2::GetResult);
}
} // namespace Libs
+2
View File
@@ -92,6 +92,7 @@ LIB_DEFINE(InitFont_1);
LIB_DEFINE(InitFontFt_1);
LIB_DEFINE(InitAgcDriver_1);
LIB_DEFINE(InitHmd2_1);
LIB_DEFINE(InitVrTracker2_1);
LIB_DEFINE(InitLibKernel_1);
LIB_DEFINE(InitNet_1);
LIB_DEFINE(InitPad_1);
@@ -131,6 +132,7 @@ void InitAll(Loader::SymbolDatabase* s) {
InitDeviceService_1(s);
InitUsbd_1(s);
InitHmd2_1(s);
InitVrTracker2_1(s);
InitLibKernel_1(s);
LibMouse::InitMouse_1(s);
LibKeyboard::InitKeyboard_1(s);
+158
View File
@@ -0,0 +1,158 @@
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "graphics/presentation/vrInput.h"
#include "kernel/pthread.h"
#include "libs/controller.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/padData.h"
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstring>
// Virtual headset only: the PS VR2 Sense controller ports. A headset runtime drives them when one
// is running; otherwise each hand reads its half of the first player's keyboard or gamepad.
namespace Libs::Controller {
LIB_NAME("Pad", "Pad");
constexpr int PAD_PORT_TYPE_VR_CONTROLLER_LEFT = 3;
// The buttons of each Sense controller. The left Create button is reserved for the system.
constexpr uint32_t HAND_BUTTONS[2] = {
PAD_BUTTON_L1 | PAD_BUTTON_L2 | PAD_BUTTON_L3 | PAD_BUTTON_TRIANGLE | PAD_BUTTON_SQUARE,
PAD_BUTTON_R1 | PAD_BUTTON_R2 | PAD_BUTTON_R3 | PAD_BUTTON_CIRCLE | PAD_BUTTON_CROSS |
PAD_BUTTON_OPTIONS};
struct PadVrControllerData {
uint32_t buttons;
uint8_t stick_x;
uint8_t stick_y;
uint8_t stick_padding[2];
uint8_t analog_l2;
uint8_t analog_r2;
uint8_t analog_padding[2];
uint32_t finger_touch;
uint8_t analog_finger_touch[4];
float acceleration[3];
float angular_velocity[3];
float orientation[4];
uint8_t reserve1[4];
bool connected;
uint8_t connected_count;
uint64_t timestamp;
uint8_t reserve2[24];
};
static_assert(sizeof(PadVrControllerData) == 104);
static_assert(offsetof(PadVrControllerData, finger_touch) == 12);
static_assert(offsetof(PadVrControllerData, acceleration) == 20);
static_assert(offsetof(PadVrControllerData, orientation) == 44);
static_assert(offsetof(PadVrControllerData, connected) == 64);
static_assert(offsetof(PadVrControllerData, timestamp) == 72);
static std::atomic<bool> g_open[2];
int PadVrPortHand(int type) {
const bool vr =
type == PAD_PORT_TYPE_VR_CONTROLLER_LEFT || type == PAD_PORT_TYPE_VR_CONTROLLER_LEFT + 1;
return vr && Config::VrEnabled() ? type - PAD_PORT_TYPE_VR_CONTROLLER_LEFT : -1;
}
int PadVrPort(int user_id, int hand, int index, bool open) {
if (!PadInitialized()) {
return PAD_ERROR_NOT_INITIALIZED;
}
const int handle = PAD_HANDLE_VR_LEFT + hand;
if (user_id != Config::GetUserId() || index != 0) {
return open ? PAD_ERROR_INVALID_ARG : PAD_ERROR_NO_HANDLE;
}
if (!open) {
return g_open[hand].load() ? handle : PAD_ERROR_NO_HANDLE;
}
return g_open[hand].exchange(true) ? PAD_ERROR_ALREADY_OPENED : handle;
}
int PadVrCheckHandle(int handle) {
const int hand = PadVrHand(handle);
return hand >= 0 && g_open[hand].load() ? OK : PAD_ERROR_INVALID_HANDLE;
}
int PadVrClose(int handle) {
const int hand = PadVrHand(handle);
return hand >= 0 && g_open[hand].exchange(false) ? OK : PAD_ERROR_INVALID_HANDLE;
}
static uint8_t ToByte(float value) {
return static_cast<uint8_t>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
}
int KYTY_SYSV_ABI PadVrControllerReadState(int handle, PadVrControllerData* data) {
PRINT_NAME();
const int hand = PadVrHand(handle);
if (hand < 0) {
return PAD_ERROR_INVALID_HANDLE;
}
if (data == nullptr) {
return PAD_ERROR_INVALID_ARG;
}
PadData pad {};
if (const auto error = PadReadState(handle, &pad); error != OK) {
return error;
}
*data = {};
auto& trigger = hand == 0 ? data->analog_l2 : data->analog_r2;
data->buttons = pad.buttons & HAND_BUTTONS[hand];
data->stick_x = hand == 0 ? pad.left_stick_x : pad.right_stick_x;
data->stick_y = hand == 0 ? pad.left_stick_y : pad.right_stick_y;
trigger = hand == 0 ? pad.analog_buttons_l2 : pad.analog_buttons_r2;
std::memcpy(data->acceleration, &pad.acceleration_x, sizeof(data->acceleration));
std::memcpy(data->angular_velocity, &pad.angular_velocity_x, sizeof(data->angular_velocity));
std::memcpy(data->orientation, &pad.orientation_x, sizeof(data->orientation));
data->connected = pad.connected;
data->connected_count = pad.connected_count;
data->timestamp = pad.timestamp;
if (const auto xr = Vr::OpenXrGetController(hand); xr.connected) {
// Touch controllers have no button left for the right Options, so the left Menu stands in.
const bool options = hand == 1 && Vr::OpenXrGetController(0).menu;
// Each mapping names both hands; HAND_BUTTONS keeps this hand's half.
const uint32_t buttons = (xr.trigger > 0.5f ? PAD_BUTTON_L2 | PAD_BUTTON_R2 : 0u) |
(xr.squeeze > 0.5f ? PAD_BUTTON_L1 | PAD_BUTTON_R1 : 0u) |
(xr.stick_click ? PAD_BUTTON_L3 | PAD_BUTTON_R3 : 0u) |
(xr.primary ? PAD_BUTTON_SQUARE | PAD_BUTTON_CROSS : 0u) |
(xr.secondary ? PAD_BUTTON_TRIANGLE | PAD_BUTTON_CIRCLE : 0u) |
(options ? PAD_BUTTON_OPTIONS : 0u);
data->buttons = buttons & HAND_BUTTONS[hand];
data->stick_x = ToByte(xr.stick_x * 0.5f + 0.5f);
data->stick_y = ToByte(0.5f - xr.stick_y * 0.5f);
trigger = ToByte(xr.trigger);
if (xr.pose.valid) {
std::memcpy(data->orientation, xr.pose.orientation, sizeof(data->orientation));
}
data->connected = true;
data->connected_count = std::max<uint8_t>(data->connected_count, 1);
data->timestamp = LibKernel::KernelGetProcessTime();
}
return OK;
}
int KYTY_SYSV_ABI PadVrControllerSetTriggerEffects(int left_handle, int right_handle,
const PadTriggerEffectParam* param) {
PRINT_NAME();
LOGF("\t left_handle = %d\n"
"\t right_handle = %d\n",
left_handle, right_handle);
if (PadVrHand(left_handle) != 0 || PadVrCheckHandle(left_handle) != OK ||
PadVrHand(right_handle) != 1 || PadVrCheckHandle(right_handle) != OK) {
return PAD_ERROR_INVALID_HANDLE;
}
return param != nullptr ? OK : PAD_ERROR_INVALID_ARG;
}
} // namespace Libs::Controller