libretro: add VR mode

This commit is contained in:
Craig Carnell committed 2026-10-16 14:27:05 +01:00
1 parent d277222a81
commit be7f2f5973
19 files changed
+685 -39

No files matched your search

+5
View File
@@ -32,6 +32,7 @@
#include "DolphinLibretro/Common/Globals.h"
#include "DolphinLibretro/Common/Options.h"
#include "DolphinLibretro/Video.h"
#include "DolphinLibretro/VR.h"
#include "DolphinLibretro/VideoContexts/ContextStatus.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPClient.h"
@@ -612,6 +613,9 @@ bool retro_load_game(const struct retro_game_info* game)
Config::SetBase(Config::GFX_PERF_QUERIES_ENABLE,
Libretro::GetOption<bool>(gfx_gamespecific::GFX_PERF_QUERIES_ENABLE, /*def=*/false));
if (Libretro::VR::IsEnabled())
Libretro::VR::ApplyConfig();
#ifdef CIFACE_USE_DUALSHOCKUDPCLIENT
// wiimote - alternative input server
{
@@ -839,6 +843,7 @@ void retro_unload_game(void)
// Rest of shutdown
g_context_status.MarkUnitialized();
Libretro::Input::Shutdown();
Libretro::VR::Shutdown();
Libretro::Log::Shutdown();
UICommon::ShutdownControllers();
UICommon::Shutdown();
@@ -7,6 +7,7 @@ add_library(dolphin_libretro SHARED
Log.cpp
Main.cpp
Video.cpp
VR.cpp
)
if(ENABLE_VULKAN)
@@ -1070,6 +1070,21 @@ static struct retro_core_option_v2_definition option_defs[] = {
},
"disabled"
},
{
Libretro::Options::gfx_settings::VR_MODE,
"Graphics > Settings > VR Mode",
"VR Mode",
"'Stereoscopic' shows the game on a virtual screen in 3D. 'Immersive' also turns the game camera with your head. Expect odd things in immersive mode. Requires a frontend with VR support.",
nullptr,
CATEGORY_GFX_SETTINGS,
{
{ "disabled", nullptr },
{ "stereo", "Stereoscopic" },
{ "immersive", "Immersive (Free Look)" },
{ nullptr, nullptr }
},
"disabled"
},
// ========== Graphics.Enhancements ==========
{
@@ -264,6 +264,7 @@ namespace gfx_settings {
constexpr const char FAST_DEPTH_CALCULATION[] = "dolphin_fast_depth_calculation";
constexpr const char DISABLE_FOG[] = "dolphin_disable_fog";
constexpr const char MODS_ENABLE[] = "dolphin_mods_enabled";
constexpr const char VR_MODE[] = "dolphin_vr_mode";
} // namespace gfx_settings
// ======================================================
+72 -23
View File
@@ -34,6 +34,7 @@
#include "DolphinLibretro/Input.h"
#include "DolphinLibretro/Common/Globals.h"
#include "DolphinLibretro/Common/Options.h"
#include "DolphinLibretro/VR.h"
#ifdef CIFACE_USE_DUALSHOCKUDPCLIENT
#include "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPClient.h"
#endif
@@ -115,6 +116,9 @@ double g_accel_pos[NUM_CONTROLLERS_FOR_SENSORS][3] = {}; // x, y, z
double g_accel_neg[NUM_CONTROLLERS_FOR_SENSORS][3] = {}; // x, y, z
double g_gyro_pos[NUM_CONTROLLERS_FOR_SENSORS][3] = {}; // x, y, z
double g_gyro_neg[NUM_CONTROLLERS_FOR_SENSORS][3] = {}; // x, y, z
double g_point[NUM_CONTROLLERS_FOR_SENSORS][4] = {}; // right, left, down, up
// Player 1's Wiimote is driven by the right VR controller (VR Mode on at boot).
static bool s_vr_motion = false;
static struct retro_input_descriptor descGC[] = {
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left"},
@@ -660,23 +664,34 @@ void InitSensors()
port_max = (Core::System::GetInstance().IsWii() &&
Libretro::Options::GetCached<int>(Libretro::Options::sysconf::ALT_GC_PORTS_ON_WII)) ? 8 : 4;
// In VR the right controller moves player 1's Wiimote.
s_vr_motion = Libretro::VR::IsEnabled();
for (int i = 0; i < port_max; i++)
{
if (sensor_interface.set_sensor_state)
if (s_vr_motion && i == 0)
{
sensor_enabled[i][SENSOR_ACCELEROMETER] = true;
sensor_enabled[i][SENSOR_GYRO] = true;
// At rest until the controller is seen: gravity reads as up.
g_accel_pos[i][2] = 9.80665;
INFO_LOG_FMT(BOOT, "Sensor interface: Port: {} driven by the right VR controller", i);
}
else if (sensor_interface.set_sensor_state)
{
sensor_enabled[i][SENSOR_ACCELEROMETER] = sensor_interface.set_sensor_state(i, RETRO_SENSOR_ACCELEROMETER_ENABLE, 60);
sensor_enabled[i][SENSOR_GYRO] = sensor_interface.set_sensor_state(i, RETRO_SENSOR_GYROSCOPE_ENABLE, 60);
if (sensor_enabled[i][SENSOR_ACCELEROMETER] || sensor_enabled[i][SENSOR_GYRO])
{
auto sensor = std::make_shared<SensorDevice>(i);
sensor->RegisterAll();
g_controller_interface.AddDevice(sensor);
}
INFO_LOG_FMT(BOOT, "Sensor interface: Port: {} ACCELEROMETER: {} GYROSCOPE: {}", i,
sensor_enabled[i][SENSOR_ACCELEROMETER], sensor_enabled[i][SENSOR_GYRO]);
}
if (sensor_enabled[i][SENSOR_ACCELEROMETER] || sensor_enabled[i][SENSOR_GYRO])
{
auto sensor = std::make_shared<SensorDevice>(i);
sensor->RegisterAll();
g_controller_interface.AddDevice(sensor);
}
}
}
@@ -702,15 +717,20 @@ void Shutdown()
Pad::ResetRumble(i);
// Was crossed over and hardcoded to port 0, leaving sensors running.
if (sensor_enabled[i][SENSOR_ACCELEROMETER])
sensor_interface.set_sensor_state(i, RETRO_SENSOR_ACCELEROMETER_DISABLE, 0);
// A port driven by a VR controller never turned the frontend's sensors on.
if (!UsesVRMotion(i))
{
if (sensor_enabled[i][SENSOR_ACCELEROMETER])
sensor_interface.set_sensor_state(i, RETRO_SENSOR_ACCELEROMETER_DISABLE, 0);
if (sensor_enabled[i][SENSOR_GYRO])
sensor_interface.set_sensor_state(i, RETRO_SENSOR_GYROSCOPE_DISABLE, 0);
if (sensor_enabled[i][SENSOR_GYRO])
sensor_interface.set_sensor_state(i, RETRO_SENSOR_GYROSCOPE_DISABLE, 0);
}
sensor_enabled[i][SENSOR_ACCELEROMETER] = false;
sensor_enabled[i][SENSOR_GYRO] = false;
}
s_vr_motion = false;
Keyboard::Shutdown();
Pad::Shutdown();
@@ -719,9 +739,30 @@ void Shutdown()
rumble.set_rumble_state = nullptr;
}
bool UsesVRMotion(unsigned port)
{
return s_vr_motion && port == 0;
}
void SetVRMotion(unsigned port, const float accel[3], const float gyro[3], const float point[2])
{
for (int i = 0; i < 3; i++)
{
g_accel_pos[port][i] = std::max(0.0f, accel[i]);
g_accel_neg[port][i] = std::max(0.0f, -accel[i]);
g_gyro_pos[port][i] = std::max(0.0f, gyro[i]);
g_gyro_neg[port][i] = std::max(0.0f, -gyro[i]);
}
g_point[port][0] = std::max(0.0f, point[0]);
g_point[port][1] = std::max(0.0f, -point[0]);
g_point[port][2] = std::max(0.0f, -point[1]);
g_point[port][3] = std::max(0.0f, point[1]);
}
void UpdateAccelerometer(unsigned port)
{
if (!sensor_enabled[port][SENSOR_ACCELEROMETER] || !sensor_interface.get_sensor_input)
if (!sensor_enabled[port][SENSOR_ACCELEROMETER] || !sensor_interface.get_sensor_input ||
UsesVRMotion(port))
return;
static const float G = 9.80665f;
@@ -753,7 +794,8 @@ void UpdateAccelerometer(unsigned port)
void UpdateGyro(unsigned port)
{
if (!sensor_enabled[port][SENSOR_GYRO] || !sensor_interface.get_sensor_input)
if (!sensor_enabled[port][SENSOR_GYRO] || !sensor_interface.get_sensor_input ||
UsesVRMotion(port))
return;
// rad/s about the remote's own axes: +X left, +Y back, +Z up.
@@ -951,19 +993,26 @@ void UpdateWiimoteMappings(const WiimoteUpdateFlags& f, unsigned port, unsigned
std::string devAnalog = Libretro::Input::GetQualifiedName(port, RETRO_DEVICE_ANALOG);
std::string devPointer = Libretro::Input::GetQualifiedName(port, RETRO_DEVICE_POINTER);
// In VR the right controller aims the cursor too, alongside the thumbstick or mouse.
const bool vr_aim = Libretro::Input::UsesVRMotion(port) && irMode != 0;
const std::string devSensor = GetQualifiedNameSensor(port);
auto aim = [&](const std::string& expr, const char* direction) {
return vr_aim ? "(" + expr + ") | `" + devSensor + ":" + direction + "`" : expr;
};
if (irMode == 0 || irMode == 1)
{
// Set right stick to control the IR
std::string irMod = Libretro::Options::GetCached<std::string>(Libretro::Options::wiimote::IR_MODIFIER);
wmIR->SetControlExpression(0, wrap(irMod, "`" + devAnalog + ":Y1-`")); // Up
wmIR->SetControlExpression(1, wrap(irMod, "`" + devAnalog + ":Y1+`")); // Down
wmIR->SetControlExpression(2, wrap(irMod, "`" + devAnalog + ":X1-`")); // Left
wmIR->SetControlExpression(3, wrap(irMod, "`" + devAnalog + ":X1+`")); // Right
wmIR->SetControlExpression(0, aim(wrap(irMod, "`" + devAnalog + ":Y1-`"), "PointUp")); // Up
wmIR->SetControlExpression(1, aim(wrap(irMod, "`" + devAnalog + ":Y1+`"), "PointDown")); // Down
wmIR->SetControlExpression(2, aim(wrap(irMod, "`" + devAnalog + ":X1-`"), "PointLeft")); // Left
wmIR->SetControlExpression(3, aim(wrap(irMod, "`" + devAnalog + ":X1+`"), "PointRight")); // Right
static_cast<ControllerEmu::NumericSetting<bool>*>(wmIR->numeric_settings[4].get())
->SetValue(irMode == 0); // Relative input
static_cast<ControllerEmu::NumericSetting<bool>*>(wmIR->numeric_settings[5].get())
->SetValue(true); // Auto hide
->SetValue(!vr_aim); // Auto hide
// Swing‑related updates
ControllerEmu::ControlGroup* wmSwing = wm->GetWiimoteGroup(WiimoteEmu::WiimoteGroup::Swing);
@@ -993,10 +1042,10 @@ void UpdateWiimoteMappings(const WiimoteUpdateFlags& f, unsigned port, unsigned
else
{
// Mouse controls IR
wmIR->SetControlExpression(0, "`" + devPointer + ":Y0-`"); // Up
wmIR->SetControlExpression(1, "`" + devPointer + ":Y0+`"); // Down
wmIR->SetControlExpression(2, "`" + devPointer + ":X0-`"); // Left
wmIR->SetControlExpression(3, "`" + devPointer + ":X0+`"); // Right
wmIR->SetControlExpression(0, aim("`" + devPointer + ":Y0-`", "PointUp")); // Up
wmIR->SetControlExpression(1, aim("`" + devPointer + ":Y0+`", "PointDown")); // Down
wmIR->SetControlExpression(2, aim("`" + devPointer + ":X0-`", "PointLeft")); // Left
wmIR->SetControlExpression(3, aim("`" + devPointer + ":X0+`", "PointRight")); // Right
static_cast<ControllerEmu::NumericSetting<bool>*>(wmIR->numeric_settings[4].get())
->SetValue(false); // Relative input
static_cast<ControllerEmu::NumericSetting<bool>*>(wmIR->numeric_settings[5].get())
+11
View File
@@ -42,12 +42,19 @@ extern double g_accel_pos[NUM_CONTROLLERS_FOR_SENSORS][3];
extern double g_accel_neg[NUM_CONTROLLERS_FOR_SENSORS][3];
extern double g_gyro_pos[NUM_CONTROLLERS_FOR_SENSORS][3];
extern double g_gyro_neg[NUM_CONTROLLERS_FOR_SENSORS][3];
// Where the VR controller points, as one-sided -1..1 pairs: right, left, down, up.
extern double g_point[NUM_CONTROLLERS_FOR_SENSORS][4];
static retro_sensor_interface sensor_interface = {0};
void Init(const WindowSystemInfo& wsi);
void InitStage2();
void InitSensors();
// VR: player 1's Wiimote takes its motion and pointing from the right VR controller.
bool UsesVRMotion(unsigned port);
// Accelerometer in m/s^2 and gyroscope in rad/s about the remote's axes (+X left, +Y back,
// +Z up), and where it points (-1..1, +X right, +Y up).
void SetVRMotion(unsigned port, const float accel[3], const float gyro[3], const float point[2]);
void UpdateAccelerometer(unsigned port);
void UpdateGyro(unsigned port);
void Update();
@@ -99,6 +106,10 @@ public:
AddInput(new ScalarInput("AccelY-", &Libretro::Input::g_accel_neg[m_port][1]));
AddInput(new ScalarInput("AccelZ+", &Libretro::Input::g_accel_pos[m_port][2]));
AddInput(new ScalarInput("AccelZ-", &Libretro::Input::g_accel_neg[m_port][2]));
AddInput(new ScalarInput("PointRight", &Libretro::Input::g_point[m_port][0]));
AddInput(new ScalarInput("PointLeft", &Libretro::Input::g_point[m_port][1]));
AddInput(new ScalarInput("PointDown", &Libretro::Input::g_point[m_port][2]));
AddInput(new ScalarInput("PointUp", &Libretro::Input::g_point[m_port][3]));
}
private:
+35 -9
View File
@@ -39,6 +39,7 @@
#include "DolphinLibretro/Input.h"
#include "DolphinLibretro/Common/Options.h"
#include "DolphinLibretro/Video.h"
#include "DolphinLibretro/VR.h"
#include "VideoBackends/OGL/OGLTexture.h"
#include "VideoBackends/OGL/OGLGfx.h"
#include "VideoCommon/AsyncRequests.h"
@@ -74,6 +75,8 @@ namespace Libretro
{
extern retro_environment_t environ_cb;
static bool widescreen;
// Whether the geometry last reported to the frontend was sized for VR
static bool vr_frame;
double g_core_refresh_rate{0};
// Reset per game (see retro_run): the first refresh-rate settle after boot must not
// trigger a mode-family reinit, only a real in-game 50<->60 switch should.
@@ -183,10 +186,10 @@ void retro_get_system_av_info(retro_system_av_info* info)
// Fixed max (>= any runtime height) so base can grow via SET_GEOMETRY alone.
int max_height = base_height > 576 ? base_height : 576;
info->geometry.base_width = EFB_WIDTH * efbScale;
info->geometry.base_width = Libretro::Video::GetAdjustedBaseWidth() * efbScale;
info->geometry.base_height = base_height * efbScale;
info->geometry.max_width = EFB_WIDTH * efbScale;
info->geometry.max_width = Libretro::Video::GetAdjustedBaseWidth() * efbScale;
info->geometry.max_height = max_height * efbScale;
if (g_widescreen)
@@ -196,6 +199,11 @@ void retro_get_system_av_info(retro_system_av_info* info)
info->geometry.aspect_ratio = Libretro::widescreen ? 16.0 / 9.0 : 4.0 / 3.0;
// The packed frame holds both eyes side by side
Libretro::vr_frame = Libretro::VR::IsEnabled();
if (Libretro::vr_frame)
info->geometry.aspect_ratio *= 2.0;
Core::System& system = Core::System::GetInstance();
double fps = system.GetVideoInterface().GetTargetRefreshRate();
if (fps <= 0.0)
@@ -324,7 +332,17 @@ void retro_run(void)
->SetSystemFrameBuffer((GLuint)Libretro::Video::hw_render.get_current_framebuffer());
}
if (Libretro::Options::IsUpdated(Libretro::Options::gfx_settings::EFB_SCALE))
// Turning VR on or off doubles or halves the frame width, so resize it as for EFB scale.
bool vr_resized = false;
if (Libretro::Options::IsUpdated(Libretro::Options::gfx_settings::VR_MODE))
{
const bool vr_enabled = Libretro::VR::IsEnabled();
if (vr_enabled || Libretro::vr_frame)
Libretro::VR::ApplyConfig();
vr_resized = vr_enabled != Libretro::vr_frame;
}
if (Libretro::Options::IsUpdated(Libretro::Options::gfx_settings::EFB_SCALE) || vr_resized)
{
g_Config.iEFBScale = Libretro::Options::GetCached<int>(
Libretro::Options::gfx_settings::EFB_SCALE);
@@ -410,13 +428,19 @@ void retro_run(void)
Libretro::Input::ResetControllers(flags);
if (Libretro::Options::IsUpdated(Libretro::Options::gfx_settings::ASPECT_RATIO))
Config::SetCurrent(
Config::GFX_ASPECT_RATIO, static_cast<AspectMode>(
Libretro::GetOption<int>(
Libretro::Options::gfx_settings::ASPECT_RATIO, static_cast<int>(AspectMode::Stretch)
{
// VR sets the aspect ratio together with its stereoscopy settings
if (Libretro::VR::IsEnabled())
Libretro::VR::ApplyConfig();
else
Config::SetCurrent(
Config::GFX_ASPECT_RATIO, static_cast<AspectMode>(
Libretro::GetOption<int>(
Libretro::Options::gfx_settings::ASPECT_RATIO, static_cast<int>(AspectMode::Stretch)
)
)
)
);
);
}
if (Libretro::Options::IsUpdated(Libretro::Options::gfx_hacks::EFB_TO_TEXTURE))
g_Config.bSkipEFBCopyToRam = Libretro::Options::GetCached<bool>(Libretro::Options::gfx_hacks::EFB_TO_TEXTURE, true);
@@ -485,6 +509,8 @@ void retro_run(void)
if (Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_ENABLED))
Libretro::Input::BluetoothPassthroughBind();
Libretro::VR::Update(Libretro::widescreen ? 16.0f / 9.0f : 4.0f / 3.0f);
RETRO_PERFORMANCE_INIT(dolphin_main_func);
RETRO_PERFORMANCE_START(dolphin_main_func);
+322
View File
@@ -0,0 +1,322 @@
#include "DolphinLibretro/VR.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <libretro.h>
#include "Common/Logging/Log.h"
#include "Common/Matrix.h"
#include "Core/Config/GraphicsSettings.h"
#include "DolphinLibretro/Common/Globals.h"
#include "DolphinLibretro/Common/Options.h"
#include "DolphinLibretro/Input.h"
#include "VideoCommon/Present.h"
#include "VideoCommon/VertexShaderManager.h"
#include "VideoCommon/VideoConfig.h"
namespace Libretro
{
namespace VR
{
static bool s_views_sent = false;
// heading that counts as straight ahead for the game camera
static bool s_have_forward = false;
static Common::Quaternion s_forward = Common::Quaternion::Identity();
// The frontend showed both eyes this session, then stopped):
// stereoscopy is off until it shows both again, so the GPU doesn't draw a second eye.
static bool s_frontend_stereo_seen = false;
static bool s_frontend_flat = false;
// The right controller as player 1's Wiimote: its last velocity, for acceleration, and the
// heading the cursor is measured from.
static bool s_have_controller_velocity = false;
static Common::Vec3 s_controller_velocity;
static std::chrono::steady_clock::time_point s_controller_time;
static bool s_have_point_forward = false;
static Common::Quaternion s_point_forward = Common::Quaternion::Identity();
static Common::Quaternion ToQuaternion(const float* xyzw)
{
return Common::Quaternion(xyzw[3], xyzw[0], xyzw[1], xyzw[2]).Normalized();
}
// A rotation about +Y by the heading of q's forward (-Z) axis, leaving out pitch and roll.
static Common::Quaternion Heading(const Common::Quaternion& q)
{
const Common::Vec4& v = q.data; // x, y, z, w
const float yaw =
std::atan2(2.0f * (v.x * v.z + v.w * v.y), 1.0f - 2.0f * (v.x * v.x + v.y * v.y));
return Common::Quaternion(std::cos(yaw * 0.5f), 0.0f, std::sin(yaw * 0.5f), 0.0f);
}
Mode GetMode()
{
const std::string mode = Options::GetCached<std::string>(Options::gfx_settings::VR_MODE);
if (mode == "stereo")
return Mode::Stereo;
if (mode == "immersive")
return Mode::Immersive;
return Mode::Off;
}
bool IsEnabled()
{
return GetMode() != Mode::Off;
}
void ApplyConfig()
{
const Mode mode = GetMode();
const bool enabled = mode != Mode::Off;
// The headset's field of view must cover each eye's whole half of the frame.
const AspectMode aspect = mode == Mode::Immersive ?
AspectMode::Stretch :
static_cast<AspectMode>(Libretro::GetOption<int>(
Options::gfx_settings::ASPECT_RATIO, static_cast<int>(AspectMode::Stretch)));
Config::SetCurrent(Config::GFX_ASPECT_RATIO, aspect);
Config::SetCurrent(Config::GFX_STEREO_MODE,
enabled && !s_frontend_flat ? StereoMode::SideBySide : StereoMode::Off);
Config::SetCurrent(Config::GFX_STEREO_PER_EYE_RESOLUTION_FULL,
enabled && aspect != AspectMode::Stretch);
}
static void DisableCamera()
{
// Take a new forward heading when the camera is next used
s_have_forward = false;
if (g_libretro_vr_camera.enabled)
{
g_libretro_vr_camera.enabled = false;
g_libretro_vr_camera.dirty = true;
}
}
static void ClearViews()
{
if (!s_views_sent)
return;
retro_video_views views{};
environ_cb(RETRO_ENVIRONMENT_SET_VIDEO_VIEWS, &views);
s_views_sent = false;
}
static bool UpdateCamera(const retro_vr_frame_state& state)
{
const float* left = state.eyes[RETRO_VR_EYE_LEFT].fov_tan;
const float* right = state.eyes[RETRO_VR_EYE_RIGHT].fov_tan;
for (int i = 0; i < 4; ++i)
{
if (!(left[i] > 0.0f) || !(right[i] > 0.0f))
return false;
}
// fov_tan is left, right, up, down. Dolphin's projection maps view-space x / -z in [-l, r] to
// [-1, 1] with scale 2 / (l + r) and offset (r - l) / (l + r); likewise y with up and down.
const auto offset = [](float neg, float pos) { return (pos - neg) / (neg + pos); };
const float left_offset_x = offset(left[0], left[1]);
const float right_offset_x = offset(right[0], right[1]);
LibretroVRCamera& camera = g_libretro_vr_camera;
camera.scale_x = 4.0f / (left[0] + left[1] + right[0] + right[1]);
camera.offset_x = (left_offset_x + right_offset_x) * 0.5f;
camera.eye_offset_x = (right_offset_x - left_offset_x) * 0.5f;
camera.scale_y = 4.0f / (left[2] + left[3] + right[2] + right[3]);
camera.offset_y = (offset(left[3], left[2]) + offset(right[3], right[2])) * 0.5f;
const Common::Quaternion head = ToQuaternion(state.eyes[RETRO_VR_EYE_LEFT].orientation);
if (!s_have_forward || (state.flags & RETRO_VR_FRAME_RECENTERED))
{
s_forward = Heading(head);
s_have_forward = true;
}
const Common::Quaternion relative = s_forward.Conjugate() * head;
camera.view = Common::Matrix44::FromQuaternion(relative.Conjugate());
camera.enabled = true;
camera.dirty = true;
return true;
}
static void UpdateVRMotion()
{
if (!Input::UsesVRMotion(0))
return;
constexpr float G = 9.80665f;
float accel[3] = {0.0f, 0.0f, G}; // at rest, level
float gyro[3] = {};
float point[2] = {};
retro_vr_controller_poses poses{};
const retro_vr_hand_state& hand = poses.hands[RETRO_VR_HAND_RIGHT];
const retro_vr_pose& aim = hand.aim;
if (!environ_cb(RETRO_ENVIRONMENT_GET_VR_CONTROLLER_POSES, &poses) ||
!(hand.flags & RETRO_VR_HAND_ACTIVE) || !(aim.flags & RETRO_VR_POSE_ORIENTATION_VALID))
{
s_have_controller_velocity = false;
Input::SetVRMotion(0, accel, gyro, point);
return;
}
const Common::Quaternion remote = ToQuaternion(aim.orientation);
const Common::Quaternion to_remote = remote.Conjugate();
Common::Vec3 acceleration(0.0f, G, 0.0f);
const auto now = std::chrono::steady_clock::now();
if (aim.flags & RETRO_VR_POSE_LINEAR_VELOCITY_VALID)
{
const Common::Vec3 velocity(aim.linear_velocity[0], aim.linear_velocity[1],
aim.linear_velocity[2]);
const float dt = std::chrono::duration<float>(now - s_controller_time).count();
if (s_have_controller_velocity && dt > 1.0f / 500.0f && dt < 0.1f)
acceleration += (velocity - s_controller_velocity) / dt;
s_controller_velocity = velocity;
s_have_controller_velocity = true;
}
else
{
s_have_controller_velocity = false;
}
s_controller_time = now;
Common::Vec3 rotation;
if (aim.flags & RETRO_VR_POSE_ANGULAR_VELOCITY_VALID)
rotation = Common::Vec3(aim.angular_velocity[0], aim.angular_velocity[1],
aim.angular_velocity[2]);
// Into the remote's axes (+X right, +Y up, +Z back), then the Wiimote's (+X left, +Y back,
// +Z up): a rotation, so rates map the same way as accelerations.
const Common::Vec3 a = to_remote * acceleration;
const Common::Vec3 w = to_remote * rotation;
accel[0] = -a.x;
accel[1] = a.z;
accel[2] = a.y;
gyro[0] = -w.x;
gyro[1] = w.z;
gyro[2] = w.y;
// The cursor is where the remoting is pointing.
retro_vr_frame_state frame_state{};
if (environ_cb(RETRO_ENVIRONMENT_GET_VR_FRAME_STATE, &frame_state) &&
(frame_state.flags & RETRO_VR_FRAME_RECENTERED))
{
s_have_point_forward = false;
}
retro_vr_head_pose head{};
if (!s_have_point_forward &&
environ_cb(RETRO_ENVIRONMENT_GET_VR_HEAD_POSE, &head) &&
(head.flags & RETRO_VR_HEAD_POSE_ORIENTATION_VALID))
{
s_point_forward = Heading(ToQuaternion(head.orientation));
s_have_point_forward = true;
}
const Common::Vec3 direction = s_point_forward.Conjugate() * (remote * Common::Vec3(0, 0, -1));
const float yaw = std::atan2(direction.x, -direction.z);
const float pitch =
std::atan2(direction.y, std::sqrt(direction.x * direction.x + direction.z * direction.z));
constexpr float DEGREES = 180.0f / 3.14159265f;
const float half_yaw =
std::max(1, Options::GetCached<int>(Options::wiimote::IR_YAW, 25)) * 0.5f;
const float half_pitch =
std::max(1, Options::GetCached<int>(Options::wiimote::IR_PITCH, 25)) * 0.5f;
point[0] = std::clamp(yaw * DEGREES / half_yaw, -1.0f, 1.0f);
point[1] = std::clamp(pitch * DEGREES / half_pitch, -1.0f, 1.0f);
Input::SetVRMotion(0, accel, gyro, point);
}
void Update(float eye_aspect_ratio)
{
UpdateVRMotion();
const Mode mode = GetMode();
if (mode == Mode::Off)
{
DisableCamera();
ClearViews();
s_frontend_stereo_seen = false;
s_frontend_flat = false;
return;
}
unsigned status = 0;
if (!environ_cb(RETRO_ENVIRONMENT_GET_VIDEO_VIEWS_STATUS, &status))
status = 0;
const bool frontend_stereo = (status & RETRO_VIDEO_VIEWS_STATUS_STEREO) != 0;
s_frontend_stereo_seen |= frontend_stereo;
const bool frontend_flat = s_frontend_stereo_seen && !frontend_stereo;
if (frontend_flat != s_frontend_flat)
{
s_frontend_flat = frontend_flat;
INFO_LOG_FMT(VIDEO, "VR: frontend {} both eyes, stereoscopy {}",
frontend_flat ? "stopped showing" : "shows", frontend_flat ? "off" : "on");
Config::SetCurrent(Config::GFX_STEREO_MODE,
frontend_flat ? StereoMode::Off : StereoMode::SideBySide);
}
const unsigned width = g_presenter ? g_presenter->GetBackbufferWidth() : 0;
const unsigned height = g_presenter ? g_presenter->GetBackbufferHeight() : 0;
if (!(status & RETRO_VIDEO_VIEWS_STATUS_PRESENTS) || width < 2 || height == 0)
{
DisableCamera();
ClearViews();
return;
}
const bool stereo = g_ActiveConfig.stereo_mode == StereoMode::SideBySide;
const unsigned eye_width = stereo ? width / 2 : width;
retro_video_view views[2] = {};
retro_video_views map{};
map.views = views;
map.reference_space = RETRO_VR_REFERENCE_SPACE_LOCAL;
if (stereo && (status & RETRO_VIDEO_VIEWS_STATUS_STEREO))
{
views[0] = {0, 0, eye_width, height, 0, RETRO_VIDEO_VIEW_EYE_LEFT, eye_aspect_ratio};
views[1] = {eye_width, 0, eye_width, height, 0, RETRO_VIDEO_VIEW_EYE_RIGHT, eye_aspect_ratio};
map.num_views = 2;
}
else
{
views[0] = {0, 0, eye_width, height, 0, RETRO_VIDEO_VIEW_EYE_NONE, eye_aspect_ratio};
map.num_views = 1;
}
g_libretro_vr_camera.display_aspect = eye_aspect_ratio;
retro_vr_frame_state frame_state{};
if (mode == Mode::Immersive && map.num_views == 2 && (status & RETRO_VIDEO_VIEWS_STATUS_HMD) &&
environ_cb(RETRO_ENVIRONMENT_GET_VR_FRAME_STATE, &frame_state) && UpdateCamera(frame_state))
{
map.flags = RETRO_VIDEO_VIEWS_FLAG_TRACKED;
}
else
{
DisableCamera();
}
environ_cb(RETRO_ENVIRONMENT_SET_VIDEO_VIEWS, &map);
s_views_sent = true;
}
void Shutdown()
{
DisableCamera();
g_libretro_vr_camera.applied = false;
g_libretro_vr_camera.game_tan_x = 0.0f;
g_libretro_vr_camera.game_tan_y = 0.0f;
s_views_sent = false;
s_frontend_stereo_seen = false;
s_frontend_flat = false;
s_have_controller_velocity = false;
s_have_point_forward = false;
}
} // namespace VR
} // namespace Libretro
+28
View File
@@ -0,0 +1,28 @@
#pragma once
namespace Libretro
{
namespace VR
{
enum class Mode
{
Off,
// Both eyes side by side, shown by the frontend on a virtual screen.
Stereo,
// As Stereo, with the game camera turned by the headset and drawn at its field of view.
Immersive,
};
Mode GetMode();
bool IsEnabled();
// Sets Dolphin's stereoscopy (and, when immersive, aspect ratio) settings for the current mode.
void ApplyConfig();
// Called every retro_run() before the frame is emulated: tells the frontend where each eye is in
// the frame and, in immersive mode, points the game camera along the headset's pose.
void Update(float eye_aspect_ratio);
void Shutdown();
} // namespace VR
} // namespace Libretro
+10 -3
View File
@@ -40,6 +40,7 @@
#include "Core/Host.h"
#include "Core/System.h"
#include "DolphinLibretro/Common/Options.h"
#include "DolphinLibretro/VR.h"
#include "DolphinLibretro/VideoContexts/ContextStatus.h"
#include "VideoCommon/AsyncRequests.h"
@@ -71,6 +72,12 @@ static Common::DynamicLibrary d3d11_library;
static Common::DynamicLibrary d3d12_library;
#endif
int GetAdjustedBaseWidth()
{
// VR packs both eyes side by side, each at the full width.
return VR::IsEnabled() ? EFB_WIDTH * 2 : EFB_WIDTH;
}
int GetAdjustedBaseHeight()
{
const bool crop_overscan = Libretro::Options::GetCached<bool>(
@@ -288,7 +295,7 @@ void ContextReset(void)
int efbScale = Libretro::Options::GetCached<int>(
Libretro::Options::gfx_settings::EFB_SCALE, 1);
Vk::SetSurfaceSize(EFB_WIDTH * efbScale,
Vk::SetSurfaceSize(GetAdjustedBaseWidth() * efbScale,
GetAdjustedBaseHeight() * efbScale);
}
#endif
@@ -360,7 +367,7 @@ void ContextReset(void)
UpdateActiveConfig();
std::unique_ptr<DX11SwapChain> swap_chain = std::make_unique<DX11SwapChain>(
wsi, EFB_WIDTH * efbScale, GetAdjustedBaseHeight() * efbScale,
wsi, GetAdjustedBaseWidth() * efbScale, GetAdjustedBaseHeight() * efbScale,
nullptr, nullptr);
auto gfx = std::make_unique<DX11::Gfx>(std::move(swap_chain), wsi.render_surface_scale);
@@ -437,7 +444,7 @@ void ContextReset(void)
UpdateActiveConfig();
auto swap_chain = std::make_unique<DX12SwapChain>(
wsi, EFB_WIDTH * efbScale, GetAdjustedBaseHeight() * efbScale, d3d12);
wsi, GetAdjustedBaseWidth() * efbScale, GetAdjustedBaseHeight() * efbScale, d3d12);
if (!swap_chain->Initialize())
{
+1
View File
@@ -36,6 +36,7 @@ namespace Libretro
{
namespace Video
{
int GetAdjustedBaseWidth();
int GetAdjustedBaseHeight();
void Init(void);
bool Video_InitializeBackend();
@@ -318,7 +318,7 @@ void* GLContextLR::GetFuncAddress(const std::string& name)
bool GLContextLR::Initialize(const WindowSystemInfo& wsi, bool stereo, bool core)
{
const int efbScale = Libretro::Options::GetCached<int>(Libretro::Options::gfx_settings::EFB_SCALE, 1);
m_backbuffer_width = EFB_WIDTH * efbScale;
m_backbuffer_width = Libretro::Video::GetAdjustedBaseWidth() * efbScale;
m_backbuffer_height = Libretro::Video::GetAdjustedBaseHeight() * efbScale;
switch (Libretro::Video::hw_render.context_type)
+3 -1
View File
@@ -418,7 +418,9 @@ static VKAPI_ATTR VkResult VKAPI_CALL vkQueuePresentKHR(VkQueue queue,
{
if (AbstractTexture* efb_tex = g_framebuffer_manager->GetEFBColorTexture())
{
out_w = efb_tex->GetWidth();
// VR packs both eyes side by side, so the frame is wider than the EFB by the same
// factor as the swapchain.
out_w = efb_tex->GetWidth() * Libretro::Video::GetAdjustedBaseWidth() / EFB_WIDTH;
out_h = efb_tex->GetHeight();
}
}
@@ -272,6 +272,11 @@ std::string GeneratePassthroughGeometryShader(u32 num_tex, u32 num_colors)
// We have to explicitly unroll this loop otherwise the GL compiler gets cranky.
for (u32 v = 0; v < 3; v++)
{
#ifdef __LIBRETRO__
// Outputs are undefined after EmitVertex()
if (v > 0)
code.Write(" gl_Layer = j;\n");
#endif
code.Write(" gl_Position = gl_in[{}].gl_Position;\n", v);
for (u32 i = 0; i < num_tex; i++)
{
@@ -9,6 +9,11 @@
#include "VideoCommon/RenderState.h"
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/XFMemory.h"
#ifdef __LIBRETRO__
#include <cmath>
#include "VideoCommon/VertexShaderManager.h"
#endif
static constexpr int LINE_PT_TEX_OFFSETS[8] = {0, 16, 8, 4, 2, 1, 1, 1};
@@ -63,6 +68,31 @@ void GeometryShaderManager::SetConstants(PrimitiveType prim)
constants.stereoparams[2] = g_ActiveConfig.stereo_convergence;
#ifdef __LIBRETRO__
if (g_libretro_vr_camera.applied)
{
// Move each eye onto its own headset frustum as well as separating it. The shader adds
// hoffset * (w - convergence), so scaling the convergence keeps the separation's constant
// term while hoffset gains the frustum offset.
const float offset = g_ActiveConfig.stereo_depth;
const float eye_offset = g_libretro_vr_camera.eye_offset_x;
const float left = eye_offset - offset;
constants.stereoparams[0] = left;
constants.stereoparams[1] = -left;
constants.stereoparams[2] =
std::abs(left) > 1e-6f ? g_ActiveConfig.stereo_convergence * -offset / left : 0.0f;
}
else if (g_libretro_vr_camera.screen_applied)
{
// 2D has w = 1, so this moves each eye's copy of the screen onto its own frustum. Both
// eyes then see it in the same direction.
const float eye_offset = g_libretro_vr_camera.eye_offset_x;
constants.stereoparams[0] = eye_offset;
constants.stereoparams[1] = -eye_offset;
constants.stereoparams[2] = 0.0f;
}
#endif
dirty = true;
}
@@ -559,6 +559,10 @@ void VertexManagerBase::Flush()
CalculateNormals(VertexLoaderManager::GetCurrentVertexFormat());
// Calculate ZSlope for zfreeze
const auto used_textures = UsedTextures();
#ifdef __LIBRETRO__
bool vr_textured = false;
bool vr_samples_copy = false;
#endif
std::vector<std::string> texture_names;
Common::SmallVector<u32, 8> texture_units;
std::array<SamplerState, 8> samplers;
@@ -571,6 +575,10 @@ void VertexManagerBase::Flush()
const auto cache_entry = g_texture_cache->Load(i);
if (!cache_entry)
continue;
#ifdef __LIBRETRO__
vr_textured = true;
vr_samples_copy |= cache_entry->IsCopy();
#endif
const float custom_tex_scale = cache_entry->GetWidth() / float(cache_entry->native_width);
samplers[i] = TextureCacheBase::GetSamplerState(
i, custom_tex_scale, cache_entry->is_custom_tex, cache_entry->has_arbitrary_mips);
@@ -583,6 +591,10 @@ void VertexManagerBase::Flush()
const auto cache_entry = g_texture_cache->Load(i);
if (cache_entry)
{
#ifdef __LIBRETRO__
vr_textured = true;
vr_samples_copy |= cache_entry->IsCopy();
#endif
if (!Common::Contains(texture_names, cache_entry->texture_info_name))
{
texture_names.push_back(cache_entry->texture_info_name);
@@ -596,6 +608,10 @@ void VertexManagerBase::Flush()
}
}
}
#ifdef __LIBRETRO__
g_libretro_vr_camera.draw_textured = vr_textured;
g_libretro_vr_camera.draw_samples_copy = vr_samples_copy;
#endif
vertex_shader_manager.SetConstants(texture_names, xf_state_manager);
if (!bpmem.genMode.zfreeze)
{
@@ -25,6 +25,44 @@
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/XFMemory.h"
#include "VideoCommon/XFStateManager.h"
#ifdef __LIBRETRO__
#include "Core/System.h"
#include "VideoCommon/GeometryShaderManager.h"
LibretroVRCamera g_libretro_vr_camera;
// The headset only replaces what is drawn to the screen. Render-to-texture passes such as shadows
// and reflections draw into smaller viewports and keep the game's projection.
static bool IsLibretroVRScreenViewport()
{
return g_libretro_vr_camera.enabled &&
std::abs(2.0f * xfmem.viewport.wd) >= EFB_WIDTH * 0.75f &&
std::abs(2.0f * xfmem.viewport.ht) >= EFB_HEIGHT * 0.5f;
}
static bool UseLibretroVRCamera()
{
return IsLibretroVRScreenViewport() && xfmem.projection.type == ProjectionType::Perspective;
}
// Textured 2D such as menus and HUD. Untextured 2D (fades) and 2D drawn from a copy of the frame
// (bloom, blur) cover the scene, which fills the headset's view, so they stay full size.
static bool UseLibretroVRScreen()
{
return IsLibretroVRScreenViewport() &&
xfmem.projection.type == ProjectionType::Orthographic &&
g_libretro_vr_camera.draw_textured && !g_libretro_vr_camera.draw_samples_copy;
}
static bool LibretroVRCameraChanged()
{
const bool camera = UseLibretroVRCamera();
const bool screen = UseLibretroVRScreen();
return camera != g_libretro_vr_camera.applied ||
screen != g_libretro_vr_camera.screen_applied ||
((camera || screen) && g_libretro_vr_camera.dirty);
}
#endif
void VertexShaderManager::Init()
{
@@ -109,8 +147,52 @@ Common::Matrix44 VertexShaderManager::LoadProjectionMatrix()
PRIM_LOG("Projection: {} {} {} {} {} {}", rawProjection[0], rawProjection[1], rawProjection[2],
rawProjection[3], rawProjection[4], rawProjection[5]);
#ifdef __LIBRETRO__
const bool use_vr_camera = UseLibretroVRCamera();
const bool use_vr_screen = UseLibretroVRScreen();
LibretroVRCamera& vr = g_libretro_vr_camera;
if (use_vr_camera)
{
// Remember how wide the game's camera sees, for the 2D screen.
vr.game_tan_x = 1.0f / std::abs(m_projection_matrix[0]);
vr.game_tan_y = 1.0f / std::abs(m_projection_matrix[5]);
// Draw with the headset's field of view, keeping the sign of a mirrored projection.
const float sign_x = rawProjection[0] < 0.0f ? -1.0f : 1.0f;
const float sign_y = rawProjection[2] < 0.0f ? -1.0f : 1.0f;
m_projection_matrix[0] = vr.scale_x * sign_x;
m_projection_matrix[2] = vr.offset_x * sign_x;
m_projection_matrix[5] = vr.scale_y * sign_y;
m_projection_matrix[6] = vr.offset_y * sign_y;
vr.dirty = false;
}
else if (use_vr_screen)
{
// Full-screen 2D
const float tan_x = vr.game_tan_x > 0.0f ? vr.game_tan_x : 0.58f;
const float tan_y = vr.game_tan_y > 0.0f ? vr.game_tan_y : 0.58f / vr.display_aspect;
const float scale_x = tan_x * vr.scale_x;
const float scale_y = tan_y * vr.scale_y;
m_projection_matrix[0] *= scale_x;
m_projection_matrix[3] = m_projection_matrix[3] * scale_x - vr.offset_x;
m_projection_matrix[5] *= scale_y;
m_projection_matrix[7] = m_projection_matrix[7] * scale_y - vr.offset_y;
vr.dirty = false;
}
// The per-eye stereo offsets depend on whether this projection uses the headset.
if (use_vr_camera || use_vr_screen || vr.applied || vr.screen_applied)
Core::System::GetInstance().GetGeometryShaderManager().SetProjectionChanged();
vr.applied = use_vr_camera;
vr.screen_applied = use_vr_screen;
#endif
auto corrected_matrix = Common::Matrix44::FromArray(m_projection_matrix);
#ifdef __LIBRETRO__
if (use_vr_camera)
corrected_matrix *= g_libretro_vr_camera.view;
#endif
if (g_freelook_camera.IsActive() && xfmem.projection.type == ProjectionType::Perspective)
corrected_matrix *= g_freelook_camera.GetView();
@@ -121,7 +203,11 @@ Common::Matrix44 VertexShaderManager::LoadProjectionMatrix()
void VertexShaderManager::SetProjectionMatrix(XFStateManager& xf_state_manager)
{
if (xf_state_manager.DidProjectionChange() || g_freelook_camera.GetController()->IsDirty())
if (xf_state_manager.DidProjectionChange() || g_freelook_camera.GetController()->IsDirty()
#ifdef __LIBRETRO__
|| LibretroVRCameraChanged()
#endif
)
{
xf_state_manager.ResetProjection();
auto corrected_matrix = LoadProjectionMatrix();
@@ -412,7 +498,11 @@ void VertexShaderManager::SetConstants(std::span<const std::string> textures,
}
if (xf_state_manager.DidProjectionChange() || g_freelook_camera.GetController()->IsDirty() ||
!projection_actions.empty() || m_projection_graphics_mod_change)
!projection_actions.empty() || m_projection_graphics_mod_change
#ifdef __LIBRETRO__
|| LibretroVRCameraChanged()
#endif
)
{
xf_state_manager.ResetProjection();
m_projection_graphics_mod_change = !projection_actions.empty();
@@ -16,6 +16,39 @@ class PointerWrap;
struct PortableVertexDeclaration;
class XFStateManager;
#ifdef __LIBRETRO__
// Head-tracked camera for the libretro core's immersive VR mode.
struct LibretroVRCamera
{
bool enabled = false;
// Set when the values below change, so the projection is reloaded.
bool dirty = false;
// Whether the projection currently loaded uses the headset camera, or the 2D screen.
bool applied = false;
bool screen_applied = false;
// Head rotation, applied after the game's view like the Free Look camera.
Common::Matrix44 view = Common::Matrix44::Identity();
// The headset's eye frustums averaged into one centred projection, in Dolphin's layout.
float scale_x = 1.0f;
float offset_x = 0.0f;
float scale_y = 1.0f;
float offset_y = 0.0f;
// How far the left eye's frustum sits from the centred one, in clip-space x per unit of w.
// The right eye's is the negation.
float eye_offset_x = 0.0f;
// Tangents of the game camera's half field of view, from its last projection; 0 until seen.
float game_tan_x = 0.0f;
float game_tan_y = 0.0f;
// The display's shape, for the 2D screen before the game camera is seen.
float display_aspect = 4.0f / 3.0f;
// What the draw being flushed samples. 2D drawn from a copy of the frame (bloom, blur, fades
// of the scene) must cover the scene exactly, so it is left at full size.
bool draw_textured = false;
bool draw_samples_copy = false;
};
extern LibretroVRCamera g_libretro_vr_camera;
#endif
// The non-API dependent parts.
class alignas(16) VertexShaderManager
{
@@ -268,6 +268,10 @@ void VideoBackendBase::PopulateBackendInfo(const WindowSystemInfo& wsi)
ActivateBackend(Config::Get(Config::MAIN_GFX_BACKEND));
g_backend_info.DisplayName = g_video_backend->GetDisplayName();
g_video_backend->InitBackendInfo(wsi);
#ifdef __LIBRETRO__
if (g_backend_info.api_type == APIType::Nothing)
return;
#endif
// We validate the config after initializing the backend info, as system-specific settings
// such as anti-aliasing, or the selected adapter may be invalid, and should be checked.
g_Config.VerifyValidity();