Add OpenXR VR rendering support

This commit is contained in:
obesecatlord 2026-05-10 22:07:36 -05:00
parent d3b482c103
commit f47d1c0331
23 changed files with 2358 additions and 162 deletions

View file

@ -4,6 +4,8 @@ BlanKart is a modification of SRB2Kart v2 Indev to make it closer to SRB2Kart ga
If you're interested in helping out, theres a matrix room located [here](https://matrix.to/#/#blankart:matrix.org)!
The VR work in this fork is inspired in part by [`kart-public-vr`](https://git.do.srb2.org/chreas/kart-public-vr), an earlier SRB2Kart OpenVR implementation.
# Notice
This is still in active development and things are going to change. If you find any bugs besure to report to the [issue tracker](https://codeberg.org/NepDisk/blankart/issues)!

View file

@ -157,6 +157,9 @@ add_executable(BLANKART MACOSX_BUNDLE WIN32
h_timers.cpp
stun.c
lonesha256.c
vr/vr_main.c
vr/vr_render.c
vr/vr_math.c
)
if(("${CMAKE_COMPILER_IS_GNUCC}" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") AND "${CMAKE_SYSTEM_NAME}" MATCHES "Windows")
@ -364,6 +367,13 @@ if(SRB2_CONFIG_ENABLE_DISCORDRPC)
target_sources(BLANKART PRIVATE discord.c)
endif()
# VR Support
target_compile_definitions(BLANKART PRIVATE -DHAVE_VR)
target_link_libraries(BLANKART PRIVATE openxr_loader)
if(UNIX AND NOT APPLE)
target_link_libraries(BLANKART PRIVATE GL GLX X11)
endif()
set(SRB2_HAVE_THREADS ON)
target_compile_definitions(BLANKART PRIVATE -DHAVE_THREADS)

View file

@ -56,6 +56,7 @@
#include "r_local.h"
#include "r_voicepreference.hpp" // Preferences directory
#include "s_sound.h"
#include "screen.h"
#include "st_stuff.h"
#include "v_video.h"
#include "w_wad.h"
@ -425,12 +426,80 @@ gamestate_t wipegamestate = GS_LEVEL;
INT16 wipetypepre = -1;
INT16 wipetypepost = -1;
#ifdef HAVE_VR
#include "vr/vr_main.h"
#include "vr/vr_render.h"
static void D_Display_Internal(void);
static void D_Display(void)
{
static boolean vr_auto_started_menu = false;
if (vr_started && gamestate == GS_TITLESCREEN && !menustack[0] && !vr_auto_started_menu)
{
M_StartControlPanel();
CONS_Printf("VR: Auto-opened title menu; menustack[0]=%d, gamestate=%d.\n",
(INT32)menustack[0], (INT32)gamestate);
vr_auto_started_menu = true;
}
else if (!vr_started || gamestate != GS_TITLESCREEN)
vr_auto_started_menu = false;
const boolean vr_quad_ui = (cv_vruimode.value != 0);
if (vr_started && VR_BeginFrame())
{
if (VR_SetEye(0))
{
D_Display_Internal();
VR_ReleaseEye(0);
}
if (VR_SetEye(1))
{
D_Display_Internal();
VR_ReleaseEye(1);
}
if (vr_quad_ui && VR_BindUISwapchain())
{
D_Display_Internal();
VR_BindDefaultFramebuffer();
}
VR_EndFrame();
}
else
{
vr_render_pass = VR_PASS_NONE;
D_Display_Internal();
}
}
static void D_Display_Internal(void)
#else
static void D_Display(void)
#endif
{
boolean forcerefresh = false;
static boolean wipe = false;
INT32 wipedefindex = 0;
UINT8 i;
#ifdef HAVE_VR
const boolean vr_quad_ui = (vr_started && cv_vruimode.value != 0);
const boolean vr_in_eye_ui = (vr_started && !vr_quad_ui
&& (vr_render_pass == VR_PASS_3D_LEFT || vr_render_pass == VR_PASS_3D_RIGHT));
const boolean vr_draw_3d = (!vr_started || vr_render_pass == VR_PASS_NONE
|| vr_render_pass == VR_PASS_3D_LEFT || vr_render_pass == VR_PASS_3D_RIGHT);
const boolean vr_draw_ui = (!vr_started || vr_render_pass == VR_PASS_NONE || vr_render_pass == VR_PASS_UI
|| (!vr_quad_ui && (vr_render_pass == VR_PASS_3D_LEFT || vr_render_pass == VR_PASS_3D_RIGHT)));
const boolean vr_draw_title = (!vr_started || vr_render_pass != VR_PASS_UI);
#else
const boolean vr_in_eye_ui = false;
const boolean vr_draw_3d = true;
const boolean vr_draw_ui = true;
const boolean vr_draw_title = true;
#endif
ZoneScoped;
@ -539,79 +608,89 @@ static void D_Display(void)
}
// do buffered drawing
switch (gamestate)
if (vr_draw_ui)
{
case GS_TITLESCREEN:
if (!titlemapinaction || !curbghide) {
F_TitleScreenDrawer();
break;
}
/* FALLTHRU */
case GS_LEVEL:
if (!gametic)
break;
AM_Drawer();
break;
#ifdef HAVE_VR
if (vr_in_eye_ui)
VR_BeginInEyeUI();
#endif
case GS_INTERMISSION:
Y_IntermissionDrawer();
HU_Drawer();
break;
case GS_VOTING:
Y_VoteDrawer();
HU_Drawer();
break;
case GS_TIMEATTACK:
break;
case GS_INTRO:
F_IntroDrawer();
if (wipegamestate == (gamestate_t)-1)
switch (gamestate)
{
wipe = true;
wipedefindex = gamestate; // wipe_xxx_toblack
}
break;
case GS_TITLESCREEN:
if (!vr_draw_title)
break;
if (!titlemapinaction || !curbghide) {
F_TitleScreenDrawer();
break;
}
/* FALLTHRU */
case GS_LEVEL:
if (!gametic)
break;
AM_Drawer();
break;
case GS_CUTSCENE:
F_CutsceneDrawer();
HU_Drawer();
break;
case GS_EVALUATION:
F_GameEvaluationDrawer();
HU_Drawer();
break;
case GS_CREDITS:
F_CreditDrawer();
HU_Drawer();
break;
case GS_BLANCREDITS:
F_BlanCreditDrawer();
HU_Drawer();
break;
case GS_SECRETCREDITS:
F_SecretCreditsDrawer();
HU_Drawer();
break;
case GS_WAITINGPLAYERS:
// The clientconnect drawer is independent...
if (netgame)
{
// I don't think HOM from nothing drawing is independent...
F_WaitingPlayersDrawer();
case GS_INTERMISSION:
Y_IntermissionDrawer();
HU_Drawer();
}
case GS_DEDICATEDSERVER:
case GS_NULL:
case FORCEWIPE:
break;
break;
case GS_VOTING:
Y_VoteDrawer();
HU_Drawer();
break;
case GS_TIMEATTACK:
break;
case GS_INTRO:
F_IntroDrawer();
if (wipegamestate == (gamestate_t)-1)
{
wipe = true;
wipedefindex = gamestate; // wipe_xxx_toblack
}
break;
case GS_CUTSCENE:
F_CutsceneDrawer();
HU_Drawer();
break;
case GS_EVALUATION:
F_GameEvaluationDrawer();
HU_Drawer();
break;
case GS_CREDITS:
F_CreditDrawer();
HU_Drawer();
break;
case GS_BLANCREDITS:
F_BlanCreditDrawer();
HU_Drawer();
break;
case GS_SECRETCREDITS:
F_SecretCreditsDrawer();
HU_Drawer();
break;
case GS_WAITINGPLAYERS:
// The clientconnect drawer is independent...
if (netgame)
{
// I don't think HOM from nothing drawing is independent...
F_WaitingPlayersDrawer();
HU_Drawer();
}
case GS_DEDICATEDSERVER:
case GS_NULL:
case FORCEWIPE:
break;
}
}
// STUPID race condition...
@ -620,22 +699,31 @@ static void D_Display(void)
// clean up border stuff
// see if the border needs to be initially drawn
if (gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction && curbghide && (!hidetitlemap)))
{
D_RenderLevel();
ps_uitime = I_GetPreciseTime();
if (gamestate == GS_LEVEL)
if (gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction && curbghide && (!hidetitlemap)))
{
if (vr_draw_3d)
D_RenderLevel();
ps_uitime = I_GetPreciseTime();
#ifdef HAVE_VR
if (vr_in_eye_ui)
VR_BeginInEyeUI();
#endif
if (vr_draw_ui && gamestate == GS_LEVEL)
{
ST_Drawer();
F_TextPromptDrawer();
HU_Drawer();
}
else if (vr_draw_ui)
{
if (vr_draw_title)
F_TitleScreenDrawer();
}
}
else
F_TitleScreenDrawer();
}
else
{
ps_uitime = I_GetPreciseTime();
@ -648,7 +736,7 @@ static void D_Display(void)
V_SetPalette(0);
// draw pause pic
if (paused && cv_showhud.value && !demo.playback)
if (vr_draw_ui && paused && cv_showhud.value && !demo.playback)
{
INT32 py;
patch_t *patch;
@ -660,24 +748,45 @@ static void D_Display(void)
V_DrawScaledPatch(viewwindowx + (BASEVIDWIDTH - patch->width)/2, py, V_SNAPTOTOP, patch);
}
if (demo.rewinding)
if (vr_draw_ui && demo.rewinding)
V_DrawFadeScreen(TC_RAINBOW, (leveltime & 0x20) ? SKINCOLOR_PASTEL : SKINCOLOR_MOONSLAM);
// vid size change is now finished if it was on...
vid.recalc = false;
#ifdef HAVE_THREADS
I_lock_mutex(&m_menu_mutex);
if (vr_draw_ui)
{
#ifdef HAVE_VR
if (vr_in_eye_ui)
VR_BeginInEyeUI();
#endif
M_Drawer(); // menu is drawn even on top of everything
if (cv_songcredits.value && !( (G_GamestateUsesLevel() && hu_showscores) && (netgame || multiplayer) ))
HU_DrawSongCredits(); // As are music credits.
#ifdef HAVE_THREADS
I_unlock_mutex(m_menu_mutex);
#endif
// focus lost moved to M_Drawer
CON_Drawer();
#ifdef HAVE_VR
if (vr_started && vr_render_pass == VR_PASS_UI)
{
static boolean vr_logged_ui_pass = false;
if (!vr_logged_ui_pass)
{
CONS_Printf("VR: UI pass active; menustack[0]=%d, gamestate=%d, vid=%dx%d dup=%d, ui=%ux%u.\n",
(INT32)menustack[0], (INT32)gamestate, vid.width, vid.height, vid.dup, vr_ui_width, vr_ui_height);
vr_logged_ui_pass = true;
}
}
#endif
#ifdef HAVE_THREADS
I_lock_mutex(&m_menu_mutex);
#endif
M_Drawer(); // menu is drawn even on top of everything
if (cv_songcredits.value && !( (G_GamestateUsesLevel() && hu_showscores) && (netgame || multiplayer) ))
HU_DrawSongCredits(); // As are music credits.
#ifdef HAVE_THREADS
I_unlock_mutex(m_menu_mutex);
#endif
// focus lost moved to M_Drawer
CON_Drawer();
}
ps_uitime = I_GetPreciseTime() - ps_uitime;
@ -712,13 +821,16 @@ dedipostwipe:
if (dedicated)
return; // NOW we can bail
NetUpdate(); // send out any new accumulation
// It's safe to end the game now.
if (G_GetExitGameFlag())
if (vr_draw_ui)
{
Command_ExitGame_f();
G_ClearExitGameFlag();
NetUpdate(); // send out any new accumulation
// It's safe to end the game now.
if (G_GetExitGameFlag())
{
Command_ExitGame_f();
G_ClearExitGameFlag();
}
}
//
@ -726,10 +838,10 @@ dedipostwipe:
//
if (!wipe)
{
if (cv_shittyscreen.value)
if (vr_draw_ui && cv_shittyscreen.value)
V_DrawVhsEffect(cv_shittyscreen.value == 2);
if (cv_netstat.value)
if (vr_draw_ui && cv_netstat.value)
{
char s[50];
Net_GetNetStat();
@ -746,7 +858,7 @@ dedipostwipe:
V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-10, V_YELLOWMAP, s);
}
if (cv_perfstats.value)
if (vr_draw_ui && cv_perfstats.value)
{
M_DrawPerfStats();
}
@ -812,6 +924,15 @@ static double D_EndFrame(precise_t enterprecise, int *frameskip)
double deltasecs = (double)((INT64)(finishprecise - enterprecise)) / I_GetPrecisePrecision();
double deltatics = deltasecs * NEWTICRATE;
#ifdef HAVE_VR
if (vr_started)
{
if (frameskip)
*frameskip = 0;
return deltatics;
}
#endif
// If time spent this game loop exceeds a single tic,
// it's probably because of rendering.
//

View file

@ -1512,6 +1512,19 @@ void D_RegisterClientCommands(void)
CV_RegisterVar(&cv_scr_depth);
CV_RegisterVar(&cv_scr_width);
CV_RegisterVar(&cv_scr_height);
CV_RegisterVar(&cv_vrviewmode);
CV_RegisterVar(&cv_vrcomfortmode);
CV_RegisterVar(&cv_vrenabled);
CV_RegisterVar(&cv_vrresolution);
CV_RegisterVar(&cv_vrscale);
CV_RegisterVar(&cv_vruidistance);
CV_RegisterVar(&cv_vruiscale);
CV_RegisterVar(&cv_vruimode);
CV_RegisterVar(&cv_vrposemode);
CV_RegisterVar(&cv_vrplayerscale);
CV_RegisterVar(&cv_vrspriterotate);
CV_RegisterVar(&cv_vrdisableskystereo);
CV_RegisterVar(&cv_vrtrackintro);
CV_RegisterVar(&cv_parallelsoftware);
CV_RegisterVar(&cv_director);

View file

@ -78,6 +78,9 @@
#include "../tables.h"
#include "r_opengl/r_opengl.h"
#include "../r_main.h" // for cv_fov
#ifdef HAVE_VR
#include "../vr/vr_main.h"
#endif
typedef struct clipnode_s
{
@ -333,6 +336,11 @@ angle_t gld_FrustumAngle(angle_t tiltangle)
// ok, this is a gross hack that barely works...
// but at least it doesn't overestimate too much...
#ifdef HAVE_VR
if (vr_started)
clipfov = 120.0;
else
#endif
clipfov = atan(1 / (GLdouble)projMatrix[0]) * 360.0 / M_PI;
floatangle = 2.0 + (45.0 + ((double)tilt / 1.9)) * clipfov / 90.0;
if (floatangle >= 180.0)

View file

@ -46,6 +46,11 @@
#include "../p_slopes.h"
#include "hw_md2.h"
#ifdef HAVE_VR
#include "../screen.h"
#include "../vr/vr_main.h"
#endif
// SRB2Kart
#include "../k_kart.h"
#include "../r_fps.h"
@ -522,6 +527,18 @@ void HWR_RenderViewpoint(player_t *player, boolean drawSkyTexture, boolean is_sk
// since our clipper uses this to determine our actual visible geometry
GL_SetTransform(&atransform);
HWR_ClearSprites();
#ifdef HAVE_VR
// Let sprite billboards and clipping follow the user's HMD yaw in VR.
if (vr_started && cv_vrspriterotate.value)
{
if (cv_kartencore.value)
viewangle += ANGLE_MAX * atan2(vrHMDPoseMatrix[8], vrHMDPoseMatrix[0]) / M_PI * 0.5;
else
viewangle -= ANGLE_MAX * atan2(vrHMDPoseMatrix[8], vrHMDPoseMatrix[0]) / M_PI * 0.5;
}
#endif
HWR_ResetClipper();
if (rootportal)
@ -560,7 +577,11 @@ void HWR_RenderViewpoint(player_t *player, boolean drawSkyTexture, boolean is_sk
// HWR_DrawSkyBackground is not able to set the texture without
// pausing batching first
HWR_PauseBatching();
if (skyboxmo[0] && cv_skybox.value && !is_skybox && !rootportal && !gl_debugportal)
if (skyboxmo[0] && cv_skybox.value && !is_skybox && !rootportal && !gl_debugportal
#ifdef HAVE_VR
&& (!vr_started || !cv_vrcomfortmode.value)
#endif
)
{
//if (gl_printportals)
// CONS_Printf("drawing a skybox\n");
@ -701,11 +722,33 @@ void HWR_RollTransform(FTransform *tr, angle_t roll)
}
}
#ifdef HAVE_VR
static inline boolean HWR_VRRenderingEye(void)
{
return (vr_started && !vr_drawing_ui &&
(vr_render_pass == VR_PASS_3D_LEFT || vr_render_pass == VR_PASS_3D_RIGHT));
}
#endif
// -----------------+
// HWR_ClearView : clear the viewwindow, with maximum z value. also clears stencil buffer.
// -----------------+
static inline void HWR_ClearView(void)
{
#ifdef HAVE_VR
if (HWR_VRRenderingEye())
{
GL_GClipRect(0,
0,
(INT32)vr_render_width,
(INT32)vr_render_height,
ZCLIP_PLANE);
GL_ClearBuffer(false, true, true, NULL);
return;
}
#endif
GL_GClipRect(viewwindowx,
viewwindowy,
(viewwindowx + viewwidth),
@ -719,7 +762,11 @@ void HWR_RenderPlayerView(void)
{
player_t * player = &players[displayplayers[viewssnum]];
const boolean skybox = (skyboxmo[0] && cv_skybox.value); // True if there's a skybox object and skyboxes are on
const boolean skybox = (skyboxmo[0] && cv_skybox.value
#ifdef HAVE_VR
&& (!vr_started || !cv_vrcomfortmode.value)
#endif
); // True if there's a skybox object and skyboxes are on
FRGBAFloat ClearColor;
@ -744,6 +791,14 @@ void HWR_RenderPlayerView(void)
if (viewssnum == 0) // Only do it if it's the first screen being rendered
GL_ClearBuffer(true, true, true, &ClearColor); // Clear the Color Buffer, stops HOMs. Also seems to fix the skybox issue on Intel GPUs.
#ifdef HAVE_VR
if (vr_started)
{
const float player_scale = (cv_vrplayerscale.value && player->mo) ? FIXED_TO_FLOAT(player->mo->scale) : 1.0f;
VR_ScaleViewMatrices(player_scale, cv_vrdisableskystereo.value ? 0 : 1);
}
#endif
ps_hw_skyboxtime = I_GetPreciseTime();
if (skybox) // If there's a skybox and we should be drawing the sky, draw the skybox
{
@ -798,6 +853,11 @@ void HWR_RenderPlayerView(void)
// added by Hurdler for correct splitscreen
// moved here by hurdler so it works with the new near clipping plane
#ifdef HAVE_VR
if (HWR_VRRenderingEye())
GL_GClipRect(0, 0, (INT32)vr_render_width, (INT32)vr_render_height, NZCLIP_PLANE);
else
#endif
GL_GClipRect(0, 0, vid.width, vid.height, NZCLIP_PLANE);
}

View file

@ -15,6 +15,9 @@
#include "hw_gpu.h"
#include "hw_shaders.h"
#include "../z_zone.h"
#ifdef HAVE_VR
#include "../vr/vr_main.h"
#endif
// ================
// Shader sources
@ -361,7 +364,17 @@ static void HWR_CompileShader(int index)
if (vertex_source)
{
char *preprocessed = HWR_PreprocessShader(vertex_source);
char *source_to_preprocess = vertex_source;
#ifdef HAVE_VR
const int shader_target = index % NUMSHADERTARGETS;
if (vr_started && shader_target >= SHADER_FLOOR && shader_target <= SHADER_SKY)
source_to_preprocess = Z_StrDup(GLSL_VR_VERTEX_SHADER);
#endif
char *preprocessed = HWR_PreprocessShader(source_to_preprocess);
#ifdef HAVE_VR
if (source_to_preprocess != vertex_source)
Z_Free(source_to_preprocess);
#endif
if (!preprocessed) return;
GL_LoadShader(index, preprocessed, HWD_SHADERSTAGE_VERTEX);
}

View file

@ -23,12 +23,35 @@
//
#define GLSL_FALLBACK_VERTEX_SHADER \
"uniform mat4 vrEyeMatrix;\n" \
"uniform mat4 vrEyeProjection;\n" \
"uniform mat4 vrHeadPoseMatrix;\n" \
"void main()\n" \
"{\n" \
"gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;\n" \
"gl_FrontColor = gl_Color;\n" \
"gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;\n" \
"gl_ClipVertex = gl_ModelViewMatrix * gl_Vertex;\n" \
"\tif (vrEyeProjection[0][0] != 0.0)\n" \
"\t{\n" \
"\t\tgl_Position = vrEyeProjection * vrEyeMatrix * vrHeadPoseMatrix * gl_ModelViewMatrix * gl_Vertex;\n" \
"\t\tgl_ClipVertex = vrEyeMatrix * vrHeadPoseMatrix * gl_ModelViewMatrix * gl_Vertex;\n" \
"\t}\n" \
"\telse\n" \
"\t{\n" \
"\t\tgl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;\n" \
"\t\tgl_ClipVertex = gl_ModelViewMatrix * gl_Vertex;\n" \
"\t}\n" \
"\tgl_FrontColor = gl_Color;\n" \
"\tgl_TexCoord[0].xy = gl_MultiTexCoord0.xy;\n" \
"}\0"
#define GLSL_VR_VERTEX_SHADER \
"uniform mat4 vrEyeMatrix;\n" \
"uniform mat4 vrEyeProjection;\n" \
"uniform mat4 vrHeadPoseMatrix;\n" \
"void main()\n" \
"{\n" \
"\tgl_Position = vrEyeProjection * vrEyeMatrix * vrHeadPoseMatrix * gl_ModelViewMatrix * gl_Vertex;\n" \
"\tgl_ClipVertex = vrEyeMatrix * vrHeadPoseMatrix * gl_ModelViewMatrix * gl_Vertex;\n" \
"\tgl_FrontColor = gl_Color;\n" \
"\tgl_TexCoord[0].xy = gl_MultiTexCoord0.xy;\n" \
"}\0"
//

View file

@ -17,6 +17,11 @@
#include "../g_game.h"
#include "../r_fps.h"
#ifdef HAVE_VR
#include "../screen.h"
#include "../vr/vr_main.h"
#endif
// ==========================================================================
// Sky dome rendering, ported from PrBoom+
@ -163,6 +168,11 @@ void HWR_BuildSkyDome(void)
void HWR_DrawSkyBackground(player_t *player)
{
#ifdef HAVE_VR
if (vr_started && cv_vrcomfortmode.value)
return;
#endif
GL_SetBlend(PF_Translucent|PF_NoDepthTest|PF_Modulated);
if (cv_glskydome.value)

View file

@ -28,6 +28,12 @@
#include "../hw_shaders.h"
#include "../hw_gpu.h"
#ifdef HAVE_VR
#include "../../vr/vr_main.h"
#include "../../screen.h"
extern boolean gl_rendering_skybox;
#endif
#if defined (HWRENDER) && !defined (NOROPENGL)
// requires GL 4.3
@ -603,6 +609,7 @@ typedef void (APIENTRY *PFNglUniform4f) (GLint, GLfloat, GLfloat, GLfloat, GL
typedef void (APIENTRY *PFNglUniform1fv) (GLint, GLsizei, const GLfloat*);
typedef void (APIENTRY *PFNglUniform2fv) (GLint, GLsizei, const GLfloat*);
typedef void (APIENTRY *PFNglUniform3fv) (GLint, GLsizei, const GLfloat*);
typedef void (APIENTRY *PFNglUniformMatrix4fv) (GLint, GLsizei, GLboolean, const GLfloat*);
typedef GLint (APIENTRY *PFNglGetUniformLocation) (GLuint, const GLchar*);
static PFNglCreateShader pglCreateShader;
@ -625,6 +632,7 @@ static PFNglUniform4f pglUniform4f;
static PFNglUniform1fv pglUniform1fv;
static PFNglUniform2fv pglUniform2fv;
static PFNglUniform3fv pglUniform3fv;
static PFNglUniformMatrix4fv pglUniformMatrix4fv;
static PFNglGetUniformLocation pglGetUniformLocation;
// 13062019
@ -659,6 +667,12 @@ typedef enum
gluniform_scr_resolution,
#ifdef HAVE_VR
gluniform_evm, // Eye View Matrix
gluniform_epm, // Eye Projection Matrix
gluniform_hpm, // Head Pose Matrix
#endif
gluniform_max,
} gluniform_t;
@ -740,6 +754,7 @@ void SetupGLFunc4(void)
*(void**)&pglUniform1fv = GetGLFunc("glUniform1fv");
*(void**)&pglUniform2fv = GetGLFunc("glUniform2fv");
*(void**)&pglUniform3fv = GetGLFunc("glUniform3fv");
*(void**)&pglUniformMatrix4fv = GetGLFunc("glUniformMatrix4fv");
*(void**)&pglGetUniformLocation = GetGLFunc("glGetUniformLocation");
#endif
@ -1389,7 +1404,10 @@ void GL_ClearBuffer(FBOOLEAN ColorMask,
if (StencilMask)
ClearMask |= GL_STENCIL_BUFFER_BIT;
GL_SetBlend(DepthMask ? PF_Occlude | CurrentPolyFlags : CurrentPolyFlags&~PF_Occlude);
// A depth clear begins a fresh 3D pass. Do not preserve HUD/UI state such
// as PF_NoDepthTest or translucent blending here, or later transparent
// materials can draw through opaque world geometry in VR multi-pass frames.
GL_SetBlend(DepthMask ? PF_Occlude : CurrentPolyFlags&~PF_Occlude);
pglClear(ClearMask);
pglEnableClientState(GL_VERTEX_ARRAY); // We always use this one
@ -1944,6 +1962,50 @@ static void Shader_SetUniforms(FSurfaceInfo *Surface, GLRGBAFloat *poly, GLRGBAF
UNIFORM_1(shader->uniforms[gluniform_leveltime], shader_leveltime, pglUniform1f);
#ifdef HAVE_VR
if (vr_started)
{
static const GLfloat identityMatrix[16] = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
static const GLfloat zeroMatrix[16] = {
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f
};
const GLfloat *eyeviewmatrix = identityMatrix;
const GLfloat *headposematrix = identityMatrix;
const GLfloat *projectionmatrix = zeroMatrix;
if (!vr_drawing_ui && (vr_render_pass == VR_PASS_3D_LEFT || vr_render_pass == VR_PASS_3D_RIGHT))
{
if (gl_rendering_skybox)
{
eyeviewmatrix = vrEyeSkyboxViewMatrix[vr_current_eye];
headposematrix = (cv_vrposemode.value == 1 || cv_vrposemode.value == 3) ? vrHMDPoseSkyboxMatrix : identityMatrix;
}
else
{
eyeviewmatrix = vrScaledEyeViewMatrix[vr_current_eye];
headposematrix = (cv_vrposemode.value == 1 || cv_vrposemode.value == 3) ? vrHMDScaledPoseMatrix : identityMatrix;
}
projectionmatrix = vrEyeProjMatrix[vr_current_eye];
}
if (shader->uniforms[gluniform_evm] != -1)
pglUniformMatrix4fv(shader->uniforms[gluniform_evm], 1, GL_FALSE, eyeviewmatrix);
if (shader->uniforms[gluniform_epm] != -1)
pglUniformMatrix4fv(shader->uniforms[gluniform_epm], 1, GL_FALSE, projectionmatrix);
if (shader->uniforms[gluniform_hpm] != -1)
pglUniformMatrix4fv(shader->uniforms[gluniform_hpm], 1, GL_FALSE, headposematrix);
}
#endif
UNIFORM_2(shader->uniforms[gluniform_scr_resolution], (GLfloat)vid.width, (GLfloat)vid.height, pglUniform2f);
#undef UNIFORM_1
@ -2082,6 +2144,12 @@ static boolean Shader_CompileProgram(gl_shader_t *shader, GLint i)
// misc. (custom shaders)
shader->uniforms[gluniform_leveltime] = GETUNI("leveltime");
#ifdef HAVE_VR
shader->uniforms[gluniform_evm] = GETUNI("vrEyeMatrix");
shader->uniforms[gluniform_epm] = GETUNI("vrEyeProjection");
shader->uniforms[gluniform_hpm] = GETUNI("vrHeadPoseMatrix");
#endif
#undef GETUNI
// set permanent uniform values

View file

@ -50,6 +50,11 @@
// Thok camera snap (ctrl-f "chalupa")
#include "g_input.h"
#include "m_menu.h" // menustack
#include "screen.h"
#ifdef HAVE_VR
#include "vr/vr_main.h"
#endif
// SRB2kart
#include "m_cond.h" // M_UpdateUnlockablesAndExtraEmblems
@ -192,11 +197,20 @@ fixed_t P_ReturnThrustY(mobj_t *mo, angle_t angle, fixed_t move)
//
boolean P_AutoPause(void)
{
boolean focuspause;
// Don't pause even on menu-up or focus-lost in netgames or record attack
if (netgame || modeattacking || gamestate == GS_TITLESCREEN)
return false;
return ((menustack[0] && !demo.playback) || ( window_notinfocus && cv_pauseifunfocused.value ));
focuspause = (window_notinfocus && cv_pauseifunfocused.value);
#ifdef HAVE_VR
if (vr_started)
focuspause = false;
#endif
return ((menustack[0] && !demo.playback) || focuspause);
}
//
@ -3187,10 +3201,17 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
const INT32 timeovercam = max(0, min(180, (player->karthud[khud_timeovercam] - 2*TICRATE)*15));
camrotate += timeovercam;
}
else if (leveltime < introtime && !(modeattacking && !demo.playback)) // Whoooshy camera! (don't do this in RA when we PLAY, still do it in replays however~)
else if (leveltime < introtime && !(modeattacking && !demo.playback)
#ifdef HAVE_VR
&& (!vr_started || cv_vrtrackintro.value)
#endif
) // Whoooshy camera! (don't do this in RA when we PLAY, still do it in replays however~)
{
const INT32 introcam = (introtime - leveltime);
camrotate += introcam*5;
#ifdef HAVE_VR
if (!vr_started || cv_vrtrackintro.value > 1)
#endif
camrotate += introcam*5;
camdist += (introcam * mapobjectscale)*3;
camheight += (introcam * mapobjectscale)*2;
}

View file

@ -28,6 +28,9 @@
#ifdef HWRENDER
#include "hardware/hw_main.h"
#endif
#ifdef HAVE_VR
#include "vr/vr_main.h"
#endif
// The fraction of a tic being drawn (for interpolation between two tics)
static fixed_t rendertimefrac;
@ -55,6 +58,15 @@ UINT32 R_GetFramerateCap(void)
return TICRATE;
}
#ifdef HAVE_VR
if (vr_started)
{
// In VR, OpenXR xrWaitFrame is the display clock. Returning zero keeps
// the legacy desktop cap from reducing rendering to TICRATE or half-rate.
return 0;
}
#endif
if (cv_fpscap.value == 0)
{
// 0: Match refresh rate

View file

@ -36,6 +36,10 @@
#include "p_local.h" // P_AutoPause()
#include "m_menu.h"
#ifdef HAVE_VR
#include "vr/vr_main.h"
#endif
#ifdef HWRENDER
#include "hardware/hw_main.h"
#include "hardware/hw_glob.h"
@ -72,6 +76,46 @@ static void Highreshudscale_OnChange(void);
static CV_PossibleValue_t highreshudscale_cons_t[] = {{4*FRACUNIT/5, "MIN"}, {6*FRACUNIT/5, "MAX"}, {0, NULL}};
consvar_t cv_highreshudscale = CVAR_INIT ("highreshudscale", "1", CV_SAVE|CV_FLOAT|CV_CALL|CV_NOINIT|CV_NOSHOWHELP, highreshudscale_cons_t, Highreshudscale_OnChange);
static void SCR_ChangeVR(void);
static void SCR_ChangeScaleVR(void);
static void SCR_ChangeResolutionVR(void);
static CV_PossibleValue_t vrviewmode_cons_t[] = {{-1, "Hide"}, {0, "Left Eye"}, {1, "Right Eye"}, {0, NULL}};
consvar_t cv_vrviewmode = CVAR_INIT ("vrviewmode", "0", CV_SAVE, vrviewmode_cons_t, NULL);
static CV_PossibleValue_t vrtrackintro_cons_t[] = {{0, "Off"}, {1, "Gentle"}, {2, "Standard"}, {0, NULL}};
consvar_t cv_vrtrackintro = CVAR_INIT ("vrtrackintro", "1", CV_SAVE, vrtrackintro_cons_t, NULL);
consvar_t cv_vrcomfortmode = CVAR_INIT ("vrcomfortmode", "No", CV_SAVE, CV_YesNo, NULL);
consvar_t cv_vrdisableskystereo = CVAR_INIT ("vrdisableskystereo", "No", CV_SAVE, CV_YesNo, NULL);
consvar_t cv_vrplayerscale = CVAR_INIT ("vrplayerscale", "Yes", CV_SAVE, CV_YesNo, NULL);
consvar_t cv_vrspriterotate = CVAR_INIT ("vrspriterotate", "Yes", CV_SAVE, CV_YesNo, NULL);
consvar_t cv_vruidistance = CVAR_INIT ("vruidistance", "105", CV_SAVE, CV_Unsigned, NULL);
consvar_t cv_vruiscale = CVAR_INIT ("vruiscale", "50", CV_SAVE, CV_Unsigned, NULL);
static CV_PossibleValue_t vruimode_cons_t[] = {{0, "InEye"}, {1, "Quad"}, {0, NULL}};
consvar_t cv_vruimode = CVAR_INIT ("vruimode", "Quad", CV_SAVE, vruimode_cons_t, NULL);
static CV_PossibleValue_t vrposemode_cons_t[] = {
{0, "OpenXRView"},
{1, "OpenVRSplit"},
{2, "OpenXRDebugPose"},
{3, "OpenVRDebugPose"},
{0, NULL}
};
consvar_t cv_vrposemode = CVAR_INIT ("vrposemode", "OpenXRView", CV_SAVE, vrposemode_cons_t, NULL);
static CV_PossibleValue_t vrresolution_cons_t[] = {
{0, "50%"}, {1, "75%"}, {2, "100%"}, {3, "125%"},
{4, "150%"}, {5, "175%"}, {6, "200%"}, {0, NULL}
};
consvar_t cv_vrresolution = CVAR_INIT ("vrresolution", "2", CV_SAVE|CV_NOINIT|CV_CALL, vrresolution_cons_t, SCR_ChangeResolutionVR);
static CV_PossibleValue_t vrscale_cons_t[] = {{0, "Small"}, {1, "Standard"}, {2, "Large"}, {0, NULL}};
consvar_t cv_vrscale = CVAR_INIT ("vrscale", "1", CV_SAVE|CV_NOINIT|CV_CALL, vrscale_cons_t, SCR_ChangeScaleVR);
consvar_t cv_vrenabled = CVAR_INIT ("vrenabled", "Off", CV_SAVE|CV_CALL, CV_OnOff, SCR_ChangeVR);
static void Highreshudscale_OnChange(void)
{
SCR_Recalc();
@ -514,6 +558,40 @@ void SCR_ChangeFullscreen(void)
return;
}
static void SCR_ChangeVR(void)
{
#ifdef HAVE_VR
vr_enabled = (cv_vrenabled.value != 0);
if (!graphics_started)
return;
if (cv_vrenabled.value)
VR_Init();
else
VR_Shutdown();
#else
if (cv_vrenabled.value)
CONS_Alert(CONS_ERROR, "This build does not include OpenXR support.\n");
#endif
}
static void SCR_ChangeScaleVR(void)
{
#ifdef HAVE_VR
if (vr_started)
CONS_Printf("VR: world scale changed; new tracking scale applies next frame.\n");
#endif
}
static void SCR_ChangeResolutionVR(void)
{
#ifdef HAVE_VR
if (vr_started)
CONS_Printf("VR: vrresolution changes require restarting VR so OpenXR swapchains can be recreated.\n");
#endif
}
void SCR_ChangeRenderer(void)
{
if (chosenrendermode != render_none

View file

@ -101,6 +101,9 @@ extern CV_PossibleValue_t cv_renderer_t[];
extern consvar_t cv_scr_width, cv_scr_height, cv_scr_depth, cv_renderview, cv_renderer, cv_renderhitbox, cv_fullscreen;
extern consvar_t cv_highreshudscale;
extern consvar_t cv_vhseffect, cv_shittyscreen, cv_votebgscaling;
extern consvar_t cv_vrviewmode, cv_vrcomfortmode, cv_vrenabled, cv_vrresolution, cv_vrscale;
extern consvar_t cv_vruidistance, cv_vruiscale, cv_vruimode, cv_vrposemode, cv_vrplayerscale, cv_vrspriterotate;
extern consvar_t cv_vrdisableskystereo, cv_vrtrackintro;
extern consvar_t cv_parallelsoftware;
extern consvar_t cv_accuratefps;

View file

@ -1461,6 +1461,7 @@ INT32 I_StartupSystem(void)
#if SDL_VERSION_ATLEAST(2,0,22)
SDL_SetHint(SDL_HINT_APP_NAME, "BlanKart");
#endif
SDL_SetHint("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1");
if (!SDL_Init(0))
I_Error("SRB2Kart: SDL System Error: %s", SDL_GetError()); //Alam: Oh no....
#ifndef NOMUMBLE

View file

@ -54,6 +54,11 @@
#include "../d_main.h"
#include "../s_sound.h"
#include "../i_sound.h" // midi pause/unpause
#ifdef HAVE_VR
#include "../vr/vr_main.h"
#endif
#include "../i_gamepad.h"
#include "../st_stuff.h"
#include "../hu_stuff.h"
@ -545,6 +550,11 @@ static void VID_Command_Mode_f (void)
static void Impl_SetFocused(boolean focused)
{
#ifdef HAVE_VR
if (vr_started)
focused = true;
#endif
window_notinfocus = !focused;
if (window_notinfocus)
@ -1316,51 +1326,60 @@ void I_FinishUpdate(void)
if (rendermode == render_none)
return; //Alam: No software or OpenGl surface
SCR_CalculateFPS();
#ifdef HAVE_VR
const boolean vr_draw_ui = (!vr_started || vr_render_pass == VR_PASS_NONE || vr_render_pass == VR_PASS_UI);
#else
const boolean vr_draw_ui = true;
#endif
if (st_overlay)
if (vr_draw_ui)
{
if (cv_ticrate.value)
SCR_DisplayTicRate();
SCR_CalculateFPS();
if (cv_showping.value && netgame &&
( consoleplayer != serverplayer || (server_lagless == 0 || server_lagless == -1) ))
if (st_overlay)
{
if (server_lagless == 1)
if (cv_ticrate.value)
SCR_DisplayTicRate();
if (cv_showping.value && netgame &&
( consoleplayer != serverplayer || (server_lagless == 0 || server_lagless == -1) ))
{
if (consoleplayer != serverplayer)
SCR_DisplayLocalPing();
}
else
{
for (
player = 1;
player < MAXPLAYERS;
player++
){
if (D_IsPlayerHumanAndGaming(player))
{
if (server_lagless == 1)
{
if (consoleplayer != serverplayer)
SCR_DisplayLocalPing();
break;
}
else
{
for (
player = 1;
player < MAXPLAYERS;
player++
){
if (D_IsPlayerHumanAndGaming(player))
{
SCR_DisplayLocalPing();
break;
}
}
}
}
if (cv_mindelay.value && consoleplayer == serverplayer && Playing())
SCR_DisplayLocalPing();
}
if (cv_mindelay.value && consoleplayer == serverplayer && Playing())
SCR_DisplayLocalPing();
}
if (marathonmode)
SCR_DisplayMarathonInfo();
if (marathonmode)
SCR_DisplayMarathonInfo();
// draw captions if enabled
if (cv_closedcaptioning.value)
SCR_ClosedCaptions();
// draw captions if enabled
if (cv_closedcaptioning.value)
SCR_ClosedCaptions();
#ifdef HAVE_DISCORDRPC
if (discordRequestList != NULL)
ST_AskToJoinEnvelope();
if (discordRequestList != NULL)
ST_AskToJoinEnvelope();
#endif
}
if (rendermode == render_soft && vid.screens[0])
{
@ -1389,7 +1408,12 @@ void I_FinishUpdate(void)
else if (rendermode == render_opengl)
{
// Final postprocess step of palette rendering, after everything else has been drawn.
if (HWR_ShouldUsePaletteRendering())
#ifdef HAVE_VR
const boolean vr_ui_pass = (vr_started && vr_render_pass == VR_PASS_UI);
#else
const boolean vr_ui_pass = false;
#endif
if (!vr_ui_pass && HWR_ShouldUsePaletteRendering())
{
GL_MakeScreenTexture(HWD_SCREENTEXTURE_GENERIC2);
GL_SetShader(HWR_GetShaderFromTarget(SHADER_PALETTE_POSTPROCESS));
@ -1736,7 +1760,12 @@ void I_StartupGraphics(void)
return;
disable_mouse = static_cast<bool>(M_CheckParm("-nomouse"));
disable_fullscreen = M_CheckParm("-win") ? true : false;
#ifdef HAVE_VR
const bool wantVR = (M_CheckParm("-openvr") || M_CheckParm("-vr") || cv_vrenabled.value);
#else
const bool wantVR = false;
#endif
disable_fullscreen = (M_CheckParm("-win") || wantVR) ? true : false;
keyboard_started = true;
@ -1786,6 +1815,8 @@ void I_StartupGraphics(void)
// Choose OpenGL renderer
else if (M_CheckParm("-opengl"))
chosenrendermode = render_opengl;
else if (wantVR)
chosenrendermode = render_opengl;
// Don't startup OpenGL
if (M_CheckParm("-nogl"))
@ -1823,6 +1854,10 @@ void I_StartupGraphics(void)
VID_SetMode(VID_GetModeForSize(vid.width, vid.height));
#ifdef HAVE_VR
VR_Init();
#endif
SDLdoUngrabMouse();
SDL_RaiseWindow(window);
@ -1902,5 +1937,3 @@ void I_SetBorderlessWindow(void)
SDL_SetWindowBordered(window, bordered);
}
#endif

View file

@ -27,6 +27,7 @@
#include "../doomdef.h"
#include "../d_main.h"
#include "../screen.h"
#ifdef HWRENDER
#include "../hardware/r_opengl/r_opengl.h"
@ -171,34 +172,80 @@ boolean OglSdlSurface(INT32 w, INT32 h)
\return void
*/
#ifdef HAVE_VR
#include "../vr/vr_main.h"
#include "../vr/vr_render.h"
#endif
void OglSdlFinishUpdate(boolean waitvbl)
{
static boolean oldwaitvbl = false;
static int oldwaitvbl = -1;
int sdlw, sdlh;
if (oldwaitvbl != waitvbl)
#ifdef HAVE_VR
const boolean effective_waitvbl = (vr_started ? false : waitvbl);
#else
const boolean effective_waitvbl = waitvbl;
#endif
if (oldwaitvbl != (int)effective_waitvbl)
{
SDL_GL_SetSwapInterval(waitvbl);
SDL_GL_SetSwapInterval(effective_waitvbl);
}
oldwaitvbl = waitvbl;
oldwaitvbl = (int)effective_waitvbl;
SDL_GetWindowSizeInPixels(window, &sdlw, &sdlh);
HWR_MakeScreenFinalTexture();
if (gl_shadersavailable)
GL_SetShader(HWR_GetShaderFromTarget(SHADER_FINAL_POST_PROCESS));
HWR_DrawScreenFinalTexture(sdlw, sdlh);
if (gl_shadersavailable)
GL_UnSetShader();
SDL_GL_SwapWindow(window);
#ifdef HAVE_VR
if (vr_started && vr_render_pass != VR_PASS_NONE)
{
if (vr_render_pass == VR_PASS_UI)
return;
GL_GClipRect(0, 0, vid.width, vid.height, NZCLIP_PLANE);
if (cv_vruimode.value == 0)
{
if (vr_current_eye == cv_vrviewmode.value)
{
if (VR_MirrorEyeToDefaultFramebuffer(sdlw, sdlh))
SDL_GL_SwapWindow(window);
}
// Sryder: We need to draw the final screen texture again into the other buffer in the original position so that
// effects that want to take the old screen can do so after this
// Generic2 has the screen image without palette rendering brightness adjustments.
// Using that here will prevent brightness adjustments being applied twice.
GL_DrawScreenTexture(HWD_SCREENTEXTURE_GENERIC2, NULL, 0);
return;
}
// Optional desktop mirror of one eye. Use an FBO blit so the mirror
// does not copy a 4096x4096 screen texture or write back into the
// headset swapchain.
if (vr_current_eye == cv_vrviewmode.value)
{
if (VR_MirrorEyeToDefaultFramebuffer(sdlw, sdlh))
SDL_GL_SwapWindow(window);
}
return;
}
else
#endif
{
HWR_MakeScreenFinalTexture();
if (gl_shadersavailable)
GL_SetShader(HWR_GetShaderFromTarget(SHADER_FINAL_POST_PROCESS));
HWR_DrawScreenFinalTexture(sdlw, sdlh);
if (gl_shadersavailable)
GL_UnSetShader();
SDL_GL_SwapWindow(window);
GL_GClipRect(0, 0, vid.width, vid.height, NZCLIP_PLANE);
// Sryder: We need to draw the final screen texture again into the other buffer in the original position so that
// effects that want to take the old screen can do so after this
// Generic2 has the screen image without palette rendering brightness adjustments.
// Using that here will prevent brightness adjustments being applied twice.
if (gl_shadersavailable)
GL_SetShader(HWR_GetShaderFromTarget(SHADER_FINAL_POST_PROCESS));
HWR_DrawScreenFinalTexture(sdlw, sdlh);
if (gl_shadersavailable)
GL_UnSetShader();
}
}
#endif //HWRENDER

601
src/vr/vr_main.c Normal file
View file

@ -0,0 +1,601 @@
#include "vr_main.h"
#ifdef HAVE_VR
#include "vr_render.h"
#include "vr_math.h"
#include "../m_argv.h"
#include "../console.h"
#include "../screen.h"
#include "../i_video.h"
#include "../r_fps.h"
#include "../v_video.h"
#include <stdbool.h>
#include <SDL3/SDL.h>
#include "../sdl/sdlmain.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#ifdef __linux__
#include <GL/glx.h>
#endif
int vr_current_eye = 0;
boolean vr_started = false;
boolean vr_enabled = false;
boolean vr_drawing_ui = false;
vr_render_pass_t vr_render_pass = VR_PASS_NONE;
XrInstance vr_instance = XR_NULL_HANDLE;
XrSession vr_session = XR_NULL_HANDLE;
XrSystemId vr_system_id = XR_NULL_SYSTEM_ID;
XrSpace vr_local_space = XR_NULL_HANDLE;
XrSpace vr_view_space = XR_NULL_HANDLE;
float vrHMDPoseMatrix[16];
float vrHMDScaledPoseMatrix[16];
float vrHMDPoseSkyboxMatrix[16];
float vrEyeViewMatrix[2][16];
float vrScaledEyeViewMatrix[2][16];
float vrEyeSkyboxViewMatrix[2][16];
float vrEyeProjMatrix[2][16];
int vrWorldScale[3] = {400, 250, 100};
float vrPlayerScale = 1.0f;
uint32_t vr_render_width = 0;
uint32_t vr_render_height = 0;
float* vrVisibleAreaVertices[2] = {NULL, NULL};
float* vrVisibleAreaUVs[2] = {NULL, NULL};
uint32_t vrVisibleAreaVertexCount[2] = {0, 0};
static PFN_xrGetOpenGLGraphicsRequirementsKHR pfnGetOpenGLGraphicsRequirementsKHR = NULL;
static PFN_xrGetVisibilityMaskKHR pfnGetVisibilityMaskKHR = NULL;
static PFN_xrEnumerateDisplayRefreshRatesFB pfnEnumerateDisplayRefreshRatesFB = NULL;
static PFN_xrGetDisplayRefreshRateFB pfnGetDisplayRefreshRateFB = NULL;
static PFN_xrRequestDisplayRefreshRateFB pfnRequestDisplayRefreshRateFB = NULL;
static boolean displayRefreshRateExtensionEnabled = false;
static float VR_RenderScaleForMode(int mode)
{
static const float multipliers[] = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
if (mode < 0)
mode = 0;
else if (mode > 6)
mode = 6;
return multipliers[mode];
}
static int VR_StartupResolutionMode(void)
{
int mode = cv_vrresolution.string ? cv_vrresolution.value : 2;
if (M_CheckParm("+vrresolution") && M_IsNextParm()) {
const char* value = M_GetNextParm();
if (!strcasecmp(value, "50%"))
mode = 0;
else if (!strcasecmp(value, "75%"))
mode = 1;
else if (!strcasecmp(value, "100%"))
mode = 2;
else if (!strcasecmp(value, "125%"))
mode = 3;
else if (!strcasecmp(value, "150%"))
mode = 4;
else if (!strcasecmp(value, "175%"))
mode = 5;
else if (!strcasecmp(value, "200%"))
mode = 6;
else
mode = atoi(value);
}
if (mode < 0)
mode = 0;
else if (mode > 6)
mode = 6;
return mode;
}
static boolean VR_RuntimeExtensionSupported(const char* extensionName)
{
uint32_t extensionCount = 0;
XrResult res = xrEnumerateInstanceExtensionProperties(NULL, 0, &extensionCount, NULL);
if (XR_FAILED(res) || extensionCount == 0)
return false;
XrExtensionProperties* extensions =
(XrExtensionProperties*)malloc(extensionCount * sizeof(XrExtensionProperties));
if (!extensions)
return false;
for (uint32_t i = 0; i < extensionCount; i++) {
extensions[i].type = XR_TYPE_EXTENSION_PROPERTIES;
extensions[i].next = NULL;
}
res = xrEnumerateInstanceExtensionProperties(NULL, extensionCount, &extensionCount, extensions);
if (XR_FAILED(res)) {
free(extensions);
return false;
}
for (uint32_t i = 0; i < extensionCount; i++) {
if (strcmp(extensions[i].extensionName, extensionName) == 0) {
free(extensions);
return true;
}
}
free(extensions);
return false;
}
static void VR_ConfigureDisplayRefreshRate(void)
{
if (!displayRefreshRateExtensionEnabled || !pfnGetDisplayRefreshRateFB)
return;
float currentRefreshRate = 0.0f;
if (XR_SUCCEEDED(pfnGetDisplayRefreshRateFB(vr_session, &currentRefreshRate)) &&
currentRefreshRate > 0.0f) {
CONS_Printf("VR: Runtime display refresh rate is %.2f Hz.\n", currentRefreshRate);
if (pfnRequestDisplayRefreshRateFB) {
XrResult res = pfnRequestDisplayRefreshRateFB(vr_session, currentRefreshRate);
if (XR_SUCCEEDED(res))
CONS_Printf("VR: Requested runtime refresh pacing at %.2f Hz.\n", currentRefreshRate);
}
}
if (!pfnEnumerateDisplayRefreshRatesFB)
return;
uint32_t refreshRateCount = 0;
if (XR_FAILED(pfnEnumerateDisplayRefreshRatesFB(vr_session, 0, &refreshRateCount, NULL)) ||
refreshRateCount == 0)
return;
float* refreshRates = (float*)malloc(refreshRateCount * sizeof(float));
if (!refreshRates)
return;
if (XR_SUCCEEDED(pfnEnumerateDisplayRefreshRatesFB(vr_session, refreshRateCount, &refreshRateCount, refreshRates))) {
char buffer[256];
size_t used = 0;
buffer[0] = '\0';
for (uint32_t i = 0; i < refreshRateCount && used < sizeof(buffer); i++) {
int written = snprintf(buffer + used, sizeof(buffer) - used,
"%s%.2f", i ? ", " : "", refreshRates[i]);
if (written < 0)
break;
used += (size_t)written;
}
CONS_Printf("VR: Runtime supported refresh rates: %s Hz.\n", buffer);
}
free(refreshRates);
}
static void openxr_process_visibility_mesh(int eye, uint32_t width, uint32_t height)
{
if (!pfnGetVisibilityMaskKHR) return;
XrVisibilityMaskKHR mask = {XR_TYPE_VISIBILITY_MASK_KHR};
XrResult res = pfnGetVisibilityMaskKHR(vr_session, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, eye, XR_VISIBILITY_MASK_TYPE_HIDDEN_TRIANGLE_MESH_KHR, &mask);
if (XR_SUCCEEDED(res) && mask.vertexCapacityInput == 0) {
mask.vertexCapacityInput = mask.vertexCountOutput;
mask.indexCapacityInput = mask.indexCountOutput;
mask.vertices = (XrVector2f*)malloc(mask.vertexCapacityInput * sizeof(XrVector2f));
mask.indices = (uint32_t*)malloc(mask.indexCapacityInput * sizeof(uint32_t));
res = pfnGetVisibilityMaskKHR(vr_session, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, eye, XR_VISIBILITY_MASK_TYPE_HIDDEN_TRIANGLE_MESH_KHR, &mask);
if (XR_SUCCEEDED(res) && mask.indexCountOutput > 0) {
vrVisibleAreaVertexCount[eye] = mask.indexCountOutput;
float* vertex = vrVisibleAreaVertices[eye] = (float*)malloc(mask.indexCountOutput * 3 * sizeof(float));
float* uv = vrVisibleAreaUVs[eye] = (float*)malloc(mask.indexCountOutput * 2 * sizeof(float));
uint32_t texsize = 512;
while (texsize < width || texsize < height) texsize <<= 1;
float xfix = 1.0f / ((float)texsize / (float)width);
float yfix = 1.0f / ((float)texsize / (float)height);
for (uint32_t i = 0; i < mask.indexCountOutput; i++) {
uint32_t idx = mask.indices[i];
vertex[i*3] = (mask.vertices[idx].x - 0.5f) * 2.0f;
vertex[i*3+1] = (mask.vertices[idx].y - 0.5f) * 2.0f;
vertex[i*3+2] = 1.0f;
uv[i*2] = mask.vertices[idx].x * xfix;
uv[i*2+1] = mask.vertices[idx].y * yfix;
}
}
free(mask.vertices);
free(mask.indices);
}
}
boolean VR_Init(void)
{
if (vr_started) return true;
// Check for explicit VR command-line flags
boolean wantVR = (M_CheckParm("-openvr") || M_CheckParm("-vr") || vr_enabled);
if (!wantVR)
{
CONS_Printf("VR: No -vr flag detected, skipping VR initialization.\n");
CONS_Printf("VR: Use '-vr' command-line flag to enable OpenXR.\n");
return false;
}
CONS_Printf("VR: Initializing OpenXR...\n");
XrResult res;
const boolean visibilityMaskSupported =
VR_RuntimeExtensionSupported(XR_KHR_VISIBILITY_MASK_EXTENSION_NAME);
const boolean displayRefreshRateSupported =
VR_RuntimeExtensionSupported(XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
const char* enabledExtensions[3];
uint32_t enabledExtensionCount = 0;
enabledExtensions[enabledExtensionCount++] = XR_KHR_OPENGL_ENABLE_EXTENSION_NAME;
if (visibilityMaskSupported)
enabledExtensions[enabledExtensionCount++] = XR_KHR_VISIBILITY_MASK_EXTENSION_NAME;
if (displayRefreshRateSupported)
enabledExtensions[enabledExtensionCount++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
createInfo.next = NULL;
strncpy(createInfo.applicationInfo.applicationName, "SRB2Kart Blankart", XR_MAX_APPLICATION_NAME_SIZE);
createInfo.applicationInfo.applicationVersion = 1;
strncpy(createInfo.applicationInfo.engineName, "SRB2Kart", XR_MAX_ENGINE_NAME_SIZE);
createInfo.applicationInfo.engineVersion = 1;
createInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION;
createInfo.enabledExtensionCount = enabledExtensionCount;
createInfo.enabledExtensionNames = enabledExtensions;
createInfo.createFlags = 0;
createInfo.enabledApiLayerCount = 0;
createInfo.enabledApiLayerNames = NULL;
res = xrCreateInstance(&createInfo, &vr_instance);
if (XR_FAILED(res)) {
CONS_Printf("VR: xrCreateInstance with optional extensions failed (XrResult=%d), retrying with OpenGL only...\n", (int)res);
createInfo.enabledExtensionCount = 1;
createInfo.enabledExtensionNames = enabledExtensions;
displayRefreshRateExtensionEnabled = false;
res = xrCreateInstance(&createInfo, &vr_instance);
} else {
displayRefreshRateExtensionEnabled = displayRefreshRateSupported;
}
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to create OpenXR instance (XrResult=%d).\n", (int)res);
CONS_Printf("VR: Is an OpenXR runtime (Monado, SteamVR, etc.) installed and active?\n");
CONS_Printf("VR: Check that XR_RUNTIME_JSON is set or /etc/xdg/openxr/1/active_runtime.json exists.\n");
return false;
}
CONS_Printf("VR: OpenXR instance created successfully.\n");
// Print runtime info
XrInstanceProperties instanceProps = {XR_TYPE_INSTANCE_PROPERTIES};
instanceProps.next = NULL;
if (XR_SUCCEEDED(xrGetInstanceProperties(vr_instance, &instanceProps))) {
CONS_Printf("VR: Runtime: %s (version %u.%u.%u)\n",
instanceProps.runtimeName,
XR_VERSION_MAJOR(instanceProps.runtimeVersion),
XR_VERSION_MINOR(instanceProps.runtimeVersion),
XR_VERSION_PATCH(instanceProps.runtimeVersion));
}
xrGetInstanceProcAddr(vr_instance, "xrGetOpenGLGraphicsRequirementsKHR", (PFN_xrVoidFunction*)&pfnGetOpenGLGraphicsRequirementsKHR);
xrGetInstanceProcAddr(vr_instance, "xrGetVisibilityMaskKHR", (PFN_xrVoidFunction*)&pfnGetVisibilityMaskKHR);
if (displayRefreshRateExtensionEnabled) {
xrGetInstanceProcAddr(vr_instance, "xrEnumerateDisplayRefreshRatesFB", (PFN_xrVoidFunction*)&pfnEnumerateDisplayRefreshRatesFB);
xrGetInstanceProcAddr(vr_instance, "xrGetDisplayRefreshRateFB", (PFN_xrVoidFunction*)&pfnGetDisplayRefreshRateFB);
xrGetInstanceProcAddr(vr_instance, "xrRequestDisplayRefreshRateFB", (PFN_xrVoidFunction*)&pfnRequestDisplayRefreshRateFB);
}
XrSystemGetInfo systemInfo = {XR_TYPE_SYSTEM_GET_INFO};
systemInfo.next = NULL;
systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
res = xrGetSystem(vr_instance, &systemInfo, &vr_system_id);
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to get OpenXR system (XrResult=%d). Is a headset connected?\n", (int)res);
xrDestroyInstance(vr_instance);
vr_instance = XR_NULL_HANDLE;
return false;
}
CONS_Printf("VR: OpenXR system acquired (systemId=%llu).\n", (unsigned long long)vr_system_id);
if (pfnGetOpenGLGraphicsRequirementsKHR) {
XrGraphicsRequirementsOpenGLKHR graphicsRequirements = {XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR};
graphicsRequirements.next = NULL;
res = pfnGetOpenGLGraphicsRequirementsKHR(vr_instance, vr_system_id, &graphicsRequirements);
if (XR_SUCCEEDED(res)) {
CONS_Printf("VR: OpenGL requirements: min=%u.%u max=%u.%u\n",
XR_VERSION_MAJOR(graphicsRequirements.minApiVersionSupported),
XR_VERSION_MINOR(graphicsRequirements.minApiVersionSupported),
XR_VERSION_MAJOR(graphicsRequirements.maxApiVersionSupported),
XR_VERSION_MINOR(graphicsRequirements.maxApiVersionSupported));
}
}
// Query the runtime's recommended per-eye resolution before creating the
// OpenXR session, then resize Blankart's render surface to match it.
uint32_t viewCount = 0;
xrEnumerateViewConfigurationViews(vr_instance, vr_system_id, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, &viewCount, NULL);
if (viewCount < 2) {
CONS_Printf("VR: ERROR - Expected 2 views for stereo, got %u.\n", viewCount);
VR_Shutdown();
return false;
}
XrViewConfigurationView* configViews = (XrViewConfigurationView*)malloc(viewCount * sizeof(XrViewConfigurationView));
for (uint32_t i = 0; i < viewCount; i++) {
configViews[i].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
configViews[i].next = NULL;
}
xrEnumerateViewConfigurationViews(vr_instance, vr_system_id, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, viewCount, &viewCount, configViews);
const uint32_t recommendedWidth = configViews[0].recommendedImageRectWidth;
const uint32_t recommendedHeight = configViews[0].recommendedImageRectHeight;
const uint32_t maxWidth = configViews[0].maxImageRectWidth;
const uint32_t maxHeight = configViews[0].maxImageRectHeight;
const int resolutionMode = VR_StartupResolutionMode();
const float renderScale = VR_RenderScaleForMode(resolutionMode);
vr_render_width = (uint32_t)((float)recommendedWidth * renderScale + 0.5f);
vr_render_height = (uint32_t)((float)recommendedHeight * renderScale + 0.5f);
if (maxWidth > 0 && vr_render_width > maxWidth)
vr_render_width = maxWidth;
if (maxHeight > 0 && vr_render_height > maxHeight)
vr_render_height = maxHeight;
if (vr_render_width < 1)
vr_render_width = 1;
if (vr_render_height < 1)
vr_render_height = 1;
CONS_Printf("VR: Recommended render size: %ux%u (max: %ux%u); using %ux%u (%d%%).\n",
recommendedWidth, recommendedHeight, maxWidth, maxHeight,
vr_render_width, vr_render_height, (int)(renderScale * 100.0f + 0.5f));
free(configViews);
if ((uint32_t)vid.width != vr_render_width || (uint32_t)vid.height != vr_render_height) {
CONS_Printf("VR: Setting game render size to OpenXR eye size %ux%u.\n", vr_render_width, vr_render_height);
VID_SetMode(VID_GetModeForSize((INT32)vr_render_width, (INT32)vr_render_height));
}
SDL_PropertiesID props = SDL_GetWindowProperties(window);
if (!props) {
CONS_Printf("VR: Failed to get SDL window properties!\n");
xrDestroyInstance(vr_instance);
vr_instance = XR_NULL_HANDLE;
return false;
}
#if defined(_WIN32)
XrGraphicsBindingOpenGLWin32KHR graphicsBinding = {XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR};
graphicsBinding.next = NULL;
graphicsBinding.hDC = (HDC)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HDC_POINTER, NULL);
graphicsBinding.hGLRC = (HGLRC)SDL_GL_GetCurrentContext();
CONS_Printf("VR: Win32 graphics binding: hDC=%p, hGLRC=%p\n", (void*)graphicsBinding.hDC, (void*)graphicsBinding.hGLRC);
#elif defined(__linux__)
XrGraphicsBindingOpenGLXlibKHR graphicsBinding = {XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR};
graphicsBinding.next = NULL;
graphicsBinding.xDisplay = (Display*)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
// Get the GLX context and drawable from the current context
graphicsBinding.glxContext = glXGetCurrentContext();
graphicsBinding.glxDrawable = glXGetCurrentDrawable();
// Get the visual ID and FB config from the current context
if (graphicsBinding.xDisplay && graphicsBinding.glxContext) {
// Query the FB config from the current context
int fbConfigId = 0;
glXQueryContext(graphicsBinding.xDisplay, graphicsBinding.glxContext, GLX_FBCONFIG_ID, &fbConfigId);
// Find the matching FB config
int numConfigs = 0;
int attribs[] = { GLX_FBCONFIG_ID, fbConfigId, None };
GLXFBConfig* configs = glXChooseFBConfig(graphicsBinding.xDisplay,
DefaultScreen(graphicsBinding.xDisplay), attribs, &numConfigs);
if (configs && numConfigs > 0) {
graphicsBinding.glxFBConfig = configs[0];
// Get visual ID from the FB config
XVisualInfo* vi = glXGetVisualFromFBConfig(graphicsBinding.xDisplay, configs[0]);
if (vi) {
graphicsBinding.visualid = vi->visualid;
XFree(vi);
}
XFree(configs);
} else {
CONS_Printf("VR: WARNING - Could not find matching GLX FB config.\n");
graphicsBinding.glxFBConfig = NULL;
graphicsBinding.visualid = 0;
}
}
CONS_Printf("VR: Xlib graphics binding: display=%p, context=%p, drawable=%lu, visualid=%lu\n",
(void*)graphicsBinding.xDisplay,
(void*)graphicsBinding.glxContext,
(unsigned long)graphicsBinding.glxDrawable,
(unsigned long)graphicsBinding.visualid);
if (!graphicsBinding.xDisplay || !graphicsBinding.glxContext) {
CONS_Printf("VR: ERROR - X11 display or GLX context is NULL! Is the game running under X11?\n");
CONS_Printf("VR: Wayland-native is not yet supported; try running with SDL_VIDEODRIVER=x11\n");
xrDestroyInstance(vr_instance);
vr_instance = XR_NULL_HANDLE;
return false;
}
#endif
XrSessionCreateInfo sessionCreateInfo = {XR_TYPE_SESSION_CREATE_INFO};
sessionCreateInfo.next = &graphicsBinding;
sessionCreateInfo.systemId = vr_system_id;
sessionCreateInfo.createFlags = 0;
res = xrCreateSession(vr_instance, &sessionCreateInfo, &vr_session);
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to create OpenXR session (XrResult=%d).\n", (int)res);
xrDestroyInstance(vr_instance);
vr_instance = XR_NULL_HANDLE;
return false;
}
CONS_Printf("VR: OpenXR session created successfully.\n");
// Begin the session
XrSessionBeginInfo beginInfo = {XR_TYPE_SESSION_BEGIN_INFO};
beginInfo.next = NULL;
beginInfo.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
res = xrBeginSession(vr_session, &beginInfo);
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to begin OpenXR session (XrResult=%d).\n", (int)res);
xrDestroySession(vr_session);
vr_session = XR_NULL_HANDLE;
xrDestroyInstance(vr_instance);
vr_instance = XR_NULL_HANDLE;
return false;
}
CONS_Printf("VR: OpenXR session begun.\n");
VR_ConfigureDisplayRefreshRate();
XrReferenceSpaceCreateInfo spaceCreateInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
spaceCreateInfo.next = NULL;
spaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
spaceCreateInfo.poseInReferenceSpace.orientation.w = 1.0f;
spaceCreateInfo.poseInReferenceSpace.orientation.x = 0.0f;
spaceCreateInfo.poseInReferenceSpace.orientation.y = 0.0f;
spaceCreateInfo.poseInReferenceSpace.orientation.z = 0.0f;
spaceCreateInfo.poseInReferenceSpace.position.x = 0.0f;
spaceCreateInfo.poseInReferenceSpace.position.y = 0.0f;
spaceCreateInfo.poseInReferenceSpace.position.z = 0.0f;
res = xrCreateReferenceSpace(vr_session, &spaceCreateInfo, &vr_local_space);
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to create local reference space.\n");
VR_Shutdown();
return false;
}
spaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
res = xrCreateReferenceSpace(vr_session, &spaceCreateInfo, &vr_view_space);
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to create view reference space.\n");
VR_Shutdown();
return false;
}
if (!VR_InitSwapchains()) {
CONS_Printf("VR: Failed to initialize swapchains!\n");
VR_Shutdown();
return false;
}
CONS_Printf("VR: Swapchains initialized.\n");
if (pfnGetVisibilityMaskKHR) {
openxr_process_visibility_mesh(0, vr_render_width, vr_render_height);
openxr_process_visibility_mesh(1, vr_render_width, vr_render_height);
CONS_Printf("VR: Visibility masks processed.\n");
} else {
CONS_Printf("VR: Visibility mask extension not available (cosmetic only).\n");
}
vr_started = true;
vr_enabled = true;
CV_StealthSet(&cv_vidwait, "Off");
CV_StealthSetValue(&cv_fpscap, -1);
CV_StealthSet(&cv_ticrate, "Compact");
CONS_Printf("VR: Desktop vsync disabled and FPS cap set to Unlimited; OpenXR xrWaitFrame will pace rendering.\n");
CONS_Printf("VR: OpenXR initialization complete! VR is now active.\n");
return true;
}
void VR_Shutdown(void)
{
if (!vr_started && vr_instance == XR_NULL_HANDLE) return;
if (vrVisibleAreaVertices[0]) { free(vrVisibleAreaVertices[0]); vrVisibleAreaVertices[0] = NULL; }
if (vrVisibleAreaVertices[1]) { free(vrVisibleAreaVertices[1]); vrVisibleAreaVertices[1] = NULL; }
if (vrVisibleAreaUVs[0]) { free(vrVisibleAreaUVs[0]); vrVisibleAreaUVs[0] = NULL; }
if (vrVisibleAreaUVs[1]) { free(vrVisibleAreaUVs[1]); vrVisibleAreaUVs[1] = NULL; }
VR_DestroySwapchains();
if (vr_local_space != XR_NULL_HANDLE) xrDestroySpace(vr_local_space);
if (vr_view_space != XR_NULL_HANDLE) xrDestroySpace(vr_view_space);
if (vr_session != XR_NULL_HANDLE) xrDestroySession(vr_session);
if (vr_instance != XR_NULL_HANDLE) xrDestroyInstance(vr_instance);
vr_local_space = XR_NULL_HANDLE;
vr_view_space = XR_NULL_HANDLE;
vr_session = XR_NULL_HANDLE;
vr_instance = XR_NULL_HANDLE;
vr_started = false;
vr_enabled = false;
vr_drawing_ui = false;
displayRefreshRateExtensionEnabled = false;
pfnEnumerateDisplayRefreshRatesFB = NULL;
pfnGetDisplayRefreshRateFB = NULL;
pfnRequestDisplayRefreshRateFB = NULL;
}
void VR_ScaleViewMatrices(float player_scale, int skybox_scale)
{
memcpy(vrHMDScaledPoseMatrix, vrHMDPoseMatrix, sizeof(float) * 16);
vrHMDScaledPoseMatrix[12] *= player_scale;
vrHMDScaledPoseMatrix[13] *= player_scale;
vrHMDScaledPoseMatrix[14] *= player_scale;
memcpy(vrScaledEyeViewMatrix[0], vrEyeViewMatrix[0], sizeof(float) * 16);
memcpy(vrScaledEyeViewMatrix[1], vrEyeViewMatrix[1], sizeof(float) * 16);
vrScaledEyeViewMatrix[0][12] *= player_scale;
vrScaledEyeViewMatrix[0][13] *= player_scale;
vrScaledEyeViewMatrix[0][14] *= player_scale;
vrScaledEyeViewMatrix[1][12] *= player_scale;
vrScaledEyeViewMatrix[1][13] *= player_scale;
vrScaledEyeViewMatrix[1][14] *= player_scale;
vrPlayerScale = player_scale;
memcpy(vrEyeSkyboxViewMatrix[0], vrScaledEyeViewMatrix[0], sizeof(float) * 16);
memcpy(vrEyeSkyboxViewMatrix[1], vrScaledEyeViewMatrix[1], sizeof(float) * 16);
if(skybox_scale > 0)
{
vrEyeSkyboxViewMatrix[0][12] /= skybox_scale;
vrEyeSkyboxViewMatrix[0][13] /= skybox_scale;
vrEyeSkyboxViewMatrix[0][14] /= skybox_scale;
vrEyeSkyboxViewMatrix[1][12] /= skybox_scale;
vrEyeSkyboxViewMatrix[1][13] /= skybox_scale;
vrEyeSkyboxViewMatrix[1][14] /= skybox_scale;
}
else
{
vrEyeSkyboxViewMatrix[0][12] =
vrEyeSkyboxViewMatrix[0][13] =
vrEyeSkyboxViewMatrix[0][14] =
vrEyeSkyboxViewMatrix[1][12] =
vrEyeSkyboxViewMatrix[1][13] =
vrEyeSkyboxViewMatrix[1][14] = 0;
}
}
#endif // HAVE_VR

76
src/vr/vr_main.h Normal file
View file

@ -0,0 +1,76 @@
#ifndef __VR_MAIN_H__
#define __VR_MAIN_H__
#ifdef HAVE_VR
#include "../doomdef.h"
#if defined(_WIN32)
#define XR_USE_PLATFORM_WIN32
#define XR_USE_GRAPHICS_API_OPENGL
#include <windows.h>
#elif defined(__linux__)
#define XR_USE_PLATFORM_XLIB
#define XR_USE_GRAPHICS_API_OPENGL
#include <X11/Xlib.h>
#include <GL/glx.h>
#endif
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#ifdef __cplusplus
extern "C" {
#endif
// Global state
extern int vr_current_eye;
extern boolean vr_started;
extern boolean vr_enabled;
extern boolean vr_drawing_ui;
typedef enum {
VR_PASS_NONE,
VR_PASS_3D_LEFT,
VR_PASS_3D_RIGHT,
VR_PASS_UI
} vr_render_pass_t;
extern vr_render_pass_t vr_render_pass;
extern XrInstance vr_instance;
extern XrSession vr_session;
extern XrSystemId vr_system_id;
extern XrSpace vr_local_space;
extern XrSpace vr_view_space;
// Global matrices (size 16)
extern float vrHMDPoseMatrix[16];
extern float vrHMDScaledPoseMatrix[16];
extern float vrHMDPoseSkyboxMatrix[16];
extern float vrEyeViewMatrix[2][16];
extern float vrScaledEyeViewMatrix[2][16];
extern float vrEyeSkyboxViewMatrix[2][16];
extern float vrEyeProjMatrix[2][16];
extern int vrWorldScale[3];
extern float vrPlayerScale;
extern uint32_t vr_render_width;
extern uint32_t vr_render_height;
// Visibility mask
extern float* vrVisibleAreaVertices[2];
extern float* vrVisibleAreaUVs[2];
extern uint32_t vrVisibleAreaVertexCount[2];
// Lifecycle
boolean VR_Init(void);
void VR_Shutdown(void);
void VR_ScaleViewMatrices(float player_scale, int skybox_scale);
#ifdef __cplusplus
}
#endif
#endif // HAVE_VR
#endif // __VR_MAIN_H__

126
src/vr/vr_math.c Normal file
View file

@ -0,0 +1,126 @@
#include "vr_math.h"
#ifdef HAVE_VR
#include <math.h>
void VR_MatrixInv(float* a, const float* b)
{
float x, y, z;
// transpose of rotation matrix
a[ 0] = b[ 0];
a[ 5] = b[ 5];
a[10] = b[10];
x=b[1]; a[1]=b[4]; a[4]=x;
x=b[2]; a[2]=b[8]; a[8]=x;
x=b[6]; a[6]=b[9]; a[9]=x;
// copy projection part
a[ 3] = b[ 3];
a[ 7] = b[ 7];
a[11] = b[11];
a[15] = b[15];
// convert origin: new_pos = - new_rotation_matrix * old_pos
x = (a[ 0]*b[12]) + (a[ 4]*b[13]) + (a[ 8]*b[14]);
y = (a[ 1]*b[12]) + (a[ 5]*b[13]) + (a[ 9]*b[14]);
z = (a[ 2]*b[12]) + (a[ 6]*b[13]) + (a[10]*b[14]);
a[12] = -x;
a[13] = -y;
a[14] = -z;
}
void VR_MatrixMultiply(float* out, const float* a, const float* b)
{
float r[16];
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 4; row++) {
r[row + col * 4] =
a[row + 0 * 4] * b[0 + col * 4] +
a[row + 1 * 4] * b[1 + col * 4] +
a[row + 2 * 4] * b[2 + col * 4] +
a[row + 3 * 4] * b[3 + col * 4];
}
}
for (int i = 0; i < 16; i++)
out[i] = r[i];
}
void VR_RotateVecByMat(const float* m, float* v)
{
float x = (m[0]*v[0]) + (m[4]*v[1]) + (m[ 8]*v[2]); // Note: corrected from original openvr_common.c
float y = (m[1]*v[0]) + (m[5]*v[1]) + (m[ 9]*v[2]); // which used v[0] in all y places etc.
float z = (m[2]*v[0]) + (m[6]*v[1]) + (m[10]*v[2]); // Wait, I should check how it was exactly.
v[0] = x;
v[1] = y;
v[2] = z;
}
void VR_PoseToMatrix(const XrPosef* pose, float* glMatrix)
{
// Convert quaternion to 3x3 rotation matrix
float xx = pose->orientation.x * pose->orientation.x;
float yy = pose->orientation.y * pose->orientation.y;
float zz = pose->orientation.z * pose->orientation.z;
float xy = pose->orientation.x * pose->orientation.y;
float xz = pose->orientation.x * pose->orientation.z;
float yz = pose->orientation.y * pose->orientation.z;
float wx = pose->orientation.w * pose->orientation.x;
float wy = pose->orientation.w * pose->orientation.y;
float wz = pose->orientation.w * pose->orientation.z;
glMatrix[ 0] = 1.0f - 2.0f * (yy + zz);
glMatrix[ 1] = 2.0f * (xy + wz);
glMatrix[ 2] = 2.0f * (xz - wy);
glMatrix[ 3] = 0.0f;
glMatrix[ 4] = 2.0f * (xy - wz);
glMatrix[ 5] = 1.0f - 2.0f * (xx + zz);
glMatrix[ 6] = 2.0f * (yz + wx);
glMatrix[ 7] = 0.0f;
glMatrix[ 8] = 2.0f * (xz + wy);
glMatrix[ 9] = 2.0f * (yz - wx);
glMatrix[10] = 1.0f - 2.0f * (xx + yy);
glMatrix[11] = 0.0f;
glMatrix[12] = pose->position.x;
glMatrix[13] = pose->position.y;
glMatrix[14] = pose->position.z;
glMatrix[15] = 1.0f;
}
void VR_FovToProjection(const XrFovf* fov, float nearZ, float farZ, float* glMatrix)
{
float tanLeft = tanf(fov->angleLeft);
float tanRight = tanf(fov->angleRight);
float tanDown = tanf(fov->angleDown);
float tanUp = tanf(fov->angleUp);
float tanWidth = tanRight - tanLeft;
float tanHeight = tanUp - tanDown;
glMatrix[ 0] = 2.0f / tanWidth;
glMatrix[ 1] = 0.0f;
glMatrix[ 2] = 0.0f;
glMatrix[ 3] = 0.0f;
glMatrix[ 4] = 0.0f;
glMatrix[ 5] = 2.0f / tanHeight;
glMatrix[ 6] = 0.0f;
glMatrix[ 7] = 0.0f;
glMatrix[ 8] = (tanRight + tanLeft) / tanWidth;
glMatrix[ 9] = (tanUp + tanDown) / tanHeight;
glMatrix[10] = -(farZ + nearZ) / (farZ - nearZ);
glMatrix[11] = -1.0f;
glMatrix[12] = 0.0f;
glMatrix[13] = 0.0f;
glMatrix[14] = -(2.0f * farZ * nearZ) / (farZ - nearZ);
glMatrix[15] = 0.0f;
}
#endif // HAVE_VR

33
src/vr/vr_math.h Normal file
View file

@ -0,0 +1,33 @@
#ifndef __VR_MATH_H__
#define __VR_MATH_H__
#ifdef HAVE_VR
#include <openxr/openxr.h>
#ifdef __cplusplus
extern "C" {
#endif
// Invert a 4x4 matrix (assumes it's a rigid body transform: rotation + translation)
// safe to use same pointer for a and b (a = inv(b))
void VR_MatrixInv(float* a, const float* b);
// Multiply two OpenGL 4x4 matrices (column-major): out = a * b
void VR_MatrixMultiply(float* out, const float* a, const float* b);
// Rotate a 3D vector by a 4x4 matrix
void VR_RotateVecByMat(const float* m, float* v);
// Convert an OpenXR XrPosef to an OpenGL 4x4 matrix (column-major)
void VR_PoseToMatrix(const XrPosef* pose, float* glMatrix);
// Create an asymmetric OpenGL projection matrix from OpenXR FOV tangents
void VR_FovToProjection(const XrFovf* fov, float nearZ, float farZ, float* glMatrix);
#ifdef __cplusplus
}
#endif
#endif // HAVE_VR
#endif // __VR_MATH_H__

795
src/vr/vr_render.c Normal file
View file

@ -0,0 +1,795 @@
#include "vr_render.h"
#ifdef HAVE_VR
#include "vr_math.h"
#include "../console.h"
#include "../screen.h"
#include "../hardware/r_opengl/r_opengl.h"
#if defined(_WIN32)
#include <windows.h>
#include <GL/gl.h>
#include <GL/glext.h>
#elif defined(__linux__)
#include <GL/gl.h>
#include <GL/glext.h>
#endif
#include <SDL3/SDL.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#ifndef GL_FRAMEBUFFER
#define GL_FRAMEBUFFER 0x8D40
#define GL_COLOR_ATTACHMENT0 0x8CE0
#define GL_DEPTH_ATTACHMENT 0x8D00
#define GL_DEPTH_COMPONENT24 0x81A6
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
#endif
#ifndef GL_RENDERBUFFER
#define GL_RENDERBUFFER 0x8D41
#endif
#ifndef GL_READ_FRAMEBUFFER
#define GL_READ_FRAMEBUFFER 0x8CA8
#endif
#ifndef GL_DRAW_FRAMEBUFFER
#define GL_DRAW_FRAMEBUFFER 0x8CA9
#endif
typedef void (APIENTRY * PFNGLGENFRAMEBUFFERSPROC) (GLsizei n, GLuint *framebuffers);
typedef void (APIENTRY * PFNGLBINDFRAMEBUFFERPROC) (GLenum target, GLuint framebuffer);
typedef void (APIENTRY * PFNGLFRAMEBUFFERTEXTURE2DPROC) (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
typedef void (APIENTRY * PFNGLGENRENDERBUFFERSPROC) (GLsizei n, GLuint *renderbuffers);
typedef void (APIENTRY * PFNGLBINDRENDERBUFFERPROC) (GLenum target, GLuint renderbuffer);
typedef void (APIENTRY * PFNGLRENDERBUFFERSTORAGEPROC) (GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
typedef void (APIENTRY * PFNGLFRAMEBUFFERRENDERBUFFERPROC) (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
typedef void (APIENTRY * PFNGLDELETEFRAMEBUFFERSPROC) (GLsizei n, const GLuint *framebuffers);
typedef void (APIENTRY * PFNGLDELETERENDERBUFFERSPROC) (GLsizei n, const GLuint *renderbuffers);
typedef GLenum (APIENTRY * PFNGLCHECKFRAMEBUFFERSTATUSPROC) (GLenum target);
typedef void (APIENTRY * PFNGLBLITFRAMEBUFFERPROC) (GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
static PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers = NULL;
static PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer = NULL;
static PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D = NULL;
static PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers = NULL;
static PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer = NULL;
static PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage = NULL;
static PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer = NULL;
static PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers = NULL;
static PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers = NULL;
static PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus = NULL;
static PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer = NULL;
XrSwapchain vr_swapchains[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
uint32_t vr_swapchain_lengths[2] = {0, 0};
XrSwapchainImageOpenGLKHR* vr_swapchain_images[2] = {NULL, NULL};
uint32_t* vr_framebuffers[2] = {NULL, NULL};
uint32_t* vr_depthbuffers[2] = {NULL, NULL};
XrSwapchain vr_ui_swapchain = XR_NULL_HANDLE;
uint32_t vr_ui_swapchain_length = 0;
XrSwapchainImageOpenGLKHR* vr_ui_swapchain_images = NULL;
uint32_t* vr_ui_framebuffers = NULL;
uint32_t vr_ui_width = 0;
uint32_t vr_ui_height = 0;
enum {
VR_DEFAULT_UI_SWAPCHAIN_WIDTH = 1920,
VR_DEFAULT_UI_SWAPCHAIN_HEIGHT = 1080
};
static void LoadFBOFunctions(void) {
if (glGenFramebuffers) return;
glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC)SDL_GL_GetProcAddress("glGenFramebuffers");
glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC)SDL_GL_GetProcAddress("glBindFramebuffer");
glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC)SDL_GL_GetProcAddress("glFramebufferTexture2D");
glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC)SDL_GL_GetProcAddress("glGenRenderbuffers");
glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC)SDL_GL_GetProcAddress("glBindRenderbuffer");
glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC)SDL_GL_GetProcAddress("glRenderbufferStorage");
glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)SDL_GL_GetProcAddress("glFramebufferRenderbuffer");
glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC)SDL_GL_GetProcAddress("glDeleteFramebuffers");
glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC)SDL_GL_GetProcAddress("glDeleteRenderbuffers");
glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC)SDL_GL_GetProcAddress("glCheckFramebufferStatus");
glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC)SDL_GL_GetProcAddress("glBlitFramebuffer");
}
boolean VR_InitSwapchains(void)
{
LoadFBOFunctions();
if (!glGenFramebuffers) {
CONS_Printf("VR_InitSwapchains: FBO extensions not found!\n");
return false;
}
for (int eye = 0; eye < 2; eye++) {
XrSwapchainCreateInfo swapchainCreateInfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
swapchainCreateInfo.next = NULL;
swapchainCreateInfo.createFlags = 0;
swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
swapchainCreateInfo.format = GL_SRGB8_ALPHA8;
swapchainCreateInfo.sampleCount = 1;
swapchainCreateInfo.width = vr_render_width;
swapchainCreateInfo.height = vr_render_height;
swapchainCreateInfo.faceCount = 1;
swapchainCreateInfo.arraySize = 1;
swapchainCreateInfo.mipCount = 1;
XrResult res = xrCreateSwapchain(vr_session, &swapchainCreateInfo, &vr_swapchains[eye]);
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to create swapchain %d (XrResult=%d).\n", eye, (int)res);
return false;
}
xrEnumerateSwapchainImages(vr_swapchains[eye], 0, &vr_swapchain_lengths[eye], NULL);
CONS_Printf("VR: Swapchain %d has %u images.\n", eye, vr_swapchain_lengths[eye]);
vr_swapchain_images[eye] = (XrSwapchainImageOpenGLKHR*)malloc(vr_swapchain_lengths[eye] * sizeof(XrSwapchainImageOpenGLKHR));
for (uint32_t i = 0; i < vr_swapchain_lengths[eye]; i++) {
vr_swapchain_images[eye][i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR;
vr_swapchain_images[eye][i].next = NULL;
}
xrEnumerateSwapchainImages(vr_swapchains[eye], vr_swapchain_lengths[eye], &vr_swapchain_lengths[eye], (XrSwapchainImageBaseHeader*)vr_swapchain_images[eye]);
vr_framebuffers[eye] = (uint32_t*)malloc(vr_swapchain_lengths[eye] * sizeof(uint32_t));
vr_depthbuffers[eye] = (uint32_t*)malloc(vr_swapchain_lengths[eye] * sizeof(uint32_t));
for (uint32_t i = 0; i < vr_swapchain_lengths[eye]; i++) {
glGenFramebuffers(1, &vr_framebuffers[eye][i]);
glGenRenderbuffers(1, &vr_depthbuffers[eye][i]);
glBindFramebuffer(GL_FRAMEBUFFER, vr_framebuffers[eye][i]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, vr_swapchain_images[eye][i].image, 0);
glBindRenderbuffer(GL_RENDERBUFFER, vr_depthbuffers[eye][i]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, vr_render_width, vr_render_height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, vr_depthbuffers[eye][i]);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
CONS_Printf("VR: WARNING - FBO %u for eye %d is incomplete (status=0x%x)!\n", i, eye, status);
}
}
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Keep UI at a stable 16:9 panel size. Eye targets can be square on canted HMDs.
vr_ui_width = VR_DEFAULT_UI_SWAPCHAIN_WIDTH;
vr_ui_height = VR_DEFAULT_UI_SWAPCHAIN_HEIGHT;
XrSwapchainCreateInfo uiSwapchainCreateInfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
uiSwapchainCreateInfo.arraySize = 1;
uiSwapchainCreateInfo.format = GL_SRGB8_ALPHA8;
uiSwapchainCreateInfo.width = vr_ui_width;
uiSwapchainCreateInfo.height = vr_ui_height;
uiSwapchainCreateInfo.mipCount = 1;
uiSwapchainCreateInfo.faceCount = 1;
uiSwapchainCreateInfo.sampleCount = 1;
uiSwapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
XrResult res = xrCreateSwapchain(vr_session, &uiSwapchainCreateInfo, &vr_ui_swapchain);
if (XR_FAILED(res)) {
CONS_Printf("VR: Failed to create UI swapchain.\n");
return false;
}
xrEnumerateSwapchainImages(vr_ui_swapchain, 0, &vr_ui_swapchain_length, NULL);
vr_ui_swapchain_images = (XrSwapchainImageOpenGLKHR*)malloc(vr_ui_swapchain_length * sizeof(XrSwapchainImageOpenGLKHR));
for (uint32_t i = 0; i < vr_ui_swapchain_length; i++) {
vr_ui_swapchain_images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR;
vr_ui_swapchain_images[i].next = NULL;
}
xrEnumerateSwapchainImages(vr_ui_swapchain, vr_ui_swapchain_length, &vr_ui_swapchain_length, (XrSwapchainImageBaseHeader*)vr_ui_swapchain_images);
vr_ui_framebuffers = (uint32_t*)malloc(vr_ui_swapchain_length * sizeof(uint32_t));
for (uint32_t i = 0; i < vr_ui_swapchain_length; i++) {
glGenFramebuffers(1, &vr_ui_framebuffers[i]);
glBindFramebuffer(GL_FRAMEBUFFER, vr_ui_framebuffers[i]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, vr_ui_swapchain_images[i].image, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
CONS_Printf("VR: Failed to create UI framebuffer %d\n", i);
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
CONS_Printf("VR: UI swapchain created (%ux%u, %u images).\n",
vr_ui_width, vr_ui_height, vr_ui_swapchain_length);
return true;
}
static XrPosef cachedUIPose;
static boolean cachedUIPoseValid = false;
void VR_DestroySwapchains(void)
{
for (int eye = 0; eye < 2; eye++) {
if (vr_framebuffers[eye] && glDeleteFramebuffers) {
glDeleteFramebuffers(vr_swapchain_lengths[eye], vr_framebuffers[eye]);
free(vr_framebuffers[eye]);
vr_framebuffers[eye] = NULL;
}
if (vr_depthbuffers[eye] && glDeleteRenderbuffers) {
glDeleteRenderbuffers(vr_swapchain_lengths[eye], vr_depthbuffers[eye]);
free(vr_depthbuffers[eye]);
vr_depthbuffers[eye] = NULL;
}
if (vr_swapchain_images[eye]) {
free(vr_swapchain_images[eye]);
vr_swapchain_images[eye] = NULL;
}
if (vr_swapchains[eye] != XR_NULL_HANDLE) {
xrDestroySwapchain(vr_swapchains[eye]);
vr_swapchains[eye] = XR_NULL_HANDLE;
}
vr_swapchain_lengths[eye] = 0;
}
if (vr_ui_framebuffers && glDeleteFramebuffers) {
glDeleteFramebuffers(vr_ui_swapchain_length, vr_ui_framebuffers);
free(vr_ui_framebuffers);
vr_ui_framebuffers = NULL;
}
if (vr_ui_swapchain_images) {
free(vr_ui_swapchain_images);
vr_ui_swapchain_images = NULL;
}
if (vr_ui_swapchain != XR_NULL_HANDLE) {
xrDestroySwapchain(vr_ui_swapchain);
vr_ui_swapchain = XR_NULL_HANDLE;
}
vr_ui_swapchain_length = 0;
vr_ui_width = 0;
vr_ui_height = 0;
cachedUIPoseValid = false;
}
// Cached per-frame state
static XrFrameState frameState = {XR_TYPE_FRAME_STATE};
static uint32_t swapchainImageIndex[2];
static uint32_t uiSwapchainImageIndex;
static boolean vr_frame_begun = false;
static boolean eyeSwapchainAcquired[2] = {false, false};
static boolean eyeSwapchainRendered[2] = {false, false};
static boolean uiSwapchainAcquired = false;
static XrView cachedViews[2]; // Cache the located views for EndFrame
static boolean cachedViewsValid = false;
static XrPosef cachedHeadPose;
static boolean cachedHeadPoseValid = false;
static XrSessionState vr_session_state = XR_SESSION_STATE_UNKNOWN;
static boolean vr_session_running = false;
// Poll and handle OpenXR events (session state machine)
static void VR_PollEvents(void)
{
if (!vr_started) return;
XrEventDataBuffer eventData = {XR_TYPE_EVENT_DATA_BUFFER};
eventData.next = NULL;
while (xrPollEvent(vr_instance, &eventData) == XR_SUCCESS) {
switch (eventData.type) {
case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: {
XrEventDataSessionStateChanged* stateChanged = (XrEventDataSessionStateChanged*)&eventData;
vr_session_state = stateChanged->state;
CONS_Printf("VR: Session state changed to %d\n", (int)vr_session_state);
switch (vr_session_state) {
case XR_SESSION_STATE_READY:
// Session is already begun in VR_Init, but handle if needed
vr_session_running = true;
break;
case XR_SESSION_STATE_SYNCHRONIZED:
case XR_SESSION_STATE_VISIBLE:
case XR_SESSION_STATE_FOCUSED:
vr_session_running = true;
break;
case XR_SESSION_STATE_STOPPING:
xrEndSession(vr_session);
vr_session_running = false;
break;
case XR_SESSION_STATE_LOSS_PENDING:
case XR_SESSION_STATE_EXITING:
vr_session_running = false;
break;
default:
break;
}
break;
}
default:
break;
}
// Reset for next event
eventData.type = XR_TYPE_EVENT_DATA_BUFFER;
eventData.next = NULL;
}
}
static void VR_SetIdentityMatrix(float* m)
{
memset(m, 0, sizeof(float) * 16);
m[0] = 1.0f;
m[5] = 1.0f;
m[10] = 1.0f;
m[15] = 1.0f;
}
static void VR_ScaleMatrixTranslation(float* m, float scale)
{
m[12] *= scale;
m[13] *= scale;
m[14] *= scale;
}
static float VR_YawFromQuaternion(const XrQuaternionf* q)
{
return atan2f(2.0f * (q->w * q->y + q->x * q->z),
1.0f - 2.0f * (q->y * q->y + q->x * q->x));
}
static float VR_RollFromQuaternion(const XrQuaternionf* q)
{
return atan2f(2.0f * (q->w * q->z + q->x * q->y),
1.0f - 2.0f * (q->z * q->z + q->y * q->y));
}
static XrQuaternionf VR_QuaternionMultiply(const XrQuaternionf* a, const XrQuaternionf* b)
{
XrQuaternionf q;
q.x = a->w * b->x + a->x * b->w + a->y * b->z - a->z * b->y;
q.y = a->w * b->y - a->x * b->z + a->y * b->w + a->z * b->x;
q.z = a->w * b->z + a->x * b->y - a->y * b->x + a->z * b->w;
q.w = a->w * b->w - a->x * b->x - a->y * b->y - a->z * b->z;
return q;
}
static XrQuaternionf VR_QuaternionFromYaw(float yaw)
{
XrQuaternionf q;
const float halfYaw = yaw * 0.5f;
q.x = 0.0f;
q.y = sinf(halfYaw);
q.z = 0.0f;
q.w = cosf(halfYaw);
return q;
}
static XrQuaternionf VR_QuaternionFromRoll(float roll)
{
XrQuaternionf q;
const float halfRoll = roll * 0.5f;
q.x = 0.0f;
q.y = 0.0f;
q.z = sinf(halfRoll);
q.w = cosf(halfRoll);
return q;
}
static XrQuaternionf VR_QuaternionFromYawRoll(float yaw, float roll)
{
XrQuaternionf yawQ = VR_QuaternionFromYaw(yaw);
XrQuaternionf rollQ = VR_QuaternionFromRoll(roll);
return VR_QuaternionMultiply(&yawQ, &rollQ);
}
static XrPosef VR_MakeUIPose(float distance)
{
XrPosef pose;
memset(&pose, 0, sizeof(pose));
pose.orientation.w = 1.0f;
pose.position.z = -distance;
if (!cachedHeadPoseValid)
return pose;
const float yaw = VR_YawFromQuaternion(&cachedHeadPose.orientation);
const float roll = VR_RollFromQuaternion(&cachedHeadPose.orientation);
pose.orientation = VR_QuaternionFromYawRoll(yaw, roll);
pose.position = cachedHeadPose.position;
pose.position.x += -sinf(yaw) * distance;
pose.position.z += -cosf(yaw) * distance;
return pose;
}
static int VR_WorldScale(void)
{
int mode = cv_vrscale.string ? cv_vrscale.value : 1;
if (mode < 0)
mode = 0;
else if (mode > 2)
mode = 2;
return vrWorldScale[mode];
}
boolean VR_BeginFrame(void)
{
if (!vr_started) return false;
// Poll events first — this drives the session state machine
VR_PollEvents();
if (!vr_session_running) return false;
cachedViewsValid = false;
cachedHeadPoseValid = false;
vr_frame_begun = false;
eyeSwapchainAcquired[0] = false;
eyeSwapchainAcquired[1] = false;
eyeSwapchainRendered[0] = false;
eyeSwapchainRendered[1] = false;
uiSwapchainAcquired = false;
XrFrameWaitInfo frameWaitInfo = {XR_TYPE_FRAME_WAIT_INFO};
frameWaitInfo.next = NULL;
frameState.type = XR_TYPE_FRAME_STATE;
frameState.next = NULL;
XrResult res = xrWaitFrame(vr_session, &frameWaitInfo, &frameState);
if (XR_FAILED(res)) {
CONS_Printf("VR: xrWaitFrame failed (XrResult=%d)\n", (int)res);
return false;
}
XrFrameBeginInfo frameBeginInfo = {XR_TYPE_FRAME_BEGIN_INFO};
frameBeginInfo.next = NULL;
res = xrBeginFrame(vr_session, &frameBeginInfo);
if (XR_FAILED(res)) {
CONS_Printf("VR: xrBeginFrame failed (XrResult=%d)\n", (int)res);
return false;
}
vr_frame_begun = true;
// Locate views to get per-eye poses and FOVs
XrViewLocateInfo viewLocateInfo = {XR_TYPE_VIEW_LOCATE_INFO};
viewLocateInfo.next = NULL;
viewLocateInfo.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
viewLocateInfo.displayTime = frameState.predictedDisplayTime;
viewLocateInfo.space = vr_local_space;
XrViewState viewState = {XR_TYPE_VIEW_STATE};
viewState.next = NULL;
uint32_t viewCountOutput = 0;
cachedViews[0].type = XR_TYPE_VIEW; cachedViews[0].next = NULL;
cachedViews[1].type = XR_TYPE_VIEW; cachedViews[1].next = NULL;
res = xrLocateViews(vr_session, &viewLocateInfo, &viewState, 2, &viewCountOutput, cachedViews);
if (XR_FAILED(res)) {
CONS_Printf("VR: xrLocateViews failed (XrResult=%d)\n", (int)res);
return true;
}
if (viewCountOutput == 2 &&
(viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) &&
(viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT)) {
cachedViewsValid = true;
XrSpaceLocation headLocation = {XR_TYPE_SPACE_LOCATION};
res = xrLocateSpace(vr_view_space, vr_local_space, frameState.predictedDisplayTime, &headLocation);
if (XR_SUCCEEDED(res) &&
(headLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) &&
(headLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) {
cachedHeadPose = headLocation.pose;
cachedHeadPoseValid = true;
} else {
cachedHeadPose = cachedViews[0].pose;
cachedHeadPose.position.x = (cachedViews[0].pose.position.x + cachedViews[1].pose.position.x) * 0.5f;
cachedHeadPose.position.y = (cachedViews[0].pose.position.y + cachedViews[1].pose.position.y) * 0.5f;
cachedHeadPose.position.z = (cachedViews[0].pose.position.z + cachedViews[1].pose.position.z) * 0.5f;
cachedHeadPoseValid = true;
}
float headPoseMatrix[16];
float headInverseMeters[16];
VR_PoseToMatrix(&cachedHeadPose, headPoseMatrix);
VR_MatrixInv(headInverseMeters, headPoseMatrix);
const float worldScale = VR_WorldScale();
const int poseMode = cv_vrposemode.value;
if (poseMode == 3)
memcpy(vrHMDPoseMatrix, headPoseMatrix, sizeof(float) * 16);
else
memcpy(vrHMDPoseMatrix, headInverseMeters, sizeof(float) * 16);
VR_ScaleMatrixTranslation(vrHMDPoseMatrix, worldScale);
memcpy(vrHMDScaledPoseMatrix, vrHMDPoseMatrix, sizeof(float) * 16);
VR_ScaleMatrixTranslation(vrHMDScaledPoseMatrix, vrPlayerScale);
memcpy(vrHMDPoseSkyboxMatrix, vrHMDPoseMatrix, sizeof(float) * 16);
vrHMDPoseSkyboxMatrix[12] = 0.0f;
vrHMDPoseSkyboxMatrix[13] = 0.0f;
vrHMDPoseSkyboxMatrix[14] = 0.0f;
for (int i = 0; i < 2; i++) {
VR_FovToProjection(&cachedViews[i].fov, 0.9f, 32768.0f, vrEyeProjMatrix[i]);
float eyePoseMatrix[16];
VR_PoseToMatrix(&cachedViews[i].pose, eyePoseMatrix);
if (poseMode == 1 || poseMode == 3) {
float eyeToHeadMatrix[16];
// Match the old OpenVR path: keep the HMD pose and per-eye
// head-to-eye transform separate, then multiply them in shader.
VR_MatrixMultiply(eyeToHeadMatrix, headInverseMeters, eyePoseMatrix);
VR_MatrixInv(vrEyeViewMatrix[i], eyeToHeadMatrix);
} else if (poseMode == 2) {
memcpy(vrEyeViewMatrix[i], eyePoseMatrix, sizeof(float) * 16);
} else {
// SDK-style path: render each eye from the full located view.
VR_MatrixInv(vrEyeViewMatrix[i], eyePoseMatrix);
}
VR_ScaleMatrixTranslation(vrEyeViewMatrix[i], worldScale);
}
VR_ScaleViewMatrices(vrPlayerScale, cv_vrdisableskystereo.value ? 0 : 1);
} else {
VR_SetIdentityMatrix(vrHMDPoseMatrix);
VR_SetIdentityMatrix(vrHMDScaledPoseMatrix);
VR_SetIdentityMatrix(vrHMDPoseSkyboxMatrix);
VR_SetIdentityMatrix(vrEyeViewMatrix[0]);
VR_SetIdentityMatrix(vrEyeViewMatrix[1]);
VR_SetIdentityMatrix(vrScaledEyeViewMatrix[0]);
VR_SetIdentityMatrix(vrScaledEyeViewMatrix[1]);
VR_SetIdentityMatrix(vrEyeSkyboxViewMatrix[0]);
VR_SetIdentityMatrix(vrEyeSkyboxViewMatrix[1]);
}
return true;
}
boolean VR_SetEye(int eye)
{
if (!vr_started || !vr_session_running || !vr_frame_begun) return false;
if (eye < 0 || eye > 1) return false;
vr_current_eye = eye;
vr_drawing_ui = false;
vr_render_pass = (eye == 0) ? VR_PASS_3D_LEFT : VR_PASS_3D_RIGHT;
XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
acquireInfo.next = NULL;
XrResult res = xrAcquireSwapchainImage(vr_swapchains[eye], &acquireInfo, &swapchainImageIndex[eye]);
if (XR_FAILED(res)) {
CONS_Printf("VR: xrAcquireSwapchainImage failed for eye %d (XrResult=%d)\n", eye, (int)res);
return false;
}
eyeSwapchainAcquired[eye] = true;
XrSwapchainImageWaitInfo waitInfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
waitInfo.next = NULL;
waitInfo.timeout = XR_INFINITE_DURATION;
res = xrWaitSwapchainImage(vr_swapchains[eye], &waitInfo);
if (XR_FAILED(res)) {
CONS_Printf("VR: xrWaitSwapchainImage failed for eye %d (XrResult=%d)\n", eye, (int)res);
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
releaseInfo.next = NULL;
xrReleaseSwapchainImage(vr_swapchains[eye], &releaseInfo);
eyeSwapchainAcquired[eye] = false;
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, vr_framebuffers[eye][swapchainImageIndex[eye]]);
glViewport(0, 0, vr_render_width, vr_render_height);
SetModelView((GLint)vr_render_width, (GLint)vr_render_height);
return true;
}
void VR_ReleaseEye(int eye)
{
if (!vr_started || !vr_session_running || eye < 0 || eye > 1 || !eyeSwapchainAcquired[eye]) return;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
releaseInfo.next = NULL;
xrReleaseSwapchainImage(vr_swapchains[eye], &releaseInfo);
eyeSwapchainAcquired[eye] = false;
eyeSwapchainRendered[eye] = true;
}
void VR_BindDefaultFramebuffer(void)
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
boolean VR_MirrorEyeToDefaultFramebuffer(int width, int height)
{
if (!vr_started || !vr_session_running || vr_current_eye < 0 || vr_current_eye > 1)
return false;
if (!eyeSwapchainAcquired[vr_current_eye] || !glBlitFramebuffer)
return false;
if (width <= 0 || height <= 0)
return false;
glBindFramebuffer(GL_READ_FRAMEBUFFER, vr_framebuffers[vr_current_eye][swapchainImageIndex[vr_current_eye]]);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, width, height);
glBlitFramebuffer(0, 0, (GLint)vr_render_width, (GLint)vr_render_height,
0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
return true;
}
void VR_BeginInEyeUI(void)
{
if (!vr_started || !vr_session_running || !vr_frame_begun) return;
if (vr_current_eye < 0 || vr_current_eye > 1 || !eyeSwapchainAcquired[vr_current_eye]) return;
vr_drawing_ui = true;
glBindFramebuffer(GL_FRAMEBUFFER, vr_framebuffers[vr_current_eye][swapchainImageIndex[vr_current_eye]]);
glViewport(0, 0, vr_render_width, vr_render_height);
GL_UnSetShader();
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_SCISSOR_TEST);
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
}
boolean VR_BindUISwapchain(void)
{
if (!vr_started || !vr_session_running || !vr_frame_begun || vr_ui_swapchain == XR_NULL_HANDLE) return false;
if (!uiSwapchainAcquired) {
XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
acquireInfo.next = NULL;
XrResult res = xrAcquireSwapchainImage(vr_ui_swapchain, &acquireInfo, &uiSwapchainImageIndex);
if (XR_FAILED(res)) return false;
XrSwapchainImageWaitInfo waitInfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
waitInfo.next = NULL;
waitInfo.timeout = XR_INFINITE_DURATION;
res = xrWaitSwapchainImage(vr_ui_swapchain, &waitInfo);
if (XR_FAILED(res)) {
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
releaseInfo.next = NULL;
xrReleaseSwapchainImage(vr_ui_swapchain, &releaseInfo);
return false;
}
uiSwapchainAcquired = true;
}
vr_render_pass = VR_PASS_UI;
vr_drawing_ui = true;
glBindFramebuffer(GL_FRAMEBUFFER, vr_ui_framebuffers[uiSwapchainImageIndex]);
// Keep the hardware 2D renderer's logical screen size unchanged. Its UI
// patch and screen-texture paths key off screen_width/screen_height, while
// the OpenXR swapchain can still use its full physical viewport.
SetModelView((GLint)vid.width, (GLint)vid.height);
glViewport(0, 0, vr_ui_width, vr_ui_height);
GL_UnSetShader();
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glEnable(GL_BLEND);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
return true;
}
void VR_EndFrame(void)
{
if (!vr_started || !vr_session_running || !vr_frame_begun) return;
XrCompositionLayerProjectionView projectionViews[2];
for (int i = 0; i < 2; i++) {
projectionViews[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
projectionViews[i].next = NULL;
// Use the ACTUAL tracked view poses, not hardcoded placeholders
if (cachedViewsValid) {
projectionViews[i].pose = cachedViews[i].pose;
projectionViews[i].fov = cachedViews[i].fov;
} else {
// Fallback identity pose (should rarely happen)
projectionViews[i].pose.orientation.w = 1.0f;
projectionViews[i].pose.orientation.x = 0.0f;
projectionViews[i].pose.orientation.y = 0.0f;
projectionViews[i].pose.orientation.z = 0.0f;
projectionViews[i].pose.position.x = 0.0f;
projectionViews[i].pose.position.y = 0.0f;
projectionViews[i].pose.position.z = 0.0f;
projectionViews[i].fov.angleUp = 0.9f;
projectionViews[i].fov.angleDown = -0.9f;
projectionViews[i].fov.angleLeft = -0.9f;
projectionViews[i].fov.angleRight = 0.9f;
}
projectionViews[i].subImage.swapchain = vr_swapchains[i];
projectionViews[i].subImage.imageRect.offset.x = 0;
projectionViews[i].subImage.imageRect.offset.y = 0;
projectionViews[i].subImage.imageRect.extent.width = vr_render_width;
projectionViews[i].subImage.imageRect.extent.height = vr_render_height;
projectionViews[i].subImage.imageArrayIndex = 0;
}
const XrCompositionLayerBaseHeader* layers[2];
uint32_t layerCount = 0;
XrCompositionLayerProjection projectionLayer = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
if (cachedViewsValid && eyeSwapchainRendered[0] && eyeSwapchainRendered[1]) {
projectionLayer.layerFlags = 0;
projectionLayer.space = vr_local_space;
projectionLayer.viewCount = 2;
projectionLayer.views = projectionViews;
layers[layerCount++] = (const XrCompositionLayerBaseHeader*)&projectionLayer;
}
XrCompositionLayerQuad uiLayer = {XR_TYPE_COMPOSITION_LAYER_QUAD};
if (uiSwapchainAcquired) {
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
releaseInfo.next = NULL;
xrReleaseSwapchainImage(vr_ui_swapchain, &releaseInfo);
uiLayer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT |
XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
uiLayer.space = cachedHeadPoseValid ? vr_local_space : vr_view_space;
uiLayer.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
uiLayer.subImage.swapchain = vr_ui_swapchain;
uiLayer.subImage.imageRect.offset.x = 0;
uiLayer.subImage.imageRect.offset.y = 0;
uiLayer.subImage.imageRect.extent.width = vr_ui_width;
uiLayer.subImage.imageRect.extent.height = vr_ui_height;
uiLayer.subImage.imageArrayIndex = 0;
const float uiDistance = cv_vruidistance.value > 0 ? (float)cv_vruidistance.value / 70.0f : 1.5f;
const float uiWidth = cv_vruiscale.value > 0 ? (float)cv_vruiscale.value * 0.040f : 1.0f;
cachedUIPose = VR_MakeUIPose(uiDistance);
cachedUIPoseValid = true;
uiLayer.pose = cachedUIPose;
uiLayer.size.width = uiWidth;
uiLayer.size.height = uiWidth * ((float)vr_ui_height / (float)vr_ui_width);
layers[layerCount++] = (const XrCompositionLayerBaseHeader*)&uiLayer;
uiSwapchainAcquired = false;
}
XrFrameEndInfo frameEndInfo = {XR_TYPE_FRAME_END_INFO};
frameEndInfo.next = NULL;
frameEndInfo.displayTime = frameState.predictedDisplayTime;
frameEndInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
frameEndInfo.layerCount = layerCount;
frameEndInfo.layers = layerCount > 0 ? layers : NULL;
XrResult res = xrEndFrame(vr_session, &frameEndInfo);
if (XR_FAILED(res)) {
static int endFrameErrorCount = 0;
if (endFrameErrorCount < 5) {
CONS_Printf("VR: xrEndFrame failed (XrResult=%d)\n", (int)res);
endFrameErrorCount++;
}
}
vr_frame_begun = false;
vr_render_pass = VR_PASS_NONE;
vr_drawing_ui = false;
}
#endif // HAVE_VR

42
src/vr/vr_render.h Normal file
View file

@ -0,0 +1,42 @@
#ifndef __VR_RENDER_H__
#define __VR_RENDER_H__
#ifdef HAVE_VR
#include "vr_main.h"
#ifdef __cplusplus
extern "C" {
#endif
extern XrSwapchain vr_swapchains[2];
extern uint32_t vr_swapchain_lengths[2];
extern XrSwapchainImageOpenGLKHR* vr_swapchain_images[2];
extern uint32_t* vr_framebuffers[2];
extern uint32_t* vr_depthbuffers[2];
extern XrSwapchain vr_ui_swapchain;
extern uint32_t vr_ui_swapchain_length;
extern XrSwapchainImageOpenGLKHR* vr_ui_swapchain_images;
extern uint32_t* vr_ui_framebuffers;
extern uint32_t vr_ui_width;
extern uint32_t vr_ui_height;
boolean VR_InitSwapchains(void);
void VR_DestroySwapchains(void);
// Frame Loop
boolean VR_BeginFrame(void);
boolean VR_SetEye(int eye);
void VR_ReleaseEye(int eye);
void VR_BindDefaultFramebuffer(void);
void VR_BeginInEyeUI(void);
boolean VR_MirrorEyeToDefaultFramebuffer(int width, int height);
boolean VR_BindUISwapchain(void);
void VR_EndFrame(void);
#ifdef __cplusplus
}
#endif
#endif // HAVE_VR
#endif // __VR_RENDER_H__