Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 3aad697ebe VR multiview: a driver self-test, views turned apart drawn one by one, driver variables
On the Steam Frame the eyes drawn at once did not fuse (each alone sharp)
while drawn one by one they did; the corrections agree with the projection
and fuse on the Quest, so the Frame's GL driver (Mesa zink on Turnip) is
the suspect: a right eye given the left one's correction shows there, where
the eyes' views are wider and unlike each other.
- d3d8_gl.c: when the layers are first made, a point drawn into two 2x1
  layers by rows picked with gl_ViewID_OVR, as the game's vertex programs
  pick them, and read back; failed, the eyes are drawn one by one
  (logcat "multiview: self-test passed/FAILED")
- vr.c: a frame whose eyes' views are turned apart (more than 0.05
  degrees, which one camera cannot serve) draws the eyes one by one; the
  views' angle and each eye's edges go to the log as they change
- debug.gl_environment: "NAME=value ..." set before the driver starts
  (TU_DEBUG=nomultipos, ZINK_DEBUG=nobgc, MESA_SHADER_CACHE_DISABLE=true)
SteamVR still draws the eyes one by one unless debug.multiview_steamvr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:09:42 +02:00
5 changed files with 219 additions and 1 deletions

No files matched your search

+13
View File
@@ -136,3 +136,16 @@ page of buttons shows this layout on the Frame.
`debug.xr_deferred_copies` = 0 brings back the old copies for comparing,
1 defers them on any runtime; `vr.log_performance` logs the frame's parts
and, with the compositor's counters, each copy's time.
- 2026-10-05 later: the doubling was the eyes not fusing (each eye sharp
alone) with multiview, and gone drawing the eyes one by one, which is now
SteamVR's default (`debug.multiview_steamvr` for multiview). The per-eye
corrections were checked against the projection (x, y from the top after
the flip, the eyes' offsets in clip units) and agree with the Quest, where
multiview fuses; the Frame's eyes are wider and differ from each other
more, so a driver giving the right eye the left one's correction (zink on
Turnip: ViewIndex, Turnip's multi-position output) would show there. For
finding it: a self-test of the driver's views when the layers are made
(logcat "multiview: self-test"), a frame whose views are turned apart
drawn one by one, the views' shape in the log as it changes ("VR views:"),
and `debug.gl_environment` ("TU_DEBUG=nomultipos", "ZINK_DEBUG=nobgc",
"MESA_SHADER_CACHE_DISABLE=true") set before the driver starts.
+37
View File
@@ -205,6 +205,24 @@ static double config_number(const char *path, const char *key, double fallback)
return value;
}
/* a string from config.toml into text (size bytes), or "" when it is not
set */
static void config_text(const char *path, const char *key, char *text, size_t size)
{
toml_result_t result = toml_parse_file_ex(path);
text[0] = 0;
if (!result.ok)
return;
{
toml_datum_t datum = toml_seek(result.toptab, key);
if (datum.type == TOML_STRING && datum.u.s)
snprintf(text, size, "%s", datum.u.s);
}
toml_free(result);
}
/* POSIX TZ for the current local offset (the guest's musl has no zone
database) */
static void time_zone(char *buffer, size_t size)
@@ -346,6 +364,25 @@ static void *game_main(void *unused)
if (deferred >= 0.0)
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
}
{
/* (debug.gl_environment: "NAME=value ..." for the GL driver, set
before it starts: the Steam Frame's Mesa (zink on Turnip) reads
ZINK_DEBUG, TU_DEBUG, MESA_SHADER_CACHE_DISABLE and the like) */
char text[512];
char *item, *rest = NULL;
config_text(path, "debug.gl_environment", text, sizeof(text));
for (item = strtok_r(text, " ;", &rest); item; item = strtok_r(NULL, " ;", &rest))
{
char *equals = strchr(item, '=');
if (!equals || equals == item)
continue;
*equals = 0;
setenv(item, equals + 1, 1);
host_logf(HOST_LOG_INFO, "GL driver environment: %s=%s (debug.gl_environment)", item, equals + 1);
}
}
{
int eye_width, eye_height;
+107
View File
@@ -918,6 +918,7 @@ static struct
/* what the game projects itself has its depth in z though not depth
tested (lens flares: d3d8_multiview_projected_depth) */
BOOL projected_depth;
BOOL tested; /* multiview_self_test */
} multiview;
/* the bound targets are the layers */
@@ -965,6 +966,102 @@ int d3d8_multiview_supported(void)
return device.gl_ready && xgpu_capabilities.multiview;
}
/* Whether the driver gives each view its own correction, as the game's
vertex programs take it (nv2a_vsh.c: a row of an array uniform picked by
gl_ViewID_OVR): a point drawn into two 2x1 layers, the left pixel's in view
0 and the right one's in view 1 by their rows, then read back. A driver
that gave both views the first one's would draw the right eye as the left
eye sees, which no two eyes fuse. Once, as the layers are first made. */
static BOOL multiview_self_test(void)
{
static const char vertex_source[] =
"#version 300 es\n"
"#extension GL_OVR_multiview2 : require\n"
"layout(num_views = 2) in;\n"
"uniform vec4 rows[4];\n"
"void main()\n"
"{\n"
"\tint row = int(gl_ViewID_OVR) * 2;\n"
"\tvec4 position = vec4(0.0, 0.0, 0.5, 1.0);\n"
"\tposition.x = dot(vec3(position.xw, 1.0), rows[row].xyz);\n"
"\tgl_Position = position;\n"
"\tgl_PointSize = 1.0;\n"
"}\n";
static const char fragment_source[] =
"#version 300 es\n"
"precision mediump float;\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"\tcolor = vec4(1.0);\n"
"}\n";
/* (x: view 0 to the left pixel's centre, view 1 to the right one's) */
static const float rows[4][4] =
{
{ 1.0f, 0.0f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 0.0f },
{ 1.0f, 0.0f, 0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 0.0f },
};
static const GLenum draw_buffer = GL_COLOR_ATTACHMENT0;
GLuint vertex = compile_shader(GL_VERTEX_SHADER, vertex_source, "multiview test vertex");
GLuint fragment = compile_shader(GL_FRAGMENT_SHADER, fragment_source, "multiview test fragment");
GLuint program, texture, framebuffer;
GLint linked = 0;
unsigned char pixels[2][8];
BOOL passed;
int layer;
if (!vertex || !fragment)
return TRUE;
program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
glDeleteShader(vertex);
glDeleteShader(fragment);
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked)
{
platform_log("multiview: the self-test's program does not link; not tested");
return TRUE;
}
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 2, 1, 2);
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0, 2);
glDrawBuffers(1, &draw_buffer);
glViewport(0, 0, 2, 1);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_CULL_FACE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glUniform4fv(glGetUniformLocation(program, "rows"), 4, &rows[0][0]);
glBindVertexArray(device.vertex_array);
glDrawArrays(GL_POINTS, 0, 1);
memset(pixels, 0, sizeof(pixels));
for (layer = 0; layer < 2; layer++)
{
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, layer);
glReadPixels(0, 0, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels[layer]);
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &framebuffer);
glDeleteTextures(1, &texture);
xgpu_gl_state_invalidate();
/* (view 0 lit only the left pixel, view 1 only the right one) */
passed = pixels[0][0] > 128 && pixels[0][4] < 128 && pixels[1][0] < 128 && pixels[1][4] > 128;
platform_log("multiview: self-test %s (view 0 drew %d %d, view 1 %d %d: each its own pixel)",
passed ? "passed" : "FAILED", pixels[0][0], pixels[0][4], pixels[1][0], pixels[1][4]);
return passed;
}
/* rows: per view, the correction of x and of y (nv2a_vsh.c); resolve_rect:
where each eye's picture lies in the shared camera's */
int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4])
@@ -973,6 +1070,16 @@ int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4])
if (!d3d8_multiview_supported() || !(back_buffer = render_target_get(&device.back_buffer)))
return FALSE;
if (!multiview.tested)
{
/* (failed: the eyes drawn one by one from now on, vr.c) */
multiview.tested = TRUE;
if (!multiview_self_test())
{
xgpu_capabilities.multiview = FALSE;
return FALSE;
}
}
if (!multiview.framebuffer || multiview.width != back_buffer->target.gl_width ||
multiview.height != back_buffer->target.gl_height)
{
+4
View File
@@ -512,6 +512,10 @@ static const struct config_setting config_settings[] =
{ "debug.gl_thread", _config_integer, "-1", "HALO_GL_THREAD", _environment_value, _platform_android,
"Mesa's GL thread (its drivers: the Steam Frame's): 1 on, 0 off, -1 as the\n"
"driver chooses. Read when the app starts." },
{ "debug.gl_environment", _config_string, "\"\"", "HALO_GL_ENVIRONMENT", _environment_value, _platform_android,
"Variables for the GL driver, \"NAME=value NAME2=value\", set before it starts\n"
"(the Steam Frame's Mesa: ZINK_DEBUG, TU_DEBUG, ...), for trying. Read when\n"
"the app starts." },
{ "debug.xr_deferred_copies", _config_integer, "-1", "HALO_XR_DEFERRED", _environment_value, _platform_android,
"The headset's layer copies: 1 deferred (no waits for the GL thread or the\n"
"GPU in the middle of the frame), 0 each one restored and released at once,\n"
+58 -1
View File
@@ -1447,6 +1447,63 @@ static int multiview_drawing;
about the camera, as though infinitely far: no eye stands aside of it */
static float multiview_rows[2][4][4];
/* The one camera between the eyes holds only while their views look the
same way (the corrections move each eye's picture aside, and scale it, but
cannot turn it): a frame whose views are turned apart, which a runtime may
report after the start (an eye's calibration), draws the eyes one by one.
The views' shape and how far apart they turn go to the log as they change. */
#define MULTIVIEW_PARALLEL_DEGREES 0.05f
static int views_parallel(void)
{
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
static float logged[HALO_XR_EYE_COUNT][4];
static float logged_degrees = -1.0f;
static unsigned long long logged_us;
const struct halo_xr_view *views = vr.frame.views;
float forward[2][3], up[2][3], along, degrees;
int eye, changed = 0;
unsigned long long now;
for (eye = 0; eye < HALO_XR_EYE_COUNT; eye++)
{
quaternion_rotate(views[eye].pose.orientation, local_forward, forward[eye]);
quaternion_rotate(views[eye].pose.orientation, local_up, up[eye]);
}
along = fminf(forward[0][0] * forward[1][0] + forward[0][1] * forward[1][1] + forward[0][2] * forward[1][2],
up[0][0] * up[1][0] + up[0][1] * up[1][1] + up[0][2] * up[1][2]);
degrees = acosf(fminf(1.0f, along)) * 180.0f / PI;
for (eye = 0; eye < HALO_XR_EYE_COUNT; eye++)
{
const float shape[4] = { views[eye].tangent_left, views[eye].tangent_right, views[eye].tangent_down,
views[eye].tangent_up };
int edge;
for (edge = 0; edge < 4; edge++)
changed |= fabsf(shape[edge] - logged[eye][edge]) > 0.005f;
}
changed |= fabsf(degrees - logged_degrees) > 0.02f;
now = now_us();
if (changed && now - logged_us > 5000000ULL)
{
platform_log("VR views: %.3f degrees apart; left eye %.3f..%.3f, %.3f..%.3f; right eye %.3f..%.3f, %.3f..%.3f",
degrees, views[0].tangent_left, views[0].tangent_right, views[0].tangent_down, views[0].tangent_up,
views[1].tangent_left, views[1].tangent_right, views[1].tangent_down, views[1].tangent_up);
for (eye = 0; eye < HALO_XR_EYE_COUNT; eye++)
{
logged[eye][0] = views[eye].tangent_left;
logged[eye][1] = views[eye].tangent_right;
logged[eye][2] = views[eye].tangent_down;
logged[eye][3] = views[eye].tangent_up;
}
logged_degrees = degrees;
logged_us = now;
}
return degrees <= MULTIVIEW_PARALLEL_DEGREES;
}
int halo_vr_multiview(void)
{
int available = vr.active && vr.multiview && d3d8_multiview_supported();
@@ -1456,7 +1513,7 @@ int halo_vr_multiview(void)
platform_log("VR: both eyes %s", available ? "in one draw (multiview)" : "drawn one by one (no multiview)");
vr.multiview_logged = 1;
}
return available && halo_vr_stereo_frame();
return available && halo_vr_stereo_frame() && views_parallel();
}
void halo_vr_center_camera(const float game_position[3], float position[3], float forward[3], float up[3],