diff --git a/README.md b/README.md index da56282ae..7d3f555d2 100644 --- a/README.md +++ b/README.md @@ -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)! diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 66e82ce39..ce7847e38 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/d_main.cpp b/src/d_main.cpp index 1bf3b5d79..a2c691780 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -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. // diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 76418e798..02766f4ff 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -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); diff --git a/src/hardware/hw_clip.c b/src/hardware/hw_clip.c index 72de109ef..95820cc5b 100644 --- a/src/hardware/hw_clip.c +++ b/src/hardware/hw_clip.c @@ -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) diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c index 52b9b0722..5583029e4 100644 --- a/src/hardware/hw_main.c +++ b/src/hardware/hw_main.c @@ -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); } diff --git a/src/hardware/hw_shaders.c b/src/hardware/hw_shaders.c index 75e760123..3250d62f9 100644 --- a/src/hardware/hw_shaders.c +++ b/src/hardware/hw_shaders.c @@ -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); } diff --git a/src/hardware/hw_shaders.h b/src/hardware/hw_shaders.h index 950d016c5..c866ff0ab 100644 --- a/src/hardware/hw_shaders.h +++ b/src/hardware/hw_shaders.h @@ -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" // diff --git a/src/hardware/hw_sky.c b/src/hardware/hw_sky.c index dccb69db2..ed0d587f3 100644 --- a/src/hardware/hw_sky.c +++ b/src/hardware/hw_sky.c @@ -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) diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c index 59a25a9b5..0ca845ea7 100644 --- a/src/hardware/r_opengl/r_opengl.c +++ b/src/hardware/r_opengl/r_opengl.c @@ -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 diff --git a/src/p_user.c b/src/p_user.c index c35d390fa..d7ef85571 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -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; } diff --git a/src/r_fps.c b/src/r_fps.c index eb68acd5a..9f72af19c 100644 --- a/src/r_fps.c +++ b/src/r_fps.c @@ -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 diff --git a/src/screen.c b/src/screen.c index ff9e73a7e..19c36423c 100644 --- a/src/screen.c +++ b/src/screen.c @@ -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 diff --git a/src/screen.h b/src/screen.h index f79113d4c..158a34e11 100644 --- a/src/screen.h +++ b/src/screen.h @@ -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; diff --git a/src/sdl/i_system.cpp b/src/sdl/i_system.cpp index 30914278f..9c6a10385 100644 --- a/src/sdl/i_system.cpp +++ b/src/sdl/i_system.cpp @@ -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 diff --git a/src/sdl/i_video.cpp b/src/sdl/i_video.cpp index eb9e843e0..b4915732a 100644 --- a/src/sdl/i_video.cpp +++ b/src/sdl/i_video.cpp @@ -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(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 - - diff --git a/src/sdl/ogl_sdl.c b/src/sdl/ogl_sdl.c index 24c92c24c..716c9fc94 100644 --- a/src/sdl/ogl_sdl.c +++ b/src/sdl/ogl_sdl.c @@ -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 diff --git a/src/vr/vr_main.c b/src/vr/vr_main.c new file mode 100644 index 000000000..d3e8b87ca --- /dev/null +++ b/src/vr/vr_main.c @@ -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 +#include +#include "../sdl/sdlmain.h" +#include +#include +#include +#ifdef __linux__ +#include +#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, ¤tRefreshRate)) && + 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 diff --git a/src/vr/vr_main.h b/src/vr/vr_main.h new file mode 100644 index 000000000..f823521fc --- /dev/null +++ b/src/vr/vr_main.h @@ -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 +#elif defined(__linux__) +#define XR_USE_PLATFORM_XLIB +#define XR_USE_GRAPHICS_API_OPENGL +#include +#include +#endif + +#include +#include + +#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__ diff --git a/src/vr/vr_math.c b/src/vr/vr_math.c new file mode 100644 index 000000000..cf81fe175 --- /dev/null +++ b/src/vr/vr_math.c @@ -0,0 +1,126 @@ +#include "vr_math.h" + +#ifdef HAVE_VR + +#include + +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 diff --git a/src/vr/vr_math.h b/src/vr/vr_math.h new file mode 100644 index 000000000..cfd8aa7f1 --- /dev/null +++ b/src/vr/vr_math.h @@ -0,0 +1,33 @@ +#ifndef __VR_MATH_H__ +#define __VR_MATH_H__ + +#ifdef HAVE_VR + +#include + +#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__ diff --git a/src/vr/vr_render.c b/src/vr/vr_render.c new file mode 100644 index 000000000..ccfdeddea --- /dev/null +++ b/src/vr/vr_render.c @@ -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 +#include +#include +#elif defined(__linux__) +#include +#include +#endif + +#include +#include +#include +#include + +#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 diff --git a/src/vr/vr_render.h b/src/vr/vr_render.h new file mode 100644 index 000000000..e6adb8863 --- /dev/null +++ b/src/vr/vr_render.h @@ -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__