VR: Fix inverted depth and GL gamma, keep the flat screen upright

- Depth was inverted on both backends: the VR shader replaced Z with the
  game's clip-space Z after the viewport transform, skipping its Z scale.
  Since the viewport transform moved into the shader, that's wrong. Replace
  it before the transform instead, as libretro already did.
- GL eye buffers are sRGB, and GLES encoded our sRGB colors again (too
  bright). Disable the encoding with GL_EXT_sRGB_write_control.
- Keep the flat screen upright instead of tilting it to the head's pitch at
  recenter time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-04 12:48:32 +01:00
1 parent abe2203657
commit 249b653962
4 files changed
+47 -14

No files matched your search

+28
View File
@@ -3,6 +3,29 @@
#if XR_USE_GRAPHICS_API_OPENGL || XR_USE_GRAPHICS_API_OPENGL_ES
#include "Common/GPU/OpenGL/GLCommon.h"
#include <cstring>
#ifndef GL_FRAMEBUFFER_SRGB_EXT
#define GL_FRAMEBUFFER_SRGB_EXT 0x8DB9
#endif
// GLFeatures.h clashes with the EGL macros the OpenXR headers bring in, so this asks GL itself.
static bool HasSRGBWriteControl() {
static int has = -1;
if (has < 0) {
GLint count = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
has = 0;
for (GLint i = 0; i < count; i++) {
const char *ext = (const char *)glGetStringi(GL_EXTENSIONS, i);
if (ext && !strcmp(ext, "GL_EXT_sRGB_write_control")) {
has = 1;
break;
}
}
}
return has == 1;
}
#endif
@@ -267,6 +290,11 @@ void* ovrFramebuffer_SetCurrent(ovrFramebuffer* frameBuffer) {
#if XR_USE_GRAPHICS_API_OPENGL_ES || XR_USE_GRAPHICS_API_OPENGL
if (UseGL()) {
GL(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frameBuffer->GLFrameBuffers[frameBuffer->TextureSwapChainIndex]));
// The swapchain is sRGB and our colors are sRGB-encoded already: GLES would encode them again on
// write (too bright). Context state, but the other framebuffers aren't sRGB, so it only affects this.
if (HasSRGBWriteControl()) {
GL(glDisable(GL_FRAMEBUFFER_SRGB_EXT));
}
}
#endif
return nullptr;
+3 -2
View File
@@ -206,8 +206,9 @@ void VR_Recenter(engine_t* engine) {
}
}
// Update menu orientation
VR_SetConfigFloat(VR_CONFIG_MENU_PITCH, hmdorientation.x);
// Update menu orientation. The screen stays upright: tilting it to the head's pitch at recenter
// time (often looking down while putting the headset on) left it at an angle for the session.
VR_SetConfigFloat(VR_CONFIG_MENU_PITCH, 0.0f);
VR_SetConfigFloat(VR_CONFIG_MENU_YAW, 0.0f);
}
+2 -10
View File
@@ -544,11 +544,10 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
WRITE(p, " vec4 outPos = mul(u_proj_lens, viewPos);\n");
WRITE(p, " vec4 orgPos = mul(u_proj, viewPos);\n");
#ifdef __LIBRETRO__
// Preserve game depth before clipping and viewport conversion. The VR lens
// changes XY, but depth must still agree with through-mode draws and clears.
// changes XY, but depth must still agree with through-mode draws and clears,
// and go through the viewport's Z scale (often negative on the PSP).
WRITE(p, " outPos.z = orgPos.z * (outPos.w / orgPos.w);\n");
#endif
} else {
WRITE(p, " vec4 outPos = mul(u_proj, viewPos);\n");
}
@@ -1020,13 +1019,6 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag
WRITE(p, " %sgl_Position = outPos;\n", compat.vsOutPrefix);
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
#ifndef __LIBRETRO__
// Z correction for the depth buffer
if (useHWTransform) {
WRITE(p, " %sgl_Position.z = orgPos.z / abs(orgPos.w) * abs(outPos.w);\n", compat.vsOutPrefix);
}
#endif
// HUD scaling
WRITE(p, " %sgl_Position.x *= u_scaleX;\n", compat.vsOutPrefix);
WRITE(p, " %sgl_Position.y *= u_scaleY;\n", compat.vsOutPrefix);
+14 -2
View File
@@ -74,7 +74,7 @@ only on dirty split GTA's scene into two layers moving at different speeds.
### The OpenGL-only parts, and their Vulkan versions
OpenGL VR goes once these are confirmed on a headset:
OpenGL VR stays an option for a while. What it does its own way, and the Vulkan equivalent:
- **Skyplane** (`compatvr.ini`, the GTA Stories games): `PreprocessSkyplane` edits GL render steps at
replay, `PreprocessSkyplaneVulkan` does the same to Vulkan steps in `PreprocessSteps`, before
@@ -95,6 +95,17 @@ The other VR compat flags (`ForceMono`, `ForceFlatScreen`, `IdentityViewHack`, `
`ProjectionHack`, `UnitsPerMeter`) are read in shared code (`PPSSPPVR.cpp`, the shader generator),
so they don't depend on the backend.
### Fixes found on the headset
- **Depth** was inverted, on GL too: the shader replaced the lens projection's Z with the game's
(`orgPos`) at the very end, skipping the viewport's Z scale, which broke when the viewport transform
moved into the shader. It's replaced before the transform now (libretro already did this).
- **Gamma on GL:** the GL eye swapchains are `GL_SRGB8_ALPHA8`, and GLES encodes writes to an sRGB
framebuffer, so our already sRGB-encoded colors came out too bright. Writes skip the encoding now
(`GL_EXT_sRGB_write_control`).
- **The flat screen** stays upright. It used to take the head's pitch at recenter time, often looking
down while putting the headset on. Recentering is the system gesture (hold the Meta button).
## Phase 2: PC VR
The desktop window (Windows, then Linux) stays the primary swapchain and shows a mirror. The
@@ -105,5 +116,6 @@ becomes the non-pacing one. The OpenXR loader on desktop is our dynamic one (`Op
## Status
- [ ] Phase 1
- [x] Phase 1: runs on a Quest 2 (menus, Burnout Legends, GTA LCS in flat mode, stereo and 6DoF on)
at 72 fps. Skyplane not checked specifically yet.
- [ ] Phase 2