From a8d859f38ba3c2e47b0ea228ad225cbaf42ea114 Mon Sep 17 00:00:00 2001 From: Yves Date: Mon, 5 Oct 2026 01:06:27 +0800 Subject: [PATCH] Match encoded desktop colors in OpenXR gameplay and native copies --- docs/OPENXR_RENDERING.md | 20 +++++++ runtime/include/openxr_color.h | 24 ++++++++ runtime/include/psx_openxr.h | 1 + runtime/src/debug_server.c | 4 +- runtime/src/gpu_gl_renderer.c | 79 ++++++++++++++++++++++-- runtime/src/psx_openxr.c | 12 ++-- runtime/tests/run_openxr_color_gl.py | 90 ++++++++++++++++++++++++++++ runtime/tests/test_openxr_color_gl.c | 75 +++++++++++++++++++++++ 8 files changed, 294 insertions(+), 11 deletions(-) create mode 100644 runtime/include/openxr_color.h create mode 100644 runtime/tests/run_openxr_color_gl.py create mode 100644 runtime/tests/test_openxr_color_gl.c diff --git a/docs/OPENXR_RENDERING.md b/docs/OPENXR_RENDERING.md index 692b663f..8fa14177 100644 --- a/docs/OPENXR_RENDERING.md +++ b/docs/OPENXR_RENDERING.md @@ -119,3 +119,23 @@ See [Third-Party Attribution](../THIRD_PARTY_ATTRIBUTION.md#openxr-sdk-loader--- for the Apache-2.0 SDK, MIT JsonCpp notices, cold-cache requirements and package distribution. Consuming this feature requires a framework pin update and runtime rebuild; it does not change guest code generation. + +## Headset color candidate (2026-10-05) + +`fix/vr-headset-color` corrects submission encoding. Native PSX/stereo textures +contain display-encoded RGB; the previous unconverted GL_RGBA8 swapchain made +the compositor interpret those values as linear. Prefer GL_SRGB8_ALPHA8, with +GL_FRAMEBUFFER_SRGB disabled during the copy and restored afterward. If only +GL_RGBA8 is available, the copy shader explicitly decodes sRGB instead. + +Gameplay applies desktop post-gamma once before encoding/decoding. Native +GL_BACK already contains that adjustment and is copied without applying it +again. Neither path changes guest GPU lighting, textures or simulation. +`openxr_stats.swapchain_format` reports the selected GL enum (35907 sRGB, +32856 linear). The linear-only native fallback uses an intermediate copy. + +`runtime/tests/run_openxr_color_gl.py` runs the actual private copy functions +and shader against source-owned RGB/grayscale ramps in hidden real GL. Eight +format/gamma/incoming-sRGB-state combinations pass for gameplay and native +copies, including vertical orientation, alpha and GL state restoration. + diff --git a/runtime/include/openxr_color.h b/runtime/include/openxr_color.h new file mode 100644 index 00000000..ae1508f3 --- /dev/null +++ b/runtime/include/openxr_color.h @@ -0,0 +1,24 @@ +/* Host presentation only: PSX/display RGB values are already encoded. */ +#pragma once +#include +#define PSX_XR_RGBA8 0x8058 +#define PSX_XR_SRGB8_ALPHA8 0x8C43 +static inline int64_t psx_xr_color_format(const int64_t *formats, uint32_t count) { + int rgba = 0; + for (uint32_t i = 0; i < count; i++) { + if (formats[i] == PSX_XR_SRGB8_ALPHA8) return PSX_XR_SRGB8_ALPHA8; + if (formats[i] == PSX_XR_RGBA8) rgba = 1; + } + return rgba ? PSX_XR_RGBA8 : 0; +} +/* Used only when gamma must be applied or the runtime offers a linear target. + * Gamma precedes decoding, matching desktop presentation. Alpha is linear. */ +#define PSX_XR_COLOR_FS \ + "#version 330\n" \ + "in vec2 v_uv; uniform sampler2D u_tex; uniform float u_gamma;\n" \ + "uniform int u_linear; out vec4 frag;\n" \ + "void main(){ vec4 c=texture(u_tex,v_uv);\n" \ + " if(u_gamma != 1.0) c.rgb=pow(max(c.rgb,vec3(0.0)),vec3(1.0/u_gamma));\n" \ + " if(u_linear != 0) c.rgb=mix(c.rgb/12.92,\n" \ + " pow((c.rgb+0.055)/1.055,vec3(2.4)),greaterThan(c.rgb,vec3(0.04045)));\n" \ + " frag=c; }\n" diff --git a/runtime/include/psx_openxr.h b/runtime/include/psx_openxr.h index f0e324c8..1039d3ca 100644 --- a/runtime/include/psx_openxr.h +++ b/runtime/include/psx_openxr.h @@ -13,6 +13,7 @@ typedef struct PSXOpenXRStats { float ipd_m; double units_per_meter; uint64_t gl_version, min_gl_version, max_gl_version; + int64_t swapchain_format; /* GL_SRGB8_ALPHA8 preferred; GL_RGBA8 decoded copy */ char runtime[128]; float pose[2][7], fov[2][4]; PSXModRenderView view[2]; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 99d52013..be43e0a6 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8151,7 +8151,7 @@ static void handle_openxr_stats(int id, const char *json) { "\"last_failure\":\"%s\",\"last_failure_result\":%d,\"view_flags\":%llu," "\"waits\":%llu,\"submitted\":%llu,\"empty\":%llu,\"failures\":%llu," "\"predicted_time\":%llu,\"ipd_m\":%.6f,\"units_per_meter\":%.6f," - "\"gl_version\":%llu,\"min_gl_version\":%llu,\"max_gl_version\":%llu," + "\"gl_version\":%llu,\"min_gl_version\":%llu,\"max_gl_version\":%llu,\"swapchain_format\":%lld," "\"submitted_layer\":%u,\"quad_submitted\":%llu," "\"submitted_source\":%u,\"native_submitted\":%llu,\"submitted_native_frame\":%llu," "\"quad_distance_m\":%.6f,\"quad_width_m\":%.6f,\"quad_height_m\":%.6f,\"losses\":%llu}", id,s.compiled,s.enabled, @@ -8161,7 +8161,7 @@ static void handle_openxr_stats(int id, const char *json) { (unsigned long long)s.submitted,(unsigned long long)s.empty, (unsigned long long)s.failures,(unsigned long long)s.predicted_time,s.ipd_m,s.units_per_meter, (unsigned long long)s.gl_version,(unsigned long long)s.min_gl_version, - (unsigned long long)s.max_gl_version,s.submitted_layer, + (unsigned long long)s.max_gl_version,(long long)s.swapchain_format,s.submitted_layer, (unsigned long long)s.quad_submitted,s.submitted_source, (unsigned long long)s.native_submitted,(unsigned long long)s.submitted_native_frame, s.quad_distance_m,s.quad_width_m,s.quad_height_m,(unsigned long long)s.losses); diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 6eff257e..5019204b 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -91,6 +91,7 @@ #include "frame_pacing.h" #include "psx_rewind.h" #include "psx_openxr.h" +#include "openxr_color.h" #include "psx_sdl.h" #if defined(PSX_SDL3) @@ -163,6 +164,7 @@ typedef void (APIENTRY *PFN_glCompileShader)(GLuint); typedef void (APIENTRY *PFN_glGetShaderiv)(GLuint, GLenum, GLint *); typedef void (APIENTRY *PFN_glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, char *); typedef void (APIENTRY *PFN_glDeleteShader)(GLuint); +typedef void (APIENTRY *PFN_glDeleteProgram)(GLuint); typedef GLuint (APIENTRY *PFN_glCreateProgram)(void); typedef void (APIENTRY *PFN_glAttachShader)(GLuint, GLuint); typedef void (APIENTRY *PFN_glLinkProgram)(GLuint); @@ -226,6 +228,7 @@ static PFN_glCompileShader p_glCompileShader; static PFN_glGetShaderiv p_glGetShaderiv; static PFN_glGetShaderInfoLog p_glGetShaderInfoLog; static PFN_glDeleteShader p_glDeleteShader; +static PFN_glDeleteProgram p_glDeleteProgram; static PFN_glCreateProgram p_glCreateProgram; static PFN_glAttachShader p_glAttachShader; static PFN_glLinkProgram p_glLinkProgram; @@ -286,6 +289,7 @@ static int load_modern_gl(void) { LOAD(p_glCreateShader, "glCreateShader"); LOAD(p_glShaderSource, "glShaderSource"); LOAD(p_glCompileShader, "glCompileShader"); LOAD(p_glGetShaderiv, "glGetShaderiv"); LOAD(p_glGetShaderInfoLog, "glGetShaderInfoLog"); LOAD(p_glDeleteShader, "glDeleteShader"); + LOAD(p_glDeleteProgram, "glDeleteProgram"); LOAD(p_glCreateProgram, "glCreateProgram"); LOAD(p_glAttachShader, "glAttachShader"); LOAD(p_glLinkProgram, "glLinkProgram"); LOAD(p_glGetProgramiv, "glGetProgramiv"); LOAD(p_glGetProgramInfoLog, "glGetProgramInfoLog"); LOAD(p_glUseProgram, "glUseProgram"); @@ -416,6 +420,7 @@ static int s_present_w = 0, s_present_h = 0; static GLuint s_osd_tex = 0; static int s_osd_tw = 0, s_osd_th = 0; static GLuint s_present_prog = 0, s_present_vao = 0; +static GLuint s_xr_color_prog = 0, s_xr_native_tex = 0; static void gl_swap_with_osd(void); static int s_native_surface_enabled, s_native_surface_pending; static int s_native_surface_rect[4]; /* Fresh native backbuffer content, GL coordinates. */ @@ -6330,6 +6335,11 @@ static void pass_free_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo, * and journal, and forget their names so a new context makes fresh ones. */ static void pass_resources_release(void) { psx_openxr_shutdown(); + if(s_ctx) { + if(s_xr_color_prog) p_glDeleteProgram(s_xr_color_prog); + if(s_xr_native_tex) glDeleteTextures(1,&s_xr_native_tex); + } + s_xr_color_prog=s_xr_native_tex=0; stereo_resources_release(); pass_gen_release(0); pass_gen_release(1); @@ -6747,10 +6757,48 @@ int psx_mod_openxr_input(PSXModOpenXRInput *input) { int psx_mod_openxr_hands(PSXModOpenXRHands *hands) { return psx_openxr_hands(hands); /* Snapshot only, also safe during replay. */ } +/* The target stores display-encoded RGB for an sRGB swapchain, or explicitly + * decoded RGB for a linear-only runtime. Never apply hardware sRGB encoding + * to these outputs. All shader state is restored before returning to native. */ +static int openxr_color_draw(GLuint source, int w, int h, int flip, float gamma, int linear) { + if (!s_xr_color_prog) s_xr_color_prog = build_program(PRESENT_VS, PSX_XR_COLOR_FS); + if (!s_xr_color_prog || !s_present_vao) return 0; + GLint program, vao, viewport[4], active, binding, min_filter, mag_filter; + GLboolean mask[4]; + const GLenum caps[] = {GL_BLEND, GL_DEPTH_TEST, GL_STENCIL_TEST, GL_CULL_FACE}; + GLboolean enabled[4]; + glGetIntegerv(0x8B8D, &program);glGetIntegerv(0x85B5, &vao); + glGetIntegerv(GL_VIEWPORT, viewport);glGetIntegerv(0x84E0, &active); + glGetBooleanv(GL_COLOR_WRITEMASK, mask); + p_glActiveTexture(PSXGL_TEXTURE0);glGetIntegerv(GL_TEXTURE_BINDING_2D, &binding); + glBindTexture(GL_TEXTURE_2D, source); + glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &min_filter); + glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &mag_filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + for (int i=0;i<4;i++) {enabled[i]=glIsEnabled(caps[i]);glDisable(caps[i]);} + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);glViewport(0,0,w,h); + p_glUseProgram(s_xr_color_prog); + p_glUniform1i(p_glGetUniformLocation(s_xr_color_prog,"u_tex"),0); + p_glUniform1i(p_glGetUniformLocation(s_xr_color_prog,"u_linear"),linear); + p_glUniform1f(p_glGetUniformLocation(s_xr_color_prog,"u_gamma"),gamma); + p_glUniform4f(p_glGetUniformLocation(s_xr_color_prog,"u_uv_rect"),0,flip?0:1,1,flip?1:0); + p_glBindVertexArray(s_present_vao);glDrawArrays(GL_TRIANGLES,0,3); + p_glBindVertexArray((GLuint)vao);p_glUseProgram((GLuint)program); + glViewport(viewport[0],viewport[1],viewport[2],viewport[3]); + glColorMask(mask[0],mask[1],mask[2],mask[3]); + for (int i=0;i<4;i++) if(enabled[i]) glEnable(caps[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); + glBindTexture(GL_TEXTURE_2D,(GLuint)binding);p_glActiveTexture((GLenum)active); + return 1; +} static int openxr_copy_eye(uint32_t eye, uint32_t texture, int w, int h) { StereoPair *p = &s_stereo_pair[s_stereo_current]; GLint read_fbo, draw_fbo; GLuint target = 0; GLboolean scissor = glIsEnabled(GL_SCISSOR_TEST); + GLboolean srgb = glIsEnabled(0x8DB9); /* GL_FRAMEBUFFER_SRGB */ + PSXOpenXRStats xr;psx_openxr_stats(&xr); glGetIntegerv(0x8CAA, &read_fbo); glGetIntegerv(0x8CA6, &draw_fbo); p_glGenFramebuffers(1, &target); @@ -6759,17 +6807,23 @@ static int openxr_copy_eye(uint32_t eye, uint32_t texture, int w, int h) { int ok = p_glCheckFramebufferStatus(PSXGL_DRAW_FRAMEBUFFER) == PSXGL_FRAMEBUFFER_COMPLETE; if (ok) { glDisable(GL_SCISSOR_TEST); + glDisable(0x8DB9); p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, p->fbo[eye]); /* Stereo capture texture row convention is opposite the XR layer. * User confirmed inverted headset output with the original direct blit. * Flip only submission; eye dumps and desktop presentation stay intact. */ - p_glBlitFramebuffer(0, p->th[eye], p->tw[eye], 0, 0, 0, w, h, - GL_COLOR_BUFFER_BIT, GL_LINEAR); + if (xr.swapchain_format == PSX_XR_SRGB8_ALPHA8 && s_present_gamma == 1.0f) + p_glBlitFramebuffer(0, p->th[eye], p->tw[eye], 0, 0, 0, w, h, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + else + ok = openxr_color_draw(p->tex[eye], w, h, 1, s_present_gamma, + xr.swapchain_format != PSX_XR_SRGB8_ALPHA8); glFlush(); } p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, (GLuint)read_fbo); p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, (GLuint)draw_fbo); if (scissor) glEnable(GL_SCISSOR_TEST); + if (srgb) glEnable(0x8DB9); p_glDeleteFramebuffers(1, &target);return ok; } int psx_mod_openxr_end(int rendered) { @@ -6783,6 +6837,8 @@ int psx_mod_openxr_end(int rendered) { static int openxr_copy_native(uint32_t eye,uint32_t texture,int w,int h) { GLint read_fbo,draw_fbo,read_buffer;GLuint target=0; GLboolean scissor=glIsEnabled(GL_SCISSOR_TEST); + GLboolean srgb=glIsEnabled(0x8DB9); + PSXOpenXRStats xr;psx_openxr_stats(&xr); (void)eye; glGetIntegerv(0x8CAA,&read_fbo);glGetIntegerv(0x8CA6,&draw_fbo); glGetIntegerv(GL_READ_BUFFER,&read_buffer); @@ -6792,15 +6848,30 @@ static int openxr_copy_native(uint32_t eye,uint32_t texture,int w,int h) { if(ok) { int *r=s_native_surface_rect; glDisable(GL_SCISSOR_TEST);p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,0);glReadBuffer(GL_BACK); + glDisable(0x8DB9); /* Default framebuffer is already upright: unlike native VRAM/eye * textures, its bottom GL row is the displayed bottom row. */ (void)pass_gl_errors(); - p_glBlitFramebuffer(r[0],r[1],r[0]+r[2],r[1]+r[3],0,0,w,h,GL_COLOR_BUFFER_BIT,GL_LINEAR); - glFlush();ok=pass_gl_errors()==0; + if (xr.swapchain_format == PSX_XR_SRGB8_ALPHA8) + p_glBlitFramebuffer(r[0],r[1],r[0]+r[2],r[1]+r[3],0,0,w,h,GL_COLOR_BUFFER_BIT,GL_LINEAR); + else { + GLint active,binding;glGetIntegerv(0x84E0,&active); + p_glActiveTexture(PSXGL_TEXTURE0);glGetIntegerv(GL_TEXTURE_BINDING_2D,&binding); + if(!s_xr_native_tex) glGenTextures(1,&s_xr_native_tex); + glBindTexture(GL_TEXTURE_2D,s_xr_native_tex); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE); + glCopyTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,r[0],r[1],r[2],r[3],0); + glBindTexture(GL_TEXTURE_2D,(GLuint)binding);p_glActiveTexture((GLenum)active); + /* Desktop gamma is already baked into GL_BACK; decode it once. */ + ok=openxr_color_draw(s_xr_native_tex,w,h,0,1.0f,1); + } + glFlush();ok=ok && pass_gl_errors()==0; } p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,(GLuint)read_fbo);glReadBuffer((GLenum)read_buffer); p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER,(GLuint)draw_fbo); if(scissor)glEnable(GL_SCISSOR_TEST); + if(srgb)glEnable(0x8DB9); p_glDeleteFramebuffers(1,&target);return ok; } static void openxr_present_native(void) { diff --git a/runtime/src/psx_openxr.c b/runtime/src/psx_openxr.c index 4ca339a2..a9a2e0fb 100644 --- a/runtime/src/psx_openxr.c +++ b/runtime/src/psx_openxr.c @@ -8,6 +8,7 @@ * no other thread creates a GL context or touches XR. Hence no locking. */ #include "psx_openxr.h" #include "vr_pose_math.h" +#include "openxr_color.h" #include #include #include @@ -239,15 +240,16 @@ static int initialize(void) { uint32_t formats_n=0; if(!check(xrEnumerateSwapchainFormats(s_session,0,&formats_n,NULL),"formats"))return 0; int64_t *formats=calloc(formats_n,sizeof *formats);if(!formats){s_stats.stage="formats_memory";return 0;} - supported=0; - if(check(xrEnumerateSwapchainFormats(s_session,formats_n,&formats_n,formats),"formats")) - for(uint32_t i=0;i