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
Stefan MaierandClaude Opus 5.5 c04ce7fd00 Steam Frame: each eye drawn apart (no multiview on SteamVR)
With both eyes drawn at once (multiview) the eyes' pictures did not fuse
on the Steam Frame, now and then: each eye alone sharp, both together
doubled as the head turned, in the menus too. With vr.multiview = false
the user saw no doubling, at the same frame time (the Frame's time goes
to the GL driver either way). debug.multiview_steamvr brings multiview
back there for finding the cause; the Quest keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:54:53 +02:00
Stefan MaierandClaude Opus 5.5 3f06b1b9d8 Revert "Steam Frame: the layers' images acquired before the frame waits"
This reverts commit 24e583c0. With the images acquired before xrWaitFrame
the frames came fast (about 8 ms of work) but the picture doubled again
on the Steam Frame as the head turned (181 of 3753 frames reprojected);
without it (673b32b1) it did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:40:35 +02:00
Stefan MaierandClaude Opus 5.5 1b2d058a04 VR multiview: lens flares placed by their depth in each eye
A lens flare is projected on the CPU (w 1, its depth in z) and drawn
without the depth test, which multiview took for something at infinity,
as the sun's flares are. A weapon's own lights have flares too, at its
markers half a metre from the eyes (the plasma pistol's at its prongs,
the plasma rifle's at its muzzle): each eye saw its flare where infinity
puts it, a green ball at the barrel the eyes could not fuse. While flares
are drawn (rasterizer_lens_flares_draw), each eye's is now placed by the
flare's depth, as its occlusion test already was; the sun's are far
enough for infinity either way. Whether a flare shows at all depends on
its occlusion test, which is why the balls came and went, and showed on
the Frame more than on the Quest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 b5fa53ed95 Steam Frame: the layers' images acquired before the frame waits
With the copies deferred the wait only moved: acquiring the images took
8-10 ms a frame, the left eye's 6-8 of it. SteamVR's acquire (or its wait
for the image) waits for the GL work queued before it, the eyes' whole
drawing when the left eye's copy asked, the HUD's for the overlay. The
images the last frame filled are now acquired before xrWaitFrame, when
little is queued and the frame would wait for the headset anyway; one a
frame needs that was not acquired ahead is acquired as before. The
performance log shows the time acquiring ahead apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 1f2c10a3ca Menus' art: a texture outside the art's range is no menu bitmap
Every texture the renderer binds asks whether it is a menu bitmap
standing for its PNG (menu_art_texture), in a game too, where the PC
menus' bitmaps are loaded for the pause menu's settings: a walk over the
150-odd registered bitmaps for each texture of each draw. Their textures
lie together, so one lowest/highest check answers for the map's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
Stefan MaierandClaude Opus 5.5 12f1155752 Steam Frame: the headset's layer copies without waits in the frame
The Frame's game thread spent most of each 13.9 ms frame waiting, not
working: the left eye's copy into its swapchain took 4.5-7 ms, the right
one's right after it 0.7, each later copy 0.7-3. Every copy read the GL
state back to restore it (glGet*, glIsEnabled, the source's swizzle) and
released its image at once; with Mesa's GL thread (zink) each read waits
for the thread to run everything queued, each release for the GPU, so the
thread stood still for the eyes' whole drawing mid-frame, and frames came
late enough for SteamVR to reproject 2-8% of them (the doubled picture
when turning the head).

Deferred copies (SteamVR by default; debug.xr_deferred_copies 0 or 1 says
otherwise; the Quest's path is unchanged):
- nothing read back: a copy leaves GL as it drew and returns 2; vr.c has
  the renderer set its state again (d3d8_host_state_changed: the state
  cache invalidated, its vertex arrays bound again)
- no swizzle round trip: copies read render targets, which have none
- the images kept until the frame ends and released together before
  xrEndFrame; the eyes' work flushed to the GPU once they are copied
- the performance log times acquiring and releasing apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
10 changed files with 424 additions and 35 deletions

No files matched your search

+26
View File
@@ -123,3 +123,29 @@ page of buttons shows this layout on the Frame.
making up of the missed frames showed as a doubled picture when turning
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
0.85) for 90.
- 2026-10-05: doubled pictures when turning at 72 Hz too, the game at the
edge of its frame (12.5 of 13.9 ms; SteamVR reprojecting 2-8% of the
frames). The time was not the drawing's but waits: the layer copies read
the GL state back (glGet*) and released each image at once, and with
Mesa's GL thread (zink) every read waits for the thread to run all that
is queued, every release for the GPU (the left eye's copy took 4.5-7 ms
of the frame, the right one's, just after it, 0.7). On SteamVR the copies
are now deferred (host_xr.c): no state read back (the game sets its own
again), the images released together as the frame ends, and the eyes'
work flushed to the GPU as soon as they are copied.
`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.
+3 -1
View File
@@ -119,7 +119,9 @@ headset before the game started, and set HALO_VR); 0 on failure */
int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate);
/* waits for the next frame and reports the predicted poses */
int host_xr_begin_frame(struct halo_xr_frame *frame);
/* copies a render target texture (0: clears) into a layer's image */
/* copies a render target texture (0: clears) into a layer's image: 0 not
copied, 1 copied with the GL state put back, 2 copied with the copy's GL
state left (the deferred copies, host_xr.c), which the game sets again */
int host_xr_copy_layer(int layer, unsigned int texture, int clear_alpha);
/* the same for part of the texture: left, top, right, bottom, 0..1 from its
top left */
+46
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)
@@ -337,6 +355,34 @@ static void *game_main(void *unused)
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
}
}
{
/* (debug.xr_deferred_copies: the headset's layer copies deferred,
host_xr.c, or one by one; -1 as the runtime has it, deferred on
SteamVR) */
double deferred = config_number(path, "debug.xr_deferred_copies", -1.0);
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;
+85 -18
View File
@@ -58,6 +58,7 @@ struct xr_swapchain
GLuint framebuffers[MAXIMUM_SWAPCHAIN_IMAGES];
uint32_t acquired_index;
int filled; /* this frame */
int held; /* acquired, released only as the frame ends (xr.deferred) */
};
struct xr_hand_actions
@@ -136,7 +137,18 @@ static struct
double copy_seconds[HALO_XR_LAYER_COUNT];
double end_seconds;
double acquire_seconds;
double release_seconds;
int timed_frames;
/* The copies without waits in the frame (SteamVR: HALO_XR_DEFERRED,
debug.xr_deferred_copies). Each copy otherwise reads the GL state back
to restore it (glGet*), and releases its image at once: with Mesa's GL
thread (the Steam Frame's zink) each read waits for the thread to run
everything queued, and each release for the GPU to finish it, so the
game's one thread stood still for the eyes' whole drawing in the middle
of every frame. Deferred, a copy leaves its GL state for the game to set
again (host_xr_copy_layer's 2) and keeps its image until the frame ends,
where all are released at once. */
int deferred;
int has_refresh_rate;
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate;
PFN_xrGetDisplayRefreshRateFB get_refresh_rate;
@@ -1283,6 +1295,16 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
}
}
{
/* (deferred on SteamVR; HALO_XR_DEFERRED "1" or "0" says otherwise:
debug.xr_deferred_copies, host_main.c) */
const char *choice = getenv("HALO_XR_DEFERRED");
xr.deferred = choice && *choice ? atoi(choice) != 0 : xr.steamvr;
host_logf(HOST_LOG_INFO, "xr: layer copies %s", xr.deferred ?
"deferred (no waits for the GL thread or the GPU in the frame)" : "restored and released one by one");
}
info->eye_width = xr.eye_width;
info->eye_height = xr.eye_height;
info->overlay_width = xr.eye_width;
@@ -1481,23 +1503,27 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
return 0;
swapchain = &xr.swapchains[layer];
if (swapchain->filled)
return 1;
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
"xrAcquireSwapchainImage"))
{
return 0;
}
return xr.deferred ? 2 : 1;
wait.timeout = XR_INFINITE_DURATION;
{
double start = xr.has_metrics ? seconds_now() : 0.0;
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
"xrAcquireSwapchainImage"))
{
return 0;
}
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
return 0;
if (xr.has_metrics)
xr.acquire_seconds += seconds_now() - start;
}
gl_save(&saved);
/* (deferred: nothing read back; the game sets its state again) */
if (xr.deferred)
glActiveTexture(GL_TEXTURE0);
else
gl_save(&saved);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, swapchain->framebuffers[swapchain->acquired_index]);
glViewport(0, 0, swapchain->width, swapchain->height);
glDisable(GL_SCISSOR_TEST);
@@ -1562,13 +1588,22 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
glBindVertexArray(xr.vertex_array);
glBindTexture(GL_TEXTURE_2D, texture);
glBindSampler(0, xr.sampler);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
if (xr.deferred)
{
/* (the game's render targets, all a copy reads, have no
swizzle: only its uploaded textures get one, xbox_textures.c) */
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
else
{
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
}
if (clear_alpha == HALO_XR_COPY_OPAQUE)
{
/* an opaque layer: the game's alpha channel holds other things */
@@ -1582,13 +1617,42 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
glClearColor(0.0f, 0.0f, 0.0f, clear_alpha ? 1.0f : 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
swapchain->filled = 1;
if (xr.deferred)
{
swapchain->held = 1;
/* (the eyes, most of the frame's work, on to the GPU while the
game draws the rest) */
if (layer == HALO_XR_LAYER_EYE_RIGHT)
glFlush();
return 2;
}
gl_restore(&saved);
xrReleaseSwapchainImage(swapchain->handle, &release);
swapchain->filled = 1;
return 1;
}
/* deferred: the images the frame's copies kept, released before it ends */
static void release_held(void)
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int layer;
for (layer = 0; layer < HALO_XR_LAYER_COUNT; layer++)
{
XrSwapchainImageReleaseInfo release = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
struct xr_swapchain *swapchain = &xr.swapchains[layer];
if (!swapchain->held)
continue;
swapchain->held = 0;
xr_ok(xrReleaseSwapchainImage(swapchain->handle, &release), "xrReleaseSwapchainImage");
}
if (xr.has_metrics)
xr.release_seconds += seconds_now() - start;
}
/* The Steam Frame: Android in a container, without XR_EXT_performance_settings
to ask for the clocks, and the scheduler free to put the game's one busy
thread, which simulates and draws each frame, on a slow core (its two
@@ -1659,6 +1723,7 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
if (!xr.frame_begun)
return 0;
xr.frame_begun = 0;
release_held();
if (xr.frame_state.shouldRender && submit->eyes && xr.views_valid &&
xr.swapchains[HALO_XR_LAYER_EYE_LEFT].filled && xr.swapchains[HALO_XR_LAYER_EYE_RIGHT].filled)
@@ -1836,9 +1901,11 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
strcat(copies, item);
xr.copy_seconds[timed] = 0.0;
}
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them waiting for images %.2f; "
"xrEndFrame %.2f", copies, xr.acquire_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames);
xr.acquire_seconds = xr.end_seconds = 0.0;
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them acquiring images %.2f; "
"releasing them at the end %.2f; xrEndFrame %.2f (copies %s)", copies,
xr.acquire_seconds * 1e3 / frames, xr.release_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames,
xr.deferred ? "deferred" : "one by one");
xr.acquire_seconds = xr.release_seconds = xr.end_seconds = 0.0;
xr.timed_frames = 0;
}
}
+3
View File
@@ -63,6 +63,9 @@ void halo_vr_multiview_sky(int sky);
/* the shared camera's projection's [2][2] and [3][2], for what the game
projects itself (sprites, lens flare tests) */
void halo_vr_multiview_projection(float projection_22, float projection_32);
/* lens flares are being drawn: placed by their depth, though not depth
tested */
void halo_vr_multiview_lens_flares(int drawing);
/* clears the back buffer to transparent black for the overlay, after the
eyes; menus_active chooses where the overlay is shown */
+130 -1
View File
@@ -915,6 +915,10 @@ static struct
/* the shared camera's projection's depth, as the game projects a point
itself (rasterizer_widget_project_billboard): z = [0] + [1] / depth */
float projection_depth[2];
/* 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 */
@@ -962,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])
@@ -970,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)
{
@@ -1016,6 +1126,14 @@ int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4])
return TRUE;
}
/* the draws from now on, depth tested or not, have their depth in z (lens
flares, drawn over the scene: a weapon's own, half a metre away, stood at
infinity in stereo, a ball each eye saw elsewhere) */
void d3d8_multiview_projected_depth(int projected)
{
multiview.projected_depth = projected != 0;
}
/* the projection's depth for what the game projects itself: z = a + b /
depth for a point that far along the view */
void d3d8_multiview_depth(float a, float b)
@@ -2938,7 +3056,7 @@ static struct program_entry *prepare_draw(BOOL immediate)
depth[0] = multiview.projection_depth[0];
depth[1] = multiview.projection_depth[1];
depth[2] = has_depth && D3D__RenderState[D3DRS_ZENABLE] ? 1.0f : 0.0f;
depth[2] = (has_depth && D3D__RenderState[D3DRS_ZENABLE]) || multiview.projected_depth ? 1.0f : 0.0f;
depth[3] = 0.0f;
uniform_vec4(entry->mv_depth, entry->mv_depth_value, depth, 1);
}
@@ -4046,6 +4164,17 @@ unsigned int d3d8_back_buffer_texture(void)
return back_buffer ? back_buffer->target.texture : 0;
}
/* the headset's copy left GL as it drew (host_xr_copy_layer's 2, the
deferred copies): every value is set again, and the vertex arrays, which
the cache does not hold, are bound again */
void d3d8_host_state_changed(void)
{
if (!device.gl_ready)
return;
xgpu_gl_state_invalidate();
glBindVertexArray(device.vertex_array);
}
/* clears the whole back buffer, its alpha too, and the depth buffer */
void d3d8_clear_back_buffer(float red, float green, float blue, float alpha)
{
+12
View File
@@ -911,6 +911,11 @@ static struct
int failed;
} art[MAXIMUM_ART];
static long art_count;
/* the lowest and highest texture the art stands for: every texture the
renderer binds asks (xbox_textures.c), every frame of a game too, and the
menus' lie together, made as the map loaded, mostly apart from the map's
(in its texture cache) */
static unsigned long art_lowest, art_highest;
void halo_menus_art_register(void const *texture, char const *png)
{
@@ -944,6 +949,10 @@ void halo_menus_art_register(void const *texture, char const *png)
memset(&art[index], 0, sizeof(art[index]));
art[index].data = data;
art[index].png = strdup(png);
if (art_count == 1 || data < art_lowest)
art_lowest = data;
if (art_count == 1 || data > art_highest)
art_highest = data;
}
void halo_menus_art_forget(void)
@@ -957,12 +966,15 @@ void halo_menus_art_forget(void)
glDeleteTextures(1, &art[index].texture);
}
art_count = 0;
art_lowest = art_highest = 0;
}
unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
{
long index;
if (!art_count || data < art_lowest || data > art_highest)
return 0;
for (index = 0; index < art_count; index++)
{
if (art[index].data == data)
+12
View File
@@ -505,9 +505,21 @@ static const struct config_setting config_settings[] =
"machines of twice it), to test the netcode as over the internet; 0 none." },
{ "debug.network_loss", _config_real, "0.0", "HALO_NETWORK_LOSS", _environment_value, _platform_all,
"Percent of datagrams received that are dropped, for the same; 0 none." },
{ "debug.multiview_steamvr", _config_boolean, "false", "HALO_MULTIVIEW_STEAMVR", _environment_value,
_platform_android,
"On SteamVR (the Steam Frame) the eyes are drawn one by one, whatever\n"
"vr.multiview says: true draws them at once there too, for testing." },
{ "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"
"-1 as the runtime needs (deferred on SteamVR). Read when the app starts." },
{ "debug.test_input", _config_string, "\"\"", "HALO_TEST_INPUT", _environment_value, _platform_all,
"\"bot:<seed>\" plays controller 1 with a scripted pattern (automated\n"
"network tests); \"look:<seed>\" stands still, only turning and looking\n"
+102 -15
View File
@@ -236,9 +236,20 @@ int d3d8_multiview_supported(void);
int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4]);
void d3d8_multiview_rows(const float rows[4][4]);
void d3d8_multiview_depth(float a, float b);
void d3d8_multiview_projected_depth(int projected);
unsigned int d3d8_multiview_end(void);
void d3d8_clear_back_buffer(float red, float green, float blue, float alpha);
void d3d8_clear_back_buffer_region(float left, float top, float right, float bottom);
void d3d8_host_state_changed(void);
/* a layer copied (host_xr_copy_layer): 2 is a deferred copy, which left GL
as it drew, for the renderer to set again */
static int vr_copied(int result)
{
if (result == 2)
d3d8_host_state_changed();
return result != 0;
}
/* The motion tracker, the HUD's one element at its bottom left, moves from
the HUD to the left wrist (host_xr.c draws it there): this part of the
@@ -503,6 +514,16 @@ void halo_vr_initialize(void)
vr.active = 1;
platform_log("VR: %s, eyes %dx%d at %.0f Hz", vr.info.system_name, vr.info.eye_width, vr.info.eye_height,
vr.info.refresh_rate);
/* SteamVR (the Steam Frame): each eye drawn apart. With both drawn at
once the eyes' pictures did not fuse, now and then: each alone was
sharp, the two together doubled as the head turned (2026-10-05), and
one by one costs it no more (its time goes to the driver either way).
debug.multiview_steamvr draws them at once there again, for finding why */
if (vr.multiview && strstr(vr.info.system_name, "SteamVR") && !config_boolean("debug.multiview_steamvr"))
{
vr.multiview = 0;
platform_log("VR: SteamVR: the eyes drawn one by one (debug.multiview_steamvr)");
}
}
int halo_vr_active(void)
@@ -1312,9 +1333,8 @@ void halo_vr_present(unsigned int back_buffer_texture)
if (vr.overlay_mode == HALO_XR_OVERLAY_HEAD_LOCKED && vr.motion_tracker_on_wrist && vr.aim_with_controller)
{
/* the motion tracker to the wrist, and out of the HUD */
submit.wrist = host_xr_copy_layer_region(HALO_XR_LAYER_WRIST, back_buffer_texture, HALO_XR_COPY_ALPHA,
WRIST_REGION_LEFT,
WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM);
submit.wrist = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_WRIST, back_buffer_texture,
HALO_XR_COPY_ALPHA, WRIST_REGION_LEFT, WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM));
submit.wrist_size = vr.motion_tracker_size;
submit.wrist_turn = vr.motion_tracker_turn;
submit.wrist_right = vr.motion_tracker_right;
@@ -1345,8 +1365,8 @@ void halo_vr_present(unsigned int back_buffer_texture)
submit.scope_size = vr.scope_size;
submit.scope_pose = vr.scope_pose;
}
host_xr_copy_layer(HALO_XR_LAYER_OVERLAY, back_buffer_texture,
vr.overlay_blend ? HALO_XR_COPY_ALPHA : HALO_XR_COPY_OPAQUE);
vr_copied(host_xr_copy_layer(HALO_XR_LAYER_OVERLAY, back_buffer_texture,
vr.overlay_blend ? HALO_XR_COPY_ALPHA : HALO_XR_COPY_OPAQUE));
host_xr_end_frame(&submit);
vr.last_overlay_mode = vr.overlay_mode;
vr.last_frame_stereo = submit.eyes;
@@ -1427,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();
@@ -1436,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],
@@ -1544,6 +1621,15 @@ void halo_vr_multiview_projection(float projection_22, float projection_32)
d3d8_multiview_depth(-projection_22, projection_32);
}
/* lens flares are being drawn (rasterizer_lights.c): each eye's by the
flare's depth, as the flare's test was, not at infinity (a weapon's own,
the plasma pistol's at its prongs, half a metre away: a ball each eye saw
elsewhere); the sun's are far enough for infinity either way */
void halo_vr_multiview_lens_flares(int drawing)
{
d3d8_multiview_projected_depth(drawing && multiview_drawing);
}
/* the sky is being drawn (render.c): as though infinitely far */
void halo_vr_multiview_sky(int sky)
{
@@ -1559,9 +1645,9 @@ void halo_vr_capture_multiview(void)
if (!multiview_drawing)
return;
multiview_drawing = 0;
if (host_xr_copy_layer_array(HALO_XR_LAYER_EYE_LEFT, layers, 0))
if (vr_copied(host_xr_copy_layer_array(HALO_XR_LAYER_EYE_LEFT, layers, 0)))
vr.eyes_captured |= 1;
if (host_xr_copy_layer_array(HALO_XR_LAYER_EYE_RIGHT, layers, 1))
if (vr_copied(host_xr_copy_layer_array(HALO_XR_LAYER_EYE_RIGHT, layers, 1)))
vr.eyes_captured |= 2;
if (vr.eyes_start_us)
vr.eyes_us += now_us() - vr.eyes_start_us;
@@ -1571,7 +1657,7 @@ void halo_vr_capture_eye(int eye)
{
if (!vr.active || eye < 0 || eye >= HALO_XR_EYE_COUNT)
return;
if (host_xr_copy_layer(HALO_XR_LAYER_EYE_LEFT + eye, d3d8_back_buffer_texture(), 1))
if (vr_copied(host_xr_copy_layer(HALO_XR_LAYER_EYE_LEFT + eye, d3d8_back_buffer_texture(), 1)))
vr.eyes_captured |= 1 << eye;
if (eye == HALO_XR_EYE_COUNT - 1 && vr.eyes_start_us)
vr.eyes_us += now_us() - vr.eyes_start_us;
@@ -2402,9 +2488,9 @@ void halo_vr_capture_names(void)
{
if (vr.name_count > 0)
{
vr.names_captured = host_xr_copy_layer_region(HALO_XR_LAYER_NAMES, d3d8_back_buffer_texture(),
vr.names_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_NAMES, d3d8_back_buffer_texture(),
HALO_XR_COPY_ALPHA, NAME_SLOT_LEFT / 640.0f, 0.0f, (NAME_SLOT_LEFT + NAME_SLOT_WIDTH) / 640.0f,
HALO_XR_MAXIMUM_NAMES * NAME_SLOT_HEIGHT / 480.0f);
HALO_XR_MAXIMUM_NAMES * NAME_SLOT_HEIGHT / 480.0f));
}
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
}
@@ -2512,8 +2598,8 @@ int halo_vr_scope_pass(void)
void halo_vr_capture_scope(float left, float top, float right, float bottom)
{
vr.scope_pass = 0;
vr.scope_captured = host_xr_copy_layer_region(HALO_XR_LAYER_SCOPE, d3d8_back_buffer_texture(), HALO_XR_COPY_DISC,
left, top, right, bottom);
vr.scope_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_SCOPE, d3d8_back_buffer_texture(),
HALO_XR_COPY_DISC, left, top, right, bottom));
}
int halo_vr_hide_crosshair(void)
@@ -2530,9 +2616,9 @@ int halo_vr_crosshair_apart(void)
void halo_vr_capture_crosshair(void)
{
vr.crosshair_captured = host_xr_copy_layer_region(HALO_XR_LAYER_CROSSHAIR, d3d8_back_buffer_texture(),
vr.crosshair_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_CROSSHAIR, d3d8_back_buffer_texture(),
HALO_XR_COPY_ALPHA, CROSSHAIR_REGION_LEFT, CROSSHAIR_REGION_TOP, CROSSHAIR_REGION_RIGHT,
CROSSHAIR_REGION_BOTTOM);
CROSSHAIR_REGION_BOTTOM));
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
}
@@ -2567,6 +2653,7 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa
}
void halo_vr_capture_multiview(void) {}
void halo_vr_multiview_sky(int sky) { (void)sky; }
void halo_vr_multiview_lens_flares(int drawing) { (void)drawing; }
void halo_vr_multiview_projection(float projection_22, float projection_32)
{
(void)projection_22; (void)projection_32;
+5
View File
@@ -98,6 +98,7 @@ symbols in this file:
#include "rasterizer_console_vars.h"
#include "objects/widgets/widget_types.h"
#include "main/main.h"
#include "halo_vr.h" /* port: VR's stereo of the flares */
#include <xtl.h>
#include "rasterizer/xbox/rasterizer_xbox.h"
#include "main/main.h"
@@ -869,6 +870,9 @@ void rasterizer_lens_flares_draw(
{
short lens_flare_index;
/* port: in VR's both eyes at once, each eye's flare where its depth
puts it (port/linux/src/vr.c) */
halo_vr_multiview_lens_flares(TRUE);
rasterizer_widget_begin(_widget_type_internal_sprite, 0);
for (lens_flare_index = 0; lens_flare_index < local_lens_flare_count; lens_flare_index++)
@@ -1123,6 +1127,7 @@ void rasterizer_lens_flares_draw(
rasterizer_set_stencil_mode(RASTERIZER_STENCIL_MODE_NONE);
rasterizer_widget_end();
halo_vr_multiview_lens_flares(FALSE);
if (rasterizer_debug_options.lens_flare_sun_glow_enabled)
{