diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index f3cdf3a98..71a34661c 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -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) diff --git a/CMakeLists.txt b/CMakeLists.txt index 444913f2e..4b24b1c0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/graphics/presentation/openxrHeadset.cpp b/src/graphics/presentation/openxrHeadset.cpp new file mode 100644 index 000000000..bbfb94466 --- /dev/null +++ b/src/graphics/presentation/openxrHeadset.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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//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 swapchain_images; + vk::Extent2D eye {}; + std::vector 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 formats(count); + xrEnumerateSwapchainFormats(g.session, count, &count, formats.data()); + constexpr std::pair 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(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(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(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 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(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(event).state; + LOGF("OpenXR: session state %d\n", static_cast(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(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(g.eye.width), static_cast(g.eye.height)}}; + view.subImage.imageArrayIndex = static_cast(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(&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(&get_requirements)); + xrGetInstanceProcAddr(g.instance, "xrGetVulkanInstanceExtensionsKHR", + reinterpret_cast(&get_extensions[0])); + // Both queries share one signature. + xrGetInstanceProcAddr(g.instance, "xrGetVulkanDeviceExtensionsKHR", + reinterpret_cast(&get_extensions[1])); + xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsDeviceKHR", + reinterpret_cast(&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& out, + const std::vector& 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(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(graphics.instance); + binding.physicalDevice = static_cast(graphics.physical_device); + binding.device = static_cast(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(source.extent.width), + static_cast(source.extent.height), 1}; + blit.dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}; + blit.dstOffsets[1] = vk::Offset3D {static_cast(slot.image.extent.width), + static_cast(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 diff --git a/src/graphics/presentation/openxrHeadset.h b/src/graphics/presentation/openxrHeadset.h new file mode 100644 index 000000000..6111ea3be --- /dev/null +++ b/src/graphics/presentation/openxrHeadset.h @@ -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 + +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& out, + const std::vector& 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_ diff --git a/src/graphics/presentation/presenter.h b/src/graphics/presentation/presenter.h index a0821a8aa..f435ec281 100644 --- a/src/graphics/presentation/presenter.h +++ b/src/graphics/presentation/presenter.h @@ -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(); diff --git a/src/graphics/presentation/videoOut.cpp b/src/graphics/presentation/videoOut.cpp index f93caa31c..247df7a48 100644 --- a/src/graphics/presentation/videoOut.cpp +++ b/src/graphics/presentation/videoOut.cpp @@ -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 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 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 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(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; } diff --git a/src/graphics/presentation/vrInput.h b/src/graphics/presentation/vrInput.h new file mode 100644 index 000000000..f1b13f4dd --- /dev/null +++ b/src/graphics/presentation/vrInput.h @@ -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_ diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index 853bd8db4..22b56b261 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -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(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; } diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 81ddaf50f..c1a2b98ff 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -1,6 +1,3 @@ -#include -#include - #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 +#include #include #include #include @@ -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(graphic_ctx); LibKernel::Memory::InstallGpuResources(render_context.get()); presenter = std::make_unique(*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); diff --git a/src/libs/controller.cpp b/src/libs/controller.cpp index ba4c32ce7..e65b25c2a 100644 --- a/src/libs/controller.cpp +++ b/src/libs/controller.cpp @@ -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(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; diff --git a/src/libs/controller.h b/src/libs/controller.h index bdae417ac..d3e83b51f 100644 --- a/src/libs/controller.h +++ b/src/libs/controller.h @@ -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); diff --git a/src/libs/hmd2.h b/src/libs/hmd2.h index ed24869ae..ac559c29c 100644 --- a/src/libs/hmd2.h +++ b/src/libs/hmd2.h @@ -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 output_mode {0}; std::atomic 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_ diff --git a/src/libs/libHmd2.cpp b/src/libs/libHmd2.cpp index f01c2ee9e..186abc790 100644 --- a/src/libs/libHmd2.cpp +++ b/src/libs/libHmd2.cpp @@ -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 #include #include #include +#include #include #include @@ -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 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 +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(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(texture.Format()), static_cast(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 diff --git a/src/libs/libPad.cpp b/src/libs/libPad.cpp index 30b82559c..e817d6473 100644 --- a/src/libs/libPad.cpp +++ b/src/libs/libPad.cpp @@ -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 { diff --git a/src/libs/libVrTracker2.cpp b/src/libs/libVrTracker2.cpp new file mode 100644 index 000000000..a5f40681b --- /dev/null +++ b/src/libs/libVrTracker2.cpp @@ -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 +#include +#include +#include + +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 g_initialized = false; +// Handle registered for each device type. +static std::atomic 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 diff --git a/src/libs/libs.cpp b/src/libs/libs.cpp index 725032445..4ab0af0f7 100644 --- a/src/libs/libs.cpp +++ b/src/libs/libs.cpp @@ -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); diff --git a/src/libs/padVr.cpp b/src/libs/padVr.cpp new file mode 100644 index 000000000..a093a03bd --- /dev/null +++ b/src/libs/padVr.cpp @@ -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 +#include +#include +#include + +// 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 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(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(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