diff --git a/VR_PORT_STATUS.md b/VR_PORT_STATUS.md index 7dc61732..e49b9c8b 100644 --- a/VR_PORT_STATUS.md +++ b/VR_PORT_STATUS.md @@ -5,7 +5,25 @@ Companion to `VR_PORT_PLAN.md`, which is the original survey and plan, and to This records what has actually been built, what was learned that contradicts the plan, and what to do next. -Branch `vr-quest3`. Everything below is committed. +Branch `vr-quest3`. The historical status below records previously committed work. + +### Code review update — September 29, 2026 + +These fixes remove the unconditional GPU wait after each eye; +`vr_traceGpu 1` restores that wait only for diagnostics. Normal submission flushes +commands and discards the eye's transient depth/stencil buffers. Swapchain timeouts +now retain the acquired image for retry, and stereo submission requires both eyes. +World/HUD timing now charges the first 2D transition's world flush to the world, +and backend timing accumulates all command batches in an eye. + +The review also fixes desktop HUD suppression, objective initialization, HUD +resource resets, health colors, weapon-wheel placement, stereo text lifetimes, +font shader passes, and sprite culling and upright sprite disappearance when +looking vertically. CMake now tracks additions to the client-game source list. +No headset was connected for this review: the measured frame rates and visual +observations below predate these changes. Device validation is still needed. +The existing font-atlas texture issue and dynamic-light darkening remain +unresolved; `r_worldDlights 0` in `autoexec.cfg` remains the lighting workaround. **On any problem this port is trying to solve, read RTCWQuest first.** It is cloned at `/home/berkybear/RTCWQuest`, clean at `4c714b9`, and it is finished. diff --git a/cmake/basegame.cmake b/cmake/basegame.cmake index 879c1238..44501324 100644 --- a/cmake/basegame.cmake +++ b/cmake/basegame.cmake @@ -12,7 +12,7 @@ file(GLOB_RECURSE BG_SOURCES ${SOURCE_DIR}/fgame/bg_voteoptions.cpp ) -file(GLOB_RECURSE CGAME_SOURCES +file(GLOB_RECURSE CGAME_SOURCES CONFIGURE_DEPENDS ${SOURCE_DIR}/cgame/*.c ${SOURCE_DIR}/cgame/*.cpp ) diff --git a/code/cgame/cg_drawtools.cpp b/code/cgame/cg_drawtools.cpp index 7048db23..be174d39 100644 --- a/code/cgame/cg_drawtools.cpp +++ b/code/cgame/cg_drawtools.cpp @@ -683,8 +683,9 @@ void CG_InitializeObjectives() cg.ObjectivesCurrentAlpha = 0.0; for (i = 0; i < MAX_OBJECTIVES; i++) { - cg.Objectives[i].flags = 0; - cg.Objectives[i].text[0] = 0; + // A new gamestate may already contain objectives (for example after + // loading a save). Subsequent changes arrive through configstrings. + CG_ProcessConfigString(CS_OBJECTIVES + i, qfalse); } } @@ -706,10 +707,6 @@ void CG_DrawObjectives() float fAlpha; iTotalNumLines = 0; - for (i = CS_OBJECTIVES; i < CS_OBJECTIVES + MAX_OBJECTIVES; ++i) { - CG_ProcessConfigString(i, qfalse); - } - iCurrentObjective = atoi(CG_ConfigString(CS_CURRENT_OBJECTIVE)); fTimeDelta = cg.ObjectivesAlphaTime - cg.time; cg.ObjectivesCurrentAlpha = cg.ObjectivesDesiredAlpha; @@ -745,7 +742,7 @@ void CG_DrawObjectives() // back into the eye buffers the pass became 0 and the objectives went // straight back to invisible, which took a device run to notice. // - if (vr_objectivesAlways && vr_objectivesAlways->integer) { + if (vr_hudPass && vr_hudPass->integer && vr_objectivesAlways && vr_objectivesAlways->integer) { fAlpha = 1.0f; } @@ -1612,29 +1609,36 @@ static void CG_DrawVRWheel(void) int count = 0; int i; float cx, cy, radius; + vec2_t textScale; if (!cgi.VR_GetWheel || !cgi.VR_GetWheel(&selection, names, (int)ARRAY_LEN(names), &count)) { return; } - if (count <= 0) { + if (count <= 0 || !cgs.media.objectiveFont) { return; } - // The same virtual screen the objectives text uses. Computing a scale - // from vidWidth/640 instead is not the same thing on this device - the - // eye buffer is 1680x1760, nothing like 4:3 - and it put the ring off - // the edge of the picture. - cx = 320.0f; - cy = 240.0f; - radius = 90.0f; + // Lay out the ring in eye pixels, then undo the text scale on both axes. + // Mixing virtual x with pixel y placed it at y=240 in a 1760-line eye. + textScale[0] = textScale[1] = cgs.glconfig.vidHeight / 1080.0f; + if (textScale[0] <= 0.0f) { + return; + } + cx = cgs.glconfig.vidWidth * 0.5f; + cy = cgs.glconfig.vidHeight * 0.5f; + radius = 120.0f * textScale[0]; + if (count > (int)ARRAY_LEN(names)) { + count = (int)ARRAY_LEN(names); + } for (i = 0; i < count; i++) { // Clockwise from straight up, matching the order the VR layer numbers // the segments in - the two have to agree or the highlight lands on // the wrong word. const float angle = (float)i * (2.0f * M_PI / (float)count); - const float x = cx + sinf(angle) * radius; + const float width = cgi.UI_FontStringWidth(cgs.media.objectiveFont, names[i], -1) * textScale[0]; + const float x = cx + sinf(angle) * radius - width * 0.5f; const float y = cy - cosf(angle) * radius; vec4_t colour; @@ -1645,7 +1649,7 @@ static void CG_DrawVRWheel(void) } cgi.R_SetColor(colour); - cgi.R_DrawString(cgs.media.objectiveFont, names[i], x, y / cgs.uiHiResScale[1], -1, cgs.uiHiResScale); + cgi.R_DrawString(cgs.media.objectiveFont, names[i], x / textScale[0], y / textScale[1], -1, textScale); } cgi.R_SetColor(NULL); @@ -1655,13 +1659,16 @@ void CG_Draw2D(void) { // // Added in OPM - // In VR this is called twice with the frame split in half: once for the - // panel on the player's wrist, and once more with the world, for the - // couple of things that would be useless anywhere else. 0 is every flat - // frame, where it is called once and draws the lot. + // 0 is the flat frame, 1 the wrist panel, 2 the world without its HUD, + // and 3 a VR eye carrying both the world and the HUD. // const int pass = vr_hudPass ? vr_hudPass->integer : 0; + if (pass) { + // Keep damage feedback current even when the wrist panel is hidden. + CG_UpdateVRHud(); + } + if (pass != 2) { CG_UpdateCountdown(); CG_DrawLagometer(); @@ -1674,7 +1681,7 @@ void CG_Draw2D(void) // headset is looking at. Drawing both would put the same text on the // view twice, in two places, at two sizes. // - if (vr_hud && vr_hud->integer) { + if (pass && vr_hud && vr_hud->integer) { CG_DrawVRHud(); } else { CG_DrawObjectives(); diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 6e76baff..60d66f7b 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -616,6 +616,8 @@ extern "C" { // // cg_vrhud.cpp // + void CG_InitVRHud(void); + void CG_UpdateVRHud(void); void CG_DrawVRHud(void); void CG_EyePosition(vec3_t *o_vPos); diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index a567a71d..50308c63 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -231,7 +231,7 @@ void CG_RegisterCvars(void) // cg_fov = cgi.Cvar_Get("cg_fov", "80", CVAR_ARCHIVE); - // Set by the client around each half of a split VR frame; 0 on flat frames. + // 0 flat, 1 wrist panel, 2 world only, 3 combined VR world and HUD. vr_hudPass = cgi.Cvar_Get("vr_hudPass", "0", 0); vr_crosshair = cgi.Cvar_Get("vr_crosshair", "0", CVAR_ARCHIVE); vr_traceObjectives = cgi.Cvar_Get("vr_traceObjectives", "0", 0); @@ -712,6 +712,7 @@ void CG_GameStateReceived(void) CG_InitializeSpecialEffectsManager(); CG_InitializeObjectives(); + CG_InitVRHud(); } /* @@ -743,6 +744,7 @@ void CG_ServerRestarted(void) CG_ResetVSSSources(); // Reset objectives CG_InitializeObjectives(); + CG_InitVRHud(); } /* diff --git a/code/cgame/cg_vrhud.cpp b/code/cgame/cg_vrhud.cpp index 05541016..40b04bda 100644 --- a/code/cgame/cg_vrhud.cpp +++ b/code/cgame/cg_vrhud.cpp @@ -54,10 +54,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // UI has always had it, as re.DrawTrianglePic, and it is what the retail compass // turns its own face with - and the three real layers are drawn here now. // -// Still not the whole of the retail compass: the damage direction ring is not -// drawn, and the objective is a pip rather than compassobjarrow. And the one -// worth having is a different thing altogether - a compass the player pulls out -// of a pocket and holds. +// The damage ring and objective arrow share the card's origin and scale. #include "cg_local.h" @@ -123,6 +120,37 @@ typedef struct { } vrHudMedia_t; static vrHudMedia_t vrHud; +static qboolean vrHudPainInitialized; +static int vrHudPainStat; +static int vrHudPainTime = -1; +static int vrHudLastTime; + +void CG_InitVRHud(void) +{ + memset(&vrHud, 0, sizeof(vrHud)); + vrHudPainInitialized = qfalse; + vrHudPainTime = -1; +} + +void CG_UpdateVRHud(void) +{ + if (!cg.snap || (cg.snap->ps.pm_flags & PMF_INTERMISSION)) { + return; + } + + const int stat = cg.snap->ps.stats[STAT_DAMAGEDIR]; + + // Observe hits even while the requested HUD is hidden. Otherwise opening + // it replays the last hit, however long ago it happened. + if (!vrHudPainInitialized || cg.time < vrHudLastTime) { + vrHudPainInitialized = qtrue; + vrHudPainTime = -1; + } else if (stat != vrHudPainStat) { + vrHudPainTime = cg.time; + } + vrHudPainStat = stat; + vrHudLastTime = cg.time; +} /* ================ @@ -133,21 +161,16 @@ the player is in a map with a snapshot, by which time the shaders are loadable; registering from the media pass would put these in front of every level load whether the HUD is switched on or not. -The handles are dropped whenever the renderer's sequence changes underneath -them, which a map change does, so this re-registers on the first frame of each -map rather than holding a handle into the previous one. +CG_InitVRHud drops the handles when a map or subsystem restarts. A map's start +time is not a renderer sequence: two loads can have the same start time. ================ */ static void CG_VRHudRegister(void) { - static int registeredMap = -1; - - if (vrHud.registered && registeredMap == cgs.levelStartTime) { + if (vrHud.registered) { return; } - registeredMap = cgs.levelStartTime; - vrHud.compassBack = cgi.R_RegisterShaderNoMip("textures/hud/compassback"); vrHud.compassFace = cgi.R_RegisterShaderNoMip("textures/hud/compassface"); vrHud.compassNeedle = cgi.R_RegisterShaderNoMip("textures/hud/compassneedle"); @@ -392,8 +415,7 @@ static void CG_VRHudBar(float x, float y, float frac, float r, float g, float b, ================ CG_VRHudCompass -An upright ring with the cardinal letters orbiting it and a pip for the current -objective. +An upright ring with a turning compass card, damage ring and objective arrow. Two angles do all of it, and both are decoded exactly the way the retail widgets decode them, so they agree with the game rather than with a guess: @@ -463,17 +485,10 @@ static void CG_VRHudCompass(float alpha) // widget starts it whenever the value changes and runs it down over 1500ms. // { - static int painStat = -1; - static int painTime; const int stat = cg.snap->ps.stats[STAT_DAMAGEDIR]; - if (stat != painStat) { - painStat = stat; - painTime = cg.time; - } - - if (painTime && cg.time - painTime < 1500) { - const float fade = 1.0f - (cg.time - painTime) / 1500.0f; + if (vrHudPainTime >= 0 && cg.time - vrHudPainTime < 1500) { + const float fade = 1.0f - (cg.time - vrHudPainTime) / 1500.0f; float frac = (AngleSubtract(eyeAngles[YAW], (stat / 3600.0f) * 360.0f + 180.0f) + 180.0f) / 360.0f; if (frac >= 0.0f && frac <= 1.0f) { @@ -592,11 +607,12 @@ void CG_DrawVRHud(void) int i; float objY; - if (!vr_hud || !vr_hud->integer) { + if (!vr_hudPass || !vr_hudPass->integer || !vr_hud || !vr_hud->integer + || !cg_hud || !cg_hud->integer) { return; } - if (!cg.snap || (cg.snap->ps.pm_flags & PMF_INTERMISSION)) { + if (!cg.snap || (cg.snap->ps.pm_flags & (PMF_NO_HUD | PMF_INTERMISSION))) { return; } @@ -637,8 +653,6 @@ void CG_DrawVRHud(void) return; } - CG_VRHudRegister(); - alpha = vr_hudAlpha ? vr_hudAlpha->value : 1.0f; if (alpha > 1.0f) { alpha = 1.0f; @@ -650,6 +664,8 @@ void CG_DrawVRHud(void) return; } + CG_VRHudRegister(); + // // Design units to pixels. The reference height keeps the cluster the same // angular size whatever the eye buffer turns out to be; the anchor is a @@ -662,7 +678,7 @@ void CG_DrawVRHud(void) } vrHudOriginX = cgs.glconfig.vidWidth * (vr_hudGroupX ? vr_hudGroupX->value : 0.5f); - vrHudOriginY = cgs.glconfig.vidHeight * (vr_hudGroupY ? vr_hudGroupY->value : 0.62f); + vrHudOriginY = cgs.glconfig.vidHeight * (vr_hudGroupY ? vr_hudGroupY->value : 0.92f); // // A backing plate, so the whole group reads as one object against whatever @@ -728,12 +744,13 @@ void CG_DrawVRHud(void) } { - const float frac = health / maxHealth; + const float frac = Com_Clamp(0.0f, 1.0f, health / maxHealth); CG_VRHudText(VRHUD_HEALTH_X, VRHUD_BAR_Y - 17.0f, "HEALTH", 0.7f, 0.6f, 0.05f, alpha); // Green through amber to red, so the colour says as much as the length. CG_VRHudBar( - VRHUD_HEALTH_X, VRHUD_BAR_Y, frac, frac > 0.5f ? (2.0f - frac * 2.0f) : 1.0f, frac * 1.6f, 0.12f, alpha + VRHUD_HEALTH_X, VRHUD_BAR_Y, frac, frac > 0.5f ? (2.0f - frac * 2.0f) : 1.0f, + Com_Clamp(0.0f, 1.0f, frac * 1.6f), 0.12f, alpha ); CG_VRHudText( VRHUD_HEALTH_X + VRHUD_BAR_W + 8.0f, VRHUD_BAR_Y - 2.0f, va("%d", (int)health), 1.0f, 1.0f, 1.0f, alpha diff --git a/code/client/cl_scrn.cpp b/code/client/cl_scrn.cpp index d907e092..2d3db8e7 100644 --- a/code/client/cl_scrn.cpp +++ b/code/client/cl_scrn.cpp @@ -532,10 +532,12 @@ void SCR_SimpleUpdateScreen( void ) { const qboolean split = VR_WristPanelEnabled(); cl_vrDrawMode = split ? VRDRAW_WORLD : VRDRAW_ALL; - Cvar_Set( "vr_hudPass", split ? "2" : "0" ); + Cvar_Set( "vr_hudPass", split ? "2" : "3" ); for( eye = 0; eye < VR_MAX_EYES; eye++ ) { - VR_PrepareEye( eye ); + if( !VR_PrepareEye( eye ) ) { + continue; + } // Deliberately STEREO_CENTER for both eyes. // STEREO_LEFT/RIGHT select the GL_BACK_LEFT/RIGHT draw diff --git a/code/client/cl_ui.cpp b/code/client/cl_ui.cpp index 1f1a5613..5c2e6ccb 100644 --- a/code/client/cl_ui.cpp +++ b/code/client/cl_ui.cpp @@ -129,6 +129,7 @@ cvar_t *ui_compass_scale; // there, and it is barely visible. See UI_PlaceVRCompass. cvar_t *vr_compassX; cvar_t *vr_compassY; +static cvar_t *ui_vrHud; static intro_stage_t intro_stage; static char server_mapname[64]; @@ -1850,16 +1851,18 @@ puts it back. */ static void UI_PlaceVRCompass(void) { - static float appliedX = -1.0f; - static float appliedY = -1.0f; UIWidget *widget; UIRect2D frame; - if (!hud_compass || !vr_compassX || !vr_compassY) { +#ifdef USE_OPENXR + if (!VR_Enabled()) { return; } +#else + return; +#endif - if (vr_compassX->value == appliedX && vr_compassY->value == appliedY) { + if (!hud_compass || !vr_compassX || !vr_compassY) { return; } @@ -1877,18 +1880,26 @@ static void UI_PlaceVRCompass(void) frame = widget->getFrame(); - frame.pos.x = uid.vidWidth * vr_compassX->value - frame.size.width * 0.5f; - frame.pos.y = uid.vidHeight * vr_compassY->value - frame.size.height * 0.5f; - widget->setFrame(frame); - - appliedX = vr_compassX->value; - appliedY = vr_compassY->value; + // Compare the actual frame so a resize or recreated menu is repositioned + // even when the cvars have not changed. Avoid relayout for an unchanged frame. + const float x = uid.vidWidth * vr_compassX->value - frame.size.width * 0.5f; + const float y = uid.vidHeight * vr_compassY->value - frame.size.height * 0.5f; + if (frame.pos.x != x || frame.pos.y != y) { + frame.pos.x = x; + frame.pos.y = y; + widget->setFrame(frame); + } } void UI_Update(void) { Menu *currentMenu; UIRect2D frame; + qboolean vrHudActive = qfalse; + +#ifdef USE_OPENXR + vrHudActive = VR_Enabled() && ui_vrHud && ui_vrHud->integer; +#endif re.SetRenderTime(cls.realtime); CL_FillUIDef(); @@ -1922,11 +1933,10 @@ void UI_Update(void) // are drawn again, together and in the middle, by CG_DrawVRHud, and // leaving these up as well would draw everything twice. // - // Read straight from the cvar rather than cached, because it is - // archived and can be changed at any time from autoexec.cfg - and - // this has to be able to go back to the retail HUD without a rebuild. + // Read the current cvar value so the retail HUD can be restored + // without a rebuild. Desktop frames always keep their own HUD. // - if (ui_hud && !view3d->LetterboxActive() && !Cvar_Get("vr_hud", "1", CVAR_ARCHIVE)->integer) { + if (ui_hud && !view3d->LetterboxActive() && !vrHudActive) { // draw the health hud if (hud_health) { hud_health->ForceShow(); @@ -2076,7 +2086,7 @@ void UI_Update(void) // thing that covers both routes. if (!ui_hud || clc.state != CA_ACTIVE || view3d->LetterboxActive() || (currentMenu && currentMenu->isFullscreen()) || server_loading || ((cl.snap.ps.pm_flags & PMF_NO_HUD) || (cl.snap.ps.pm_flags & PMF_INTERMISSION)) - || Cvar_Get("vr_hud", "1", CVAR_ARCHIVE)->integer) { + || vrHudActive) { if (crosshairhud) { crosshairhud->ForceHide(); } @@ -4014,6 +4024,7 @@ void UI_ResolutionChange(void) vr_compassX = Cvar_Get("vr_compassX", "0.5", CVAR_ARCHIVE); vr_compassY = Cvar_Get("vr_compassY", "0.62", CVAR_ARCHIVE); + ui_vrHud = Cvar_Get("vr_hud", "1", CVAR_ARCHIVE); CL_FillUIImports(); CL_FillUIDef(); diff --git a/code/client/cl_uiview3d.cpp b/code/client/cl_uiview3d.cpp index 0c9f64a7..b75f3eac 100644 --- a/code/client/cl_uiview3d.cpp +++ b/code/client/cl_uiview3d.cpp @@ -50,9 +50,11 @@ View3D::View3D() setBorderStyle(border_none); AllowActivate(true); - m_printfadetime = 0.0; - m_print_mat = NULL; - m_locationprint = qfalse; + m_printfadetime = 0.0; + m_printUpdateTime = cls.realtime; + m_subtitleUpdateTime = cls.realtime; + m_print_mat = NULL; + m_locationprint = qfalse; } void View3D::UpdateCenterPrint(const char *s, float alpha) @@ -65,19 +67,21 @@ void View3D::UpdateCenterPrint(const char *s, float alpha) m_print_mat = NULL; } - m_printalpha = alpha; - m_printfadetime = 4000.0; - m_locationprint = qfalse; + m_printalpha = alpha; + m_printfadetime = 4000.0; + m_printUpdateTime = cls.realtime; + m_locationprint = qfalse; } void View3D::UpdateLocationPrint(int x, int y, const char *s, float alpha) { - m_printstring = s; - m_printalpha = alpha; - m_printfadetime = 4000.0; - m_x_coord = x; - m_y_coord = y; - m_locationprint = qtrue; + m_printstring = s; + m_printalpha = alpha; + m_printfadetime = 4000.0; + m_printUpdateTime = cls.realtime; + m_x_coord = x; + m_y_coord = y; + m_locationprint = qtrue; } void View3D::FrameInitialized(void) @@ -647,6 +651,17 @@ void View3D::Draw2D(void) #endif } +// Fixed in OPM +// Advance by elapsed client time, not by the number of rendered eyes. +void View3D::UpdatePrintTime(void) +{ + const int elapsed = cls.realtime - m_printUpdateTime; + m_printUpdateTime = cls.realtime; + if (elapsed > 0) { + m_printfadetime = Q_max(0.0f, m_printfadetime - elapsed); + } +} + void View3D::CenterPrint(void) { float alpha; @@ -655,6 +670,7 @@ void View3D::CenterPrint(void) float x, y; float w, h; + UpdatePrintTime(); if (!m_printfadetime) { return; } @@ -703,12 +719,6 @@ void View3D::CenterPrint(void) re.DrawStretchPic(x, y, w, h, 0, 0, 1, 1, mat); } - - m_printfadetime -= cls.frametime; - - if (m_printfadetime < 0) { - m_printfadetime = 0; - } } void View3D::LocationPrint(void) @@ -720,6 +730,7 @@ void View3D::LocationPrint(void) float alpha; UIRect2D frame; + UpdatePrintTime(); if (!m_printfadetime) { m_locationprint = false; return; @@ -774,11 +785,6 @@ void View3D::LocationPrint(void) ); m_font->setColor(UBlack); - m_printfadetime -= cls.frametime; - - if (m_printfadetime < 0) { - m_printfadetime = 0; - } } void View3D::DrawLetterbox(void) @@ -821,11 +827,10 @@ void View3D::DrawFades(void) void View3D::Draw(void) { - const int drawStart = Sys_Milliseconds(); #ifdef USE_OPENXR + const int drawStart = Sys_Milliseconds(); VR_TraceEvent(VRTRACE_VIEW3D); #endif - int hudStart; // // Added in OPM @@ -838,8 +843,6 @@ void View3D::Draw(void) SCR_DrawScreenField(); } - hudStart = Sys_Milliseconds(); - if (cl_vrDrawMode == VRDRAW_WORLD) { // // Added in OPM @@ -847,8 +850,16 @@ void View3D::Draw(void) // the scope surround, which have to stay with the world - see // CG_Draw2D, which draws only those while vr_hudPass says so. // +#ifdef USE_OPENXR + int hudStart = Sys_Milliseconds(); +#endif if (cge && !cls.no_menus) { set2D(); + // Set2DWindow flushes the queued world commands. Charge that + // work to the world before starting the HUD timer. +#ifdef USE_OPENXR + hudStart = Sys_Milliseconds(); +#endif cge->CG_Draw2D(); } #ifdef USE_OPENXR @@ -857,20 +868,14 @@ void View3D::Draw(void) return; } -#ifdef USE_OPENXR - { - const int setupStart = Sys_Milliseconds(); - - set2D(); - re.SavePerformanceCounters(); - - VR_TraceHudParts(Sys_Milliseconds() - setupStart, 0, 0, 0, 0); - } -#else set2D(); - re.SavePerformanceCounters(); +#ifdef USE_OPENXR + // The transition issues the pending world draw calls; starting the timer + // before it incorrectly reports those calls as HUD setup time. + const int hudStart = Sys_Milliseconds(); #endif + re.SavePerformanceCounters(); Draw2D(); @@ -885,6 +890,7 @@ void View3D::InitSubtitle(void) { float totalWidth; + m_subtitleUpdateTime = cls.realtime; for (int i = 0; i < 4; i++) { subs[i] = Cvar_Get(va("subtitle%d", i), "", 0); teams[i] = Cvar_Get(va("subteam%d", i), "0", 0); @@ -907,6 +913,9 @@ void View3D::DrawSubtitleOverlay(void) int i; float minX, maxX; int line; + const int elapsed = Q_max(0, cls.realtime - m_subtitleUpdateTime); + + m_subtitleUpdateTime = cls.realtime; subAlpha = Cvar_Get("subAlpha", "0.5", 0); @@ -918,6 +927,15 @@ void View3D::DrawSubtitleOverlay(void) fadeTime[i] = 2500 * ((strlen(subs[i]->string) / 68) + 1.f) + 1500; subLife[i] = fadeTime[i]; Q_strncpyz(oldStrings[i], subs[i]->string, sizeof(oldStrings[i])); + } else { + fadeTime[i] = Q_max(0.0f, fadeTime[i] - elapsed); + } + + if (fadeTime[i] <= 0) { + oldStrings[i][0] = 0; + if (subs[i]->string && subs[i]->string[0]) { + Cvar_Set(va("subtitle%d", i), ""); + } } if (fadeTime[i] > subLife[i] - 750.f) { @@ -927,17 +945,6 @@ void View3D::DrawSubtitleOverlay(void) } else { alpha[i] = 1.f; } - - fadeTime[i] -= cls.frametime; - if (fadeTime[i] < 0) { - // Clear the subtitle - fadeTime[i] = 0; - oldStrings[i][0] = 0; - - if (subs[i]->string && subs[i]->string[0]) { - Cvar_Set(va("subtitle%d", i), ""); - } - } } minX = m_screenframe.size.height - m_font->getHeight(getHighResScale()) * 10; diff --git a/code/client/cl_uiview3d.h b/code/client/cl_uiview3d.h index ef11c986..8faae990 100644 --- a/code/client/cl_uiview3d.h +++ b/code/client/cl_uiview3d.h @@ -25,6 +25,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA class View3D : public UIWidget { float m_printfadetime; + int m_printUpdateTime; + int m_subtitleUpdateTime; float m_printalpha; str m_printstring; UIReggedMaterial *m_print_mat; @@ -47,6 +49,7 @@ protected: void DrawSoundOverlay(void); void CenterPrint(void); void LocationPrint(void); + void UpdatePrintTime(void); void OnActivate(Event *ev); void OnDeactivate(Event *ev); void DrawSubtitleOverlay(void); diff --git a/code/renderergl1/tr_backend.c b/code/renderergl1/tr_backend.c index bfbf0f2a..250c02d6 100644 --- a/code/renderergl1/tr_backend.c +++ b/code/renderergl1/tr_backend.c @@ -1505,7 +1505,7 @@ void RB_ExecuteRenderCommands( const void *data ) { default: // stop rendering t2 = ri.Milliseconds (); - backEnd.pc.msec = t2 - t1; + backEnd.pc.msec += t2 - t1; return; } } diff --git a/code/renderergl1/tr_cmds.c b/code/renderergl1/tr_cmds.c index c8f97ebf..9ce72863 100644 --- a/code/renderergl1/tr_cmds.c +++ b/code/renderergl1/tr_cmds.c @@ -108,7 +108,10 @@ void R_IssueRenderCommands( qboolean runPerformanceCounters ) { cmdList->used = 0; if ( runPerformanceCounters ) { + int backEndMsec = backEnd.pc.msec; R_PerformanceCounters(); + // EndFrame still needs the time spent in earlier command batches. + backEnd.pc.msec = backEndMsec; } // actually start the commands going @@ -127,7 +130,7 @@ Issue any pending commands and wait for them to complete. ==================== */ void R_IssuePendingRenderCommands( void ) { - if ( !tr.registered ) { + if ( !tr.registered || !backEndData->commands.used ) { return; } R_IssueRenderCommands( qfalse ); diff --git a/code/renderergl1/tr_font.cpp b/code/renderergl1/tr_font.cpp index fbe27b77..89901a2d 100644 --- a/code/renderergl1/tr_font.cpp +++ b/code/renderergl1/tr_font.cpp @@ -535,10 +535,10 @@ void R_LoadFontShader(fontheader_sgl_t* font) { for (i = 0; i < fontshader->numUnfoggedPasses; i++) { - if (fontshader->unfoggedStages[0] != NULL && fontshader->unfoggedStages[0]->active) + if (fontshader->unfoggedStages[i] != NULL && fontshader->unfoggedStages[i]->active) { - fontshader->unfoggedStages[0]->rgbGen = CGEN_GLOBAL_COLOR; - fontshader->unfoggedStages[0]->alphaGen = AGEN_GLOBAL_ALPHA; + fontshader->unfoggedStages[i]->rgbGen = CGEN_GLOBAL_COLOR; + fontshader->unfoggedStages[i]->alphaGen = AGEN_GLOBAL_ALPHA; } } diff --git a/code/renderergl1/tr_main.c b/code/renderergl1/tr_main.c index 32bcea21..d5f79b32 100644 --- a/code/renderergl1/tr_main.c +++ b/code/renderergl1/tr_main.c @@ -1520,10 +1520,14 @@ void R_AddSpriteSurfaces() tr.currentEntityNum = ENTITYNUM_WORLD; tr.shiftedEntityNum = tr.currentEntityNum << QSORT_ENTITYNUM_SHIFT; - if (sprite->hModel && sprite->hModel < tr.numModels) { + if (sprite->hModel > 0 && sprite->hModel < tr.numModels) { tr.currentModel = &tr.models[sprite->hModel]; } else { - tr.currentModel = &tr.models[0]; + continue; + } + + if (tr.currentModel->type != MOD_SPRITE || !tr.currentModel->d.sprite) { + continue; } R_AdjustVisBoundsForSprite(&tr.refdef.sprites[tr.currentSpriteNum], &tr.viewParms, &tr.ori ); diff --git a/code/renderergl1/tr_sprite.c b/code/renderergl1/tr_sprite.c index 763c0c56..5a47aec1 100644 --- a/code/renderergl1/tr_sprite.c +++ b/code/renderergl1/tr_sprite.c @@ -61,13 +61,13 @@ static int CullSprite(const vec3_t* points) { for (i = 0; i < backEnd.viewParms.fog.extrafrustums + 4; i++) { for (j = 0; j < 4; j++) { - if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) - backEnd.viewParms.frustum[i].dist > 0.0f) { + if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) >= backEnd.viewParms.frustum[i].dist) { break; } } if (j == 4) { - return CULL_CLIP; + return CULL_OUT; } } @@ -87,15 +87,15 @@ void RB_DrawSprite( const refSprite_t *spr ) { float org_x, org_y; float scale; - if (!spr->hModel) + if (spr->hModel <= 0 || spr->hModel >= tr.numModels) { ri.Printf(PRINT_WARNING, "No model found for Sprite\n"); return; } - model = tr.models; - if (spr->hModel > 0 && spr->hModel < tr.numModels) { - model = &tr.models[spr->hModel]; + model = &tr.models[spr->hModel]; + if (model->type != MOD_SPRITE || !model->d.sprite) { + return; } scale = spr->scale * model->d.sprite->scale; @@ -108,10 +108,16 @@ void RB_DrawSprite( const refSprite_t *spr ) { float cr, sr; int i; - invmag = 1.0 / sqrt(spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1]); - - cr = invmag * spr->axis[1][1]; - sr = invmag * spr->axis[1][2]; + invmag = spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1]; + if (invmag > 0.0f) { + invmag = 1.0f / sqrt(invmag); + cr = invmag * spr->axis[1][1]; + sr = invmag * spr->axis[1][2]; + } else { + // No projected roll axis: keep the quad finite and unrotated. + cr = 1.0f; + sr = 0.0f; + } for (i = 0; i < 3; i++) { @@ -158,9 +164,9 @@ void RB_DrawSprite( const refSprite_t *spr ) { VectorCopy(backEnd.viewParms.ori.axis[0], norm); VectorCopy(backEnd.viewParms.ori.axis[1], right); VectorCopy(backEnd.viewParms.ori.axis[2], up); - if (backEnd.viewParms.ori.axis[0][2] > 0.999) { - return; - } else if (backEnd.viewParms.ori.axis[0][2] < -0.999) { + // VR facing comes from the viewer's position, so looking vertically + // does not make an otherwise visible upright sprite degenerate. + if (!vrView.active && fabs(backEnd.viewParms.ori.axis[0][2]) > 0.999f) { return; } @@ -179,8 +185,9 @@ void RB_DrawSprite( const refSprite_t *spr ) { if (VectorNormalize(toViewer) > 0.001f) { VectorSet(right, -toViewer[1], toViewer[0], 0.0f); } else { - VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f); - VectorNormalize(right); + // Directly above/below: use a stable axis, even when the view + // direction is vertical too. + VectorSet(right, 1.0f, 0.0f, 0.0f); } } else { VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f); diff --git a/code/renderergl2/tr_backend.c b/code/renderergl2/tr_backend.c index 4ed603ff..1bb8f944 100644 --- a/code/renderergl2/tr_backend.c +++ b/code/renderergl2/tr_backend.c @@ -2046,7 +2046,7 @@ void RB_ExecuteRenderCommands( const void *data ) { // stop rendering t2 = ri.Milliseconds (); - backEnd.pc.msec = t2 - t1; + backEnd.pc.msec += t2 - t1; return; } } diff --git a/code/renderergl2/tr_cmds.c b/code/renderergl2/tr_cmds.c index e3ed5185..0e4c0075 100644 --- a/code/renderergl2/tr_cmds.c +++ b/code/renderergl2/tr_cmds.c @@ -116,7 +116,10 @@ void R_IssueRenderCommands( qboolean runPerformanceCounters ) { cmdList->used = 0; if ( runPerformanceCounters ) { + int backEndMsec = backEnd.pc.msec; R_PerformanceCounters(); + // EndFrame still needs the time spent in earlier command batches. + backEnd.pc.msec = backEndMsec; } // actually start the commands going @@ -135,7 +138,7 @@ Issue any pending commands and wait for them to complete. ==================== */ void R_IssuePendingRenderCommands( void ) { - if ( !tr.registered ) { + if ( !tr.registered || (!backEndData->commands.used && !tess.numIndexes) ) { return; } R_IssueRenderCommands( qfalse ); diff --git a/code/renderergl2/tr_font.cpp b/code/renderergl2/tr_font.cpp index 37360699..cf49e5f1 100644 --- a/code/renderergl2/tr_font.cpp +++ b/code/renderergl2/tr_font.cpp @@ -533,10 +533,10 @@ void R_LoadFontShader(fontheader_sgl_t* font) { for (i = 0; i < fontshader->numUnfoggedPasses; i++) { - if (fontshader->stages[0] != NULL && fontshader->stages[0]->active) + if (fontshader->stages[i] != NULL && fontshader->stages[i]->active) { - fontshader->stages[0]->rgbGen = CGEN_ENTITY; - fontshader->stages[0]->alphaGen = AGEN_ENTITY; + fontshader->stages[i]->rgbGen = CGEN_ENTITY; + fontshader->stages[i]->alphaGen = AGEN_ENTITY; } } diff --git a/code/renderergl2/tr_main.c b/code/renderergl2/tr_main.c index cdb69f70..ce271063 100644 --- a/code/renderergl2/tr_main.c +++ b/code/renderergl2/tr_main.c @@ -3154,13 +3154,13 @@ void R_AddSpriteSurfaces() tr.currentEntityNum = ENTITYNUM_WORLD; tr.shiftedEntityNum = tr.currentEntityNum << QSORT_REFENTITYNUM_SHIFT; - if (sprite->hModel && sprite->hModel < tr.numModels) { + if (sprite->hModel > 0 && sprite->hModel < tr.numModels) { tr.currentModel = tr.models[sprite->hModel]; } else { - tr.currentModel = tr.models[0]; + continue; } - if (tr.currentModel->type != MOD_SPRITE) { + if (tr.currentModel->type != MOD_SPRITE || !tr.currentModel->d.sprite) { continue; } diff --git a/code/renderergl2/tr_sprite.c b/code/renderergl2/tr_sprite.c index 26069ca7..8ed4d075 100644 --- a/code/renderergl2/tr_sprite.c +++ b/code/renderergl2/tr_sprite.c @@ -61,13 +61,13 @@ static int CullSprite(const vec3_t* points) { for (i = 0; i < backEnd.viewParms.fog.extrafrustums + 4; i++) { for (j = 0; j < 4; j++) { - if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) - backEnd.viewParms.frustum[i].dist > 0.0f) { + if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) >= backEnd.viewParms.frustum[i].dist) { break; } } if (j == 4) { - return CULL_CLIP; + return CULL_OUT; } } @@ -88,15 +88,15 @@ void RB_DrawSprite( const refSprite_t *spr ) { float scale; int i; - if (!spr->hModel) + if (spr->hModel <= 0 || spr->hModel >= tr.numModels) { ri.Printf(PRINT_WARNING, "No model found for Sprite\n"); return; } - model = tr.models; - if (spr->hModel > 0 && spr->hModel < tr.numModels) { - model = tr.models[spr->hModel]; + model = tr.models[spr->hModel]; + if (model->type != MOD_SPRITE || !model->d.sprite) { + return; } scale = spr->scale * model->d.sprite->scale; @@ -109,10 +109,16 @@ void RB_DrawSprite( const refSprite_t *spr ) { float cr, sr; int i; - invmag = 1.0 / sqrt(spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1]); - - cr = invmag * spr->axis[1][1]; - sr = invmag * spr->axis[1][2]; + invmag = spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1]; + if (invmag > 0.0f) { + invmag = 1.0f / sqrt(invmag); + cr = invmag * spr->axis[1][1]; + sr = invmag * spr->axis[1][2]; + } else { + // No projected roll axis: keep the quad finite and unrotated. + cr = 1.0f; + sr = 0.0f; + } for (i = 0; i < 3; i++) { @@ -159,9 +165,9 @@ void RB_DrawSprite( const refSprite_t *spr ) { VectorCopy(backEnd.viewParms.ori.axis[0], norm); VectorCopy(backEnd.viewParms.ori.axis[1], right); VectorCopy(backEnd.viewParms.ori.axis[2], up); - if (backEnd.viewParms.ori.axis[0][2] > 0.999) { - return; - } else if (backEnd.viewParms.ori.axis[0][2] < -0.999) { + // VR facing comes from the viewer's position, so looking vertically + // does not make an otherwise visible upright sprite degenerate. + if (!vrView.active && fabs(backEnd.viewParms.ori.axis[0][2]) > 0.999f) { return; } @@ -180,8 +186,9 @@ void RB_DrawSprite( const refSprite_t *spr ) { if (VectorNormalize(toViewer) > 0.001f) { VectorSet(right, -toViewer[1], toViewer[0], 0.0f); } else { - VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f); - VectorNormalize(right); + // Directly above/below: use a stable axis, even when the view + // direction is vertical too. + VectorSet(right, 1.0f, 0.0f, 0.0f); } } else { VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f); diff --git a/code/vr/vr_common.h b/code/vr/vr_common.h index 6db3bb5c..b2ff05d4 100644 --- a/code/vr/vr_common.h +++ b/code/vr/vr_common.h @@ -75,13 +75,15 @@ void VR_DestroySession(void); // The per-eye render target size the runtime asks for. void VR_GetRenderResolution(int *width, int *height); -// Begins a frame and updates the tracked poses. Returns false when the session -// is not running, meaning nothing should be rendered or submitted this frame. +// Begins a frame and updates the tracked poses. False means skip rendering +// (inactive session, hidden frame, or unavailable tracking). Still call +// VR_SubmitFrame to end any begun frame with no layers. qboolean VR_BeginFrame(void); // Bracket each eye's rendering. Between them the engine draws its scene into // the framebuffer VR_PrepareEye bound. -void VR_PrepareEye(int eye); +// False if the swapchain image is not ready; skip drawing and finishing it. +qboolean VR_PrepareEye(int eye); void VR_FinishEye(int eye); // True when the frame is flat content - menus, the loading screen, a cinematic diff --git a/code/vr/vr_openxr.c b/code/vr/vr_openxr.c index f195d2ee..b4f55662 100644 --- a/code/vr/vr_openxr.c +++ b/code/vr/vr_openxr.c @@ -355,6 +355,7 @@ typedef struct { GLuint *frameBuffers; uint32_t acquiredIndex; qboolean acquired; + qboolean ready; } vrSwapchain_t; static struct { @@ -394,6 +395,7 @@ static struct { GLuint uiFramebuffer; qboolean screenLayerActive; qboolean layerReady; + uint32_t finishedEyeMask; XrPosef screenAnchor; qboolean screenAnchorValid; @@ -674,6 +676,7 @@ static const vrHolster_t vrHolsters[] = { static cvar_t *vr_traceTracking; static cvar_t *vr_traceFrame; +static cvar_t *vr_traceGpu; static cvar_t *vr_captureEye; static cvar_t *vr_reloadTapMs; static cvar_t *vr_holsters; @@ -1072,6 +1075,9 @@ qboolean VR_Init(void) // On by default while the frame budget is still an open question; there is // no console in the headset to turn it on with when it is wanted. vr_traceFrame = Cvar_Get("vr_traceFrame", "1", 0); + // Synchronous GPU timing stalls both eye passes. Keep it separate from + // ordinary frame tracing so measuring CPU time does not change the budget. + vr_traceGpu = Cvar_Get("vr_traceGpu", "0", 0); // Read by the game, which otherwise clamps where the player may look while // they are climbing - see Player::PlayerAngles. Cvar_Set2("vr_freeLook", "1", qtrue); @@ -1290,9 +1296,69 @@ static void VR_DestroySwapchain(vrSwapchain_t *swapchain) } swapchain->acquired = qfalse; + swapchain->ready = qfalse; swapchain->imageCount = 0; } +/* +An acquired image may still belong to the compositor until its wait succeeds. +XR_TIMEOUT_EXPIRED is a success-class result, but does not permit rendering or +release. Keep that image pending and retry its wait on the next frame instead +of acquiring another image and eventually exhausting the swapchain. +*/ +static qboolean VR_AcquireSwapchain(vrSwapchain_t *swapchain) +{ + XrSwapchainImageAcquireInfo acquireInfo; + XrSwapchainImageWaitInfo waitInfo; + XrResult result; + + if (!swapchain->acquired) { + memset(&acquireInfo, 0, sizeof(acquireInfo)); + acquireInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO; + if (!XR_CHECK(xrAcquireSwapchainImage(swapchain->handle, &acquireInfo, + &swapchain->acquiredIndex))) { + return qfalse; + } + swapchain->acquired = qtrue; + swapchain->ready = qfalse; + } + + if (!swapchain->ready) { + memset(&waitInfo, 0, sizeof(waitInfo)); + waitInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO; + waitInfo.timeout = 100 * 1000 * 1000; + result = xrWaitSwapchainImage(swapchain->handle, &waitInfo); + if (result == XR_TIMEOUT_EXPIRED || !VR_CheckResult(result, "xrWaitSwapchainImage")) { + return qfalse; + } + swapchain->ready = qtrue; + } + + return qtrue; +} + +static qboolean VR_ReleaseSwapchain(vrSwapchain_t *swapchain) +{ + XrSwapchainImageReleaseInfo releaseInfo; + + if (!swapchain->acquired || !swapchain->ready) { + return qfalse; + } + + // As in RTCWQuest's EFXR_SwapWindow: submit the commands without waiting + // for GPU completion. OpenXR synchronizes compositor access after release. + glFlush(); + memset(&releaseInfo, 0, sizeof(releaseInfo)); + releaseInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO; + if (!XR_CHECK(xrReleaseSwapchainImage(swapchain->handle, &releaseInfo))) { + return qfalse; + } + + swapchain->acquired = qfalse; + swapchain->ready = qfalse; + return qtrue; +} + /* ================== VR_InitMultisampling @@ -3893,6 +3959,11 @@ qboolean VR_BeginFrame(void) vr.frameStarted = qtrue; vr.viewsValid = qfalse; + vr.finishedEyeMask = 0; + vr.layerReady = qfalse; + vr.screenLayerActive = qfalse; + vr.pointerLayerReady = qfalse; + vr.wristLayerReady = qfalse; memset(&locateInfo, 0, sizeof(locateInfo)); locateInfo.type = XR_TYPE_VIEW_LOCATE_INFO; @@ -3905,6 +3976,7 @@ qboolean VR_BeginFrame(void) if (XR_CHECK(xrLocateViews(vr.session, &locateInfo, &viewState, VR_MAX_EYES, &viewCount, vr.views)) + && viewCount == VR_MAX_EYES && (viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) && (viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT)) { @@ -3957,7 +4029,7 @@ qboolean VR_BeginFrame(void) // both eyes, and on a menu frame into both eye images again. vr.beamVertexCount = VR_BuildBeamGeometry(vr.beamVerts, ARRAY_LEN(vr.beamVerts) / 3); - return vr.frameState.shouldRender ? qtrue : qfalse; + return (vr.frameState.shouldRender && vr.viewsValid) ? qtrue : qfalse; } /* @@ -3979,34 +4051,20 @@ void VR_GetEyeView(int eye, vrEyeView_t *view) VR_PrepareEye ================== */ -void VR_PrepareEye(int eye) +qboolean VR_PrepareEye(int eye) { vrSwapchain_t *swapchain; - XrSwapchainImageAcquireInfo acquireInfo; - XrSwapchainImageWaitInfo waitInfo; - if (!vr.frameStarted || eye < 0 || eye >= VR_MAX_EYES) { - return; + if (!vr.frameStarted || !vr.viewsValid || eye < 0 || eye >= VR_MAX_EYES) { + return qfalse; } swapchain = &vr.swapchains[eye]; - memset(&acquireInfo, 0, sizeof(acquireInfo)); - acquireInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO; - - if (!XR_CHECK(xrAcquireSwapchainImage(swapchain->handle, &acquireInfo, &swapchain->acquiredIndex))) { - return; + if (!VR_AcquireSwapchain(swapchain)) { + return qfalse; } - memset(&waitInfo, 0, sizeof(waitInfo)); - waitInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO; - waitInfo.timeout = 100 * 1000 * 1000; // 100ms; never block the frame loop forever - - if (!XR_CHECK(xrWaitSwapchainImage(swapchain->handle, &waitInfo))) { - return; - } - - swapchain->acquired = qtrue; vr.eyeStart = Sys_Milliseconds(); // Tell the renderer before moving the target, not after. @@ -4039,6 +4097,7 @@ void VR_PrepareEye(int eye) view->tanLeft, view->tanRight, view->tanUp, view->tanDown, vr.baseYaw, eye); } + return qtrue; } /* @@ -4246,7 +4305,7 @@ static void VR_CaptureEye(int eye) void VR_FinishEye(int eye) { vrSwapchain_t *swapchain; - XrSwapchainImageReleaseInfo releaseInfo; + qboolean released; if (!vr.frameStarted || eye < 0 || eye >= VR_MAX_EYES) { return; @@ -4254,7 +4313,7 @@ void VR_FinishEye(int eye) swapchain = &vr.swapchains[eye]; - if (!swapchain->acquired) { + if (!swapchain->acquired || !swapchain->ready) { return; } @@ -4300,13 +4359,20 @@ void VR_FinishEye(int eye) // Diagnostic only, and expensive by design: waiting for the GPU to actually // finish is the only way to tell work from queueing. Everything measured so // far is wall clock, which charges whichever call happens to block. - { + if (vr_traceGpu && vr_traceGpu->integer) { const int gpuStart = Sys_Milliseconds(); glFinish(); vr.phaseGpu += Sys_Milliseconds() - gpuStart; } + // The compositor only consumes color. Discard transient depth/stencil + // while this eye is still bound so the tiler need not store them to RAM. + { + const GLenum attachments[] = { GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT }; + glInvalidateFramebuffer(GL_FRAMEBUFFER, ARRAY_LEN(attachments), attachments); + } + if (re.SetDefaultFramebuffer) { re.SetDefaultFramebuffer(0); } @@ -4319,16 +4385,13 @@ void VR_FinishEye(int eye) re.SetVRView(NULL, NULL, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0); } - memset(&releaseInfo, 0, sizeof(releaseInfo)); - releaseInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO; - XR_CHECK(xrReleaseSwapchainImage(swapchain->handle, &releaseInfo)); - - swapchain->acquired = qfalse; + released = VR_ReleaseSwapchain(swapchain); vr.phaseEyes += Sys_Milliseconds() - vr.eyeStart; - - if (eye == VR_MAX_EYES - 1) { - vr.layerReady = qtrue; + if (!released) { + return; } + vr.finishedEyeMask |= 1u << eye; + vr.layerReady = vr.finishedEyeMask == (1u << VR_MAX_EYES) - 1u; vr.projectionViews[eye].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; vr.projectionViews[eye].next = NULL; @@ -4890,28 +4953,8 @@ static void VR_RenderPointerLayer(void) for (eye = 0; eye < VR_MAX_EYES; eye++) { vrSwapchain_t *swapchain = &vr.swapchains[eye]; - XrSwapchainImageAcquireInfo acquireInfo; - XrSwapchainImageWaitInfo waitInfo; - XrSwapchainImageReleaseInfo releaseInfo; - memset(&acquireInfo, 0, sizeof(acquireInfo)); - acquireInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO; - - if (!XR_CHECK(xrAcquireSwapchainImage(swapchain->handle, &acquireInfo, &swapchain->acquiredIndex))) { - return; - } - - memset(&releaseInfo, 0, sizeof(releaseInfo)); - releaseInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO; - - memset(&waitInfo, 0, sizeof(waitInfo)); - waitInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO; - waitInfo.timeout = 100 * 1000 * 1000; - - // Released even when the wait fails: the image is acquired either way, - // and holding one back starves the swapchain within a few frames. - if (!XR_CHECK(xrWaitSwapchainImage(swapchain->handle, &waitInfo))) { - XR_CHECK(xrReleaseSwapchainImage(swapchain->handle, &releaseInfo)); + if (!VR_AcquireSwapchain(swapchain)) { return; } @@ -4924,13 +4967,15 @@ static void VR_RenderPointerLayer(void) // menu is the one place the beam actually has a job. glClearColor(0.0f, 0.0f, 0.0f, 0.0f); VR_DepthMask(GL_TRUE); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); VR_DrawBeam(eye, vr.beamVerts, vr.beamVertexCount); VR_BindFramebuffer(0); - XR_CHECK(xrReleaseSwapchainImage(swapchain->handle, &releaseInfo)); + if (!VR_ReleaseSwapchain(swapchain)) { + return; + } vr.projectionViews[eye].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; vr.projectionViews[eye].next = NULL; @@ -5083,9 +5128,6 @@ VR_FinishScreenLayer void VR_FinishScreenLayer(void) { vrSwapchain_t *swapchain = &vr.uiSwapchain; - XrSwapchainImageAcquireInfo acquireInfo; - XrSwapchainImageWaitInfo waitInfo; - XrSwapchainImageReleaseInfo releaseInfo; if (!vr.frameStarted) { return; @@ -5098,18 +5140,7 @@ void VR_FinishScreenLayer(void) re.SetDefaultFramebuffer(0); } - memset(&acquireInfo, 0, sizeof(acquireInfo)); - acquireInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO; - - if (!XR_CHECK(xrAcquireSwapchainImage(swapchain->handle, &acquireInfo, &swapchain->acquiredIndex))) { - return; - } - - memset(&waitInfo, 0, sizeof(waitInfo)); - waitInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO; - waitInfo.timeout = 100 * 1000 * 1000; - - if (!XR_CHECK(xrWaitSwapchainImage(swapchain->handle, &waitInfo))) { + if (!VR_AcquireSwapchain(swapchain)) { return; } @@ -5130,11 +5161,9 @@ void VR_FinishScreenLayer(void) VR_BindFramebuffer(0); - memset(&releaseInfo, 0, sizeof(releaseInfo)); - releaseInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO; - XR_CHECK(xrReleaseSwapchainImage(swapchain->handle, &releaseInfo)); - - swapchain->acquired = qfalse; + if (!VR_ReleaseSwapchain(swapchain)) { + return; + } vr.screenLayerActive = qtrue; vr.layerReady = qtrue; @@ -5255,9 +5284,6 @@ VR_FinishWristPanel void VR_FinishWristPanel(void) { vrSwapchain_t *swapchain = &vr.uiSwapchain; - XrSwapchainImageAcquireInfo acquireInfo; - XrSwapchainImageWaitInfo waitInfo; - XrSwapchainImageReleaseInfo releaseInfo; vr.wristLayerReady = qfalse; @@ -5269,22 +5295,7 @@ void VR_FinishWristPanel(void) re.SetDefaultFramebuffer(0); } - memset(&acquireInfo, 0, sizeof(acquireInfo)); - acquireInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO; - - if (!XR_CHECK(xrAcquireSwapchainImage(swapchain->handle, &acquireInfo, &swapchain->acquiredIndex))) { - return; - } - - memset(&releaseInfo, 0, sizeof(releaseInfo)); - releaseInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO; - - memset(&waitInfo, 0, sizeof(waitInfo)); - waitInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO; - waitInfo.timeout = 100 * 1000 * 1000; - - if (!XR_CHECK(xrWaitSwapchainImage(swapchain->handle, &waitInfo))) { - XR_CHECK(xrReleaseSwapchainImage(swapchain->handle, &releaseInfo)); + if (!VR_AcquireSwapchain(swapchain)) { return; } @@ -5298,9 +5309,9 @@ void VR_FinishWristPanel(void) VR_BindFramebuffer(0); - XR_CHECK(xrReleaseSwapchainImage(swapchain->handle, &releaseInfo)); - - swapchain->acquired = qfalse; + if (!VR_ReleaseSwapchain(swapchain)) { + return; + } vr.wristLayerReady = qtrue; } @@ -5364,11 +5375,12 @@ static void VR_TraceFrameTiming(void) (vr.phaseEyes - vr.phaseWorld - vr.phaseHud) / frames); Com_Printf("VR paths: draw2d %d cgame2d %d updateviews %d view3d %d" - " | mode %d hudPass %d noMenus %d | res %ux%u | gpuwait %dms\n", + " | mode %d hudPass %d noMenus %d | res %ux%u | gpuwait %dms (enabled %d)\n", vr.traceEvents[VRTRACE_DRAW2D], vr.traceEvents[VRTRACE_CGAME_HUD], vr.traceEvents[VRTRACE_UPDATEVIEWS], vr.traceEvents[VRTRACE_VIEW3D], vr.traceDrawMode, vr.traceHudPass, vr.traceNoMenus, - vr.eyeWidth, vr.eyeHeight, vr.phaseGpu / frames); + vr.eyeWidth, vr.eyeHeight, vr.phaseGpu / frames, + vr_traceGpu && vr_traceGpu->integer ? 1 : 0); Com_Printf("VR hud: setup %dms fades %dms cgame %dms prints %dms overlays %dms tail %dms\n", vr.hudSetup / frames, vr.hudFades / frames, vr.phaseCgameHud / frames, diff --git a/misc/android/autoexec.cfg b/misc/android/autoexec.cfg index 6e623bef..0a06beeb 100644 --- a/misc/android/autoexec.cfg +++ b/misc/android/autoexec.cfg @@ -262,6 +262,10 @@ set vr_crosshair 0 // prints once. set vr_traceFrame 0 +// Opt-in synchronous GPU timing. This waits for GPU completion after each eye +// and changes frame performance; leave it off for normal play and CPU profiling. +set vr_traceGpu 0 + set vr_captureEye 0 // One line per use press, naming which of the three ways it can come up empty