Match encoded desktop colors in OpenXR gameplay and native copies

This commit is contained in:
Yves
2026-10-05 01:06:27 +08:00
parent 5a4a5ee94c
commit a8d859f38b
8 changed files with 294 additions and 11 deletions
+20
View File
@@ -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.
+24
View File
@@ -0,0 +1,24 @@
/* Host presentation only: PSX/display RGB values are already encoded. */
#pragma once
#include <stdint.h>
#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"
+1
View File
@@ -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];
+2 -2
View File
@@ -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);
+75 -4
View File
@@ -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) {
+7 -5
View File
@@ -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 <string.h>
#include <stdlib.h>
#include <math.h>
@@ -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<formats_n;i++)if(formats[i]==GL_RGBA8)supported=1;
if(!check(xrEnumerateSwapchainFormats(s_session,formats_n,&formats_n,formats),"formats")) {
free(formats);return 0;
}
s_stats.swapchain_format=psx_xr_color_format(formats,formats_n);
free(formats);
if(!supported){s_stats.stage="rgba8_format_missing";return 0;}
if(!s_stats.swapchain_format){s_stats.stage="color_format_missing";return 0;}
for(int eye=0;eye<2;eye++) {
s_w[eye]=config[eye].recommendedImageRectWidth;s_h[eye]=config[eye].recommendedImageRectHeight;
XrSwapchainCreateInfo sc={XR_TYPE_SWAPCHAIN_CREATE_INFO};
sc.usageFlags=XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;sc.format=GL_RGBA8;
sc.usageFlags=XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;sc.format=s_stats.swapchain_format;
sc.sampleCount=1;sc.width=s_w[eye];sc.height=s_h[eye];sc.faceCount=1;sc.arraySize=1;sc.mipCount=1;
if(!check(xrCreateSwapchain(s_session,&sc,&s_chain[eye]),"swapchain"))return 0;
if(!check(xrEnumerateSwapchainImages(s_chain[eye],0,&s_count[eye],NULL),"images"))return 0;
+90
View File
@@ -0,0 +1,90 @@
"""Run actual renderer XR-copy functions against a source-owned ramp on real GL.
Extract the private submission functions verbatim into an isolated fixture:
Windows linkers otherwise retain unrelated public renderer entry points.
No mocks of OpenGL or shader/color conversion are used.
"""
import argparse
import json
import pathlib
import platform
import subprocess
def function(source, signature):
start = source.index(signature)
brace = source.index("{", start)
while source.index(";", start) < brace:
start = source.index(signature, start + 1)
brace = source.index("{", start)
depth = 0
for pos in range(brace, len(source)):
if source[pos] == "{":
depth += 1
elif source[pos] == "}":
depth -= 1
if not depth:
return source[start:pos + 1]
raise ValueError(signature)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cc", default="cc")
parser.add_argument("--sdl-include", required=True)
parser.add_argument("--sdl-library", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
root = pathlib.Path(__file__).resolve().parents[2]
dest = pathlib.Path(args.output).resolve()
dest.mkdir(parents=True, exist_ok=True)
source = (root / "runtime/src/gpu_gl_renderer.c").read_text(encoding="utf-8")
loader = function(source, "static int load_modern_gl(void)")
prefix = source[:source.index("static int load_modern_gl(void)")] + loader
vs_start = source.index("static const char *PRESENT_VS")
vs = source[vs_start:source.index("/* Present sampling", vs_start)]
pair_start = source.index("typedef struct StereoPair")
pair = source[pair_start:source.index("} StereoPair;", pair_start) + len("} StereoPair;")]
declarations = """
static SDL_GLContext s_ctx;
static GLuint s_present_vao, s_xr_color_prog, s_xr_native_tex;
static float s_present_gamma=1.0f;
static GLenum s_last_fbo_status;
static StereoPair s_stereo_pair[2];
static int s_stereo_current, s_native_surface_rect[4];
"""
signatures = ["static GLuint compile_shader(", "static GLuint build_program_ex(",
"static GLuint build_program(", "static GLuint make_tex(",
"static int make_fbo(", "static uint32_t pass_gl_errors(",
"static int openxr_color_draw(", "static int openxr_copy_eye(",
"static int openxr_copy_native("]
header = prefix + vs + pair + declarations + "\n".join(function(source, s) for s in signatures)
(dest / "openxr_color_fixture_renderer.h").write_text(header, encoding="utf-8")
exe = dest / ("openxr_color.exe" if platform.system() == "Windows" else "openxr_color")
cmd = [args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
"-DSDL_MAIN_HANDLED", "-I" + str(root / "runtime/include"),
"-I" + str(root / "runtime/src"),
"-I" + args.sdl_include, "-I" + str(dest),
str(root / "runtime/tests/test_openxr_color_gl.c"), args.sdl_library,
"-o", str(exe)]
if platform.system() == "Windows":
cmd += ["-lopengl32", "-lmingw32", "-luser32", "-lgdi32", "-lwinmm", "-limm32",
"-lole32", "-loleaut32", "-lversion", "-luuid", "-lsetupapi", "-lcfgmgr32", "-lm"]
else:
cmd += ["-lGL", "-lm", "-ldl", "-lpthread"]
build = subprocess.run(cmd, capture_output=True, text=True)
receipt = {"command": cmd, "build_exit": build.returncode, "build_stderr": build.stderr}
if not build.returncode:
result = subprocess.run([str(exe)], capture_output=True, text=True)
receipt.update(run_exit=result.returncode, stdout=result.stdout, stderr=result.stderr)
(dest / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
print("build:", build.returncode, "GL fixture:", receipt.get("run_exit", "not run"))
if build.returncode:
print(build.stderr[-4000:])
elif receipt["run_exit"]:
print(receipt["stderr"][-2000:])
return build.returncode or receipt.get("run_exit", 1)
if __name__ == "__main__":
raise SystemExit(main())
+75
View File
@@ -0,0 +1,75 @@
/* Source-owned grayscale/color ramp, actual submission callbacks and hidden GL.
* No headset or retail payload required. Compile with function-section GC. */
#include "openxr_color_fixture_renderer.h"
#include <assert.h>
static PSXOpenXRStats fixture_xr;
void psx_openxr_stats(PSXOpenXRStats *out) { *out=fixture_xr; }
static double decode(double x) {return x<=.04045?x/12.92:pow((x+.055)/1.055,2.4);}
static void check_pixels(const unsigned char *got,const unsigned char *src,int flip,
double gamma,int linear,int alpha) {
for(int y=0;y<2;y++) for(int x=0;x<256;x++) for(int c=0;c<(alpha?4:3);c++) {
double v=src[((flip?1-y:y)*256+x)*4+c]/255.;
if(c<3) {v=pow(v,1/gamma);if(linear)v=decode(v);}
int expected=(int)floor(v*255+.5);
assert(abs((int)got[(y*256+x)*4+c]-expected)<=1);
}
}
int main(void) {
int64_t formats[]={0x1234,PSX_XR_RGBA8,PSX_XR_SRGB8_ALPHA8};
assert(psx_xr_color_format(formats,3)==PSX_XR_SRGB8_ALPHA8);
assert(psx_xr_color_format(formats,2)==PSX_XR_RGBA8);
assert(!psx_xr_color_format(formats,1));assert(!psx_xr_color_format(NULL,0));
assert(SDL_Init(SDL_INIT_VIDEO)==0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION,3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE,8);
SDL_Window *win=SDL_CreateWindow("XR color fixture",0,0,256,64,SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN);
assert(win);s_ctx=SDL_GL_CreateContext(win);assert(s_ctx);assert(load_modern_gl());
p_glGenVertexArrays(1,&s_present_vao);
unsigned char src[256*2*4],got[sizeof src],native[sizeof src];
for(int y=0;y<2;y++) for(int x=0;x<256;x++) {
int i=(y*256+x)*4;
src[i]=y?255-x:x;src[i+1]=y?x:255-x;src[i+2]=x/2;src[i+3]=77;
}
StereoPair *pair=&s_stereo_pair[0];s_stereo_current=0;
pair->tex[0]=make_tex(GL_RGBA8,256,2,GL_RGBA,GL_UNSIGNED_BYTE);
glBindTexture(GL_TEXTURE_2D,pair->tex[0]);
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,256,2,GL_RGBA,GL_UNSIGNED_BYTE,src);
assert(make_fbo(&pair->fbo[0],pair->tex[0],0));pair->tw[0]=256;pair->th[0]=2;
for(int linear=0;linear<2;linear++) for(int gamma=0;gamma<2;gamma++) for(int enabled=0;enabled<2;enabled++) {
fixture_xr.swapchain_format=linear?PSX_XR_RGBA8:PSX_XR_SRGB8_ALPHA8;
GLuint target=make_tex((GLint)fixture_xr.swapchain_format,256,2,GL_RGBA,GL_UNSIGNED_BYTE),fbo=0;
assert(make_fbo(&fbo,target,0));
p_glBindFramebuffer(PSXGL_FRAMEBUFFER,0);glViewport(3,4,17,19);
glEnable(GL_SCISSOR_TEST);glScissor(0,0,1,1);
if(enabled) glEnable(0x8DB9);else glDisable(0x8DB9);
s_present_gamma=gamma?1.7f:1.f;
glEnable(GL_BLEND);glEnable(GL_DEPTH_TEST);glEnable(GL_STENCIL_TEST);glEnable(GL_CULL_FACE);
glColorMask(GL_FALSE,GL_TRUE,GL_FALSE,GL_TRUE);
assert(openxr_copy_eye(0,target,256,2));assert(glGetError()==GL_NO_ERROR);
GLint viewport[4],read_fbo,draw_fbo;GLboolean mask[4];
glGetIntegerv(GL_VIEWPORT,viewport);glGetIntegerv(0x8CAA,&read_fbo);glGetIntegerv(0x8CA6,&draw_fbo);
glGetBooleanv(GL_COLOR_WRITEMASK,mask);
assert(viewport[0]==3&&viewport[1]==4&&viewport[2]==17&&viewport[3]==19);
assert(!read_fbo&&!draw_fbo&&!mask[0]&&mask[1]&&!mask[2]&&mask[3]);
assert(glIsEnabled(GL_SCISSOR_TEST)&&glIsEnabled(0x8DB9)==enabled);
assert(glIsEnabled(GL_BLEND)&&glIsEnabled(GL_DEPTH_TEST)&&glIsEnabled(GL_STENCIL_TEST)&&glIsEnabled(GL_CULL_FACE));
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,fbo);glReadBuffer(PSXGL_COLOR_ATTACHMENT0);
glReadPixels(0,0,256,2,GL_RGBA,GL_UNSIGNED_BYTE,got);
check_pixels(got,src,1,s_present_gamma,linear,1);
/* Native already contains desktop gamma: do not apply it a second time. */
p_glBindFramebuffer(PSXGL_FRAMEBUFFER,0);glDisable(GL_SCISSOR_TEST);glDisable(0x8DB9);
assert(openxr_color_draw(pair->tex[0],256,2,0,s_present_gamma,0));
glReadBuffer(GL_BACK);glReadPixels(0,0,256,2,GL_RGBA,GL_UNSIGNED_BYTE,native);
s_native_surface_rect[0]=s_native_surface_rect[1]=0;
s_native_surface_rect[2]=256;s_native_surface_rect[3]=2;
if(enabled)glEnable(0x8DB9);
assert(openxr_copy_native(0,target,256,2));assert(glIsEnabled(0x8DB9)==enabled);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,fbo);glReadBuffer(PSXGL_COLOR_ATTACHMENT0);
glReadPixels(0,0,256,2,GL_RGBA,GL_UNSIGNED_BYTE,got);
check_pixels(got,native,0,1,linear,0);assert(glGetError()==GL_NO_ERROR);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER,0);p_glDeleteFramebuffers(1,&fbo);glDeleteTextures(1,&target);
}
SDL_GL_DeleteContext(s_ctx);SDL_DestroyWindow(win);SDL_Quit();return 0;
}