From d462f80fa9cb423c421642ae6d97dbdd0417bbe7 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:37:28 +0200 Subject: [PATCH 01/10] VR: draw both eyes in one pass (multiview, first cut) With GL_OVR_multiview2 (vr.multiview), the scene is drawn once, from a camera between the eyes whose view takes in both (plus a margin), into a two-layer back buffer: every NV2A vertex program gets a two-view variant that moves each view's clip position to its eye (x' = a x + b w + c, from the shared view's and the eye's edges and the eye's place; nv2a_vsh.c), screen-space geometry excepted. Sampling the back buffer meanwhile reads the left eye's picture; visibility counts are halved. The host copies each layer to its eye's image (host_xr_copy_layer_array). Co-Authored-By: Claude Opus 5.5 --- port/android/guest/runtime/guest_host.h | 3 + port/android/host/host_xr.c | 113 +++++++++++++++- port/android/host_imports.list | 1 + port/linux/include/halo_vr.h | 9 ++ port/linux/src/d3d8_gl.c | 170 ++++++++++++++++++++++-- port/linux/src/gl.h | 8 ++ port/linux/src/nv2a_vsh.c | 28 +++- port/linux/src/port_config.c | 3 + port/linux/src/vr.c | 139 +++++++++++++++++++ port/linux/src/xgpu.h | 6 +- source/render/render.c | 24 +++- 11 files changed, 489 insertions(+), 15 deletions(-) diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index 3fac2cb2..b47ffa1b 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -124,6 +124,9 @@ int host_xr_copy_layer_region(int layer, unsigned int texture, int clear_alpha, /* submits the frame begun by host_xr_begin_frame */ int host_xr_end_frame(const struct halo_xr_submit *submit); void host_xr_haptic(int hand, float amplitude, float seconds); +/* copies a layer of a texture array (the eyes drawn at once, multiview) +into a layer's image, opaque */ +int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer); /* darkens the eye images from their edges, strength 0..1 (0: off) */ void host_xr_set_vignette(float strength); diff --git a/port/android/host/host_xr.c b/port/android/host/host_xr.c index d300d022..acd28848 100644 --- a/port/android/host/host_xr.c +++ b/port/android/host/host_xr.c @@ -124,6 +124,13 @@ static struct GLint vignette_uniform; GLint tangents_uniform; float vignette; + /* the same copy from a layer of a texture array (the eyes drawn at + once, multiview): its program and uniforms, and the layer for the copy + under way (host_xr_copy_layer_array; -1: a plain texture) */ + GLuint array_program; + GLint array_source_uniform, array_rect_uniform, array_layer_uniform; + GLint array_vignette_uniform, array_tangents_uniform; + int copy_array_layer; GLuint vertex_array; GLuint sampler; } xr; @@ -248,6 +255,33 @@ static const char copy_fragment_shader[] = " }\n" "}\n"; +static const char copy_array_fragment_shader[] = + "#version 300 es\n" + "precision highp float;\n" + "uniform highp sampler2DArray source;\n" + "uniform float layer;\n" + "uniform float vignette;\n" + "uniform vec4 tangents;\n" + "in vec2 coordinate;\n" + "in vec2 disc;\n" + "out vec4 color;\n" + "vec3 to_linear(vec3 c)\n" + "{\n" + " return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n" + "}\n" + "void main()\n" + "{\n" + " vec4 texel = texture(source, vec3(coordinate, layer));\n" + " color = vec4(to_linear(texel.rgb), texel.a);\n" + " if (vignette > 0.0)\n" + " {\n" + " vec2 corner = disc * 0.5 + 0.5;\n" + " vec2 away = vec2(mix(tangents.x, tangents.y, corner.x), mix(tangents.z, tangents.w, corner.y));\n" + " float edge = mix(1.8, 0.45, vignette);\n" + " color.rgb *= 1.0 - smoothstep(edge - 0.3, edge, length(away));\n" + " }\n" + "}\n"; + static GLuint compile_shader(GLenum type, const char *text) { GLuint shader = glCreateShader(type); @@ -293,6 +327,38 @@ static int copy_initialize(void) xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc"); xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette"); xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents"); + xr.copy_array_layer = -1; + { + GLuint array_vertex = compile_shader(GL_VERTEX_SHADER, copy_vertex_shader); + GLuint array_fragment = compile_shader(GL_FRAGMENT_SHADER, copy_array_fragment_shader); + + if (array_vertex && array_fragment) + { + xr.array_program = glCreateProgram(); + glAttachShader(xr.array_program, array_vertex); + glAttachShader(xr.array_program, array_fragment); + glLinkProgram(xr.array_program); + glGetProgramiv(xr.array_program, GL_LINK_STATUS, &status); + if (!status) + { + host_logf(HOST_LOG_ERROR, "xr: array copy program does not link"); + glDeleteProgram(xr.array_program); + xr.array_program = 0; + } + else + { + xr.array_source_uniform = glGetUniformLocation(xr.array_program, "source"); + xr.array_rect_uniform = glGetUniformLocation(xr.array_program, "rect"); + xr.array_layer_uniform = glGetUniformLocation(xr.array_program, "layer"); + xr.array_vignette_uniform = glGetUniformLocation(xr.array_program, "vignette"); + xr.array_tangents_uniform = glGetUniformLocation(xr.array_program, "tangents"); + } + } + if (array_vertex) + glDeleteShader(array_vertex); + if (array_fragment) + glDeleteShader(array_fragment); + } glGenVertexArrays(1, &xr.vertex_array); glGenSamplers(1, &xr.sampler); glSamplerParameteri(xr.sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -1150,6 +1216,20 @@ int host_xr_copy_layer(int layer, uint32_t texture, int clear_alpha) return host_xr_copy_layer_region(layer, texture, clear_alpha, 0.0f, 0.0f, 1.0f, 1.0f); } +/* copies a layer of a texture array (the eyes drawn at once) into a +layer's image, opaque */ +int host_xr_copy_layer_array(int layer, uint32_t texture, int array_layer) +{ + int copied; + + if (!xr.array_program) + return 0; + xr.copy_array_layer = array_layer; + copied = host_xr_copy_layer_region(layer, texture, HALO_XR_COPY_OPAQUE, 0.0f, 0.0f, 1.0f, 1.0f); + xr.copy_array_layer = -1; + return copied; +} + /* copies part of a render target (left, top, right, bottom, 0..1 from its top left) into a layer's image */ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right, @@ -1184,7 +1264,38 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa glDisable(GL_CULL_FACE); glDisable(GL_STENCIL_TEST); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - if (texture) + if (texture && xr.copy_array_layer >= 0) + { + /* a layer of the eyes' texture array (multiview) */ + glUseProgram(xr.array_program); + glUniform1i(xr.array_source_uniform, 0); + glUniform4f(xr.array_rect_uniform, left, top, right, bottom); + glUniform1f(xr.array_layer_uniform, (float)xr.copy_array_layer); + if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f) + { + const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov; + + glUniform1f(xr.array_vignette_uniform, xr.vignette); + glUniform4f(xr.array_tangents_uniform, tanf(fov->angleLeft), tanf(fov->angleRight), + tanf(fov->angleDown), tanf(fov->angleUp)); + } + else + { + glUniform1f(xr.array_vignette_uniform, 0.0f); + } + glBindVertexArray(xr.vertex_array); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + glBindSampler(0, xr.sampler); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + if (clear_alpha == HALO_XR_COPY_OPAQUE) + { + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + } + } + else if (texture) { GLint swizzle_r, swizzle_b; diff --git a/port/android/host_imports.list b/port/android/host_imports.list index 19fdc961..dbd55481 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -57,6 +57,7 @@ host_xr_copy_layer_region host_xr_end_frame host_xr_haptic host_xr_set_vignette +host_xr_copy_layer_array host_android_path host_errno diff --git a/port/linux/include/halo_vr.h b/port/linux/include/halo_vr.h index c95d07af..8b7e628a 100644 --- a/port/linux/include/halo_vr.h +++ b/port/linux/include/halo_vr.h @@ -50,6 +50,15 @@ void halo_vr_eye_camera(int eye, const float game_position[3], /* copies the back buffer, just drawn for an eye, to that eye's image */ void halo_vr_capture_eye(int eye); +/* Both eyes in one draw (vr.multiview, where GL_OVR_multiview2 is): whether +this frame draws so; the camera between the eyes whose view takes in both +(as halo_vr_eye_camera), which also starts drawing into both eyes' layers; +and after the draw, the layers taken for the eyes' images. */ +int halo_vr_multiview(void); +void halo_vr_center_camera(const float game_position[3], float position[3], float forward[3], float up[3], + float *vertical_field_of_view, float frustum_bounds[4]); +void halo_vr_capture_multiview(void); + /* clears the back buffer to transparent black for the overlay, after the eyes; menus_active chooses where the overlay is shown */ void halo_vr_begin_overlay(int menus_active); diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index d3050d4f..9debc929 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -175,8 +175,9 @@ struct vertex_shader_object struct vertex_element elements[XGPU_VERTEX_ATTRIBUTE_COUNT]; unsigned long element_count; unsigned long packed_mask; - /* [0] streams per the declaration, [1] immediate mode (all floats) */ - GLuint shader[2]; + /* [0] streams per the declaration, [1] immediate mode (all floats); + [2] and [3] the same for both eyes at once (multiview) */ + GLuint shader[4]; }; /* ---------- programs */ @@ -224,6 +225,10 @@ struct program_entry GLint bump_matrix, bump_luminance, texture_scale; GLint texture_lod_bias; GLint screen_offset; + /* multiview: each view's correction (nv2a_vsh.c), and multiview.serial + when it was last uploaded */ + GLint mv_rows; + unsigned long mv_serial; /* the vertex constants c[0..constant_count) the program uses; with consecutive locations, a changed range is uploaded by itself */ @@ -370,6 +375,8 @@ struct gl_device { unsigned short slot; unsigned short counter; + /* drawn into both eyes at once: counted twice */ + unsigned short views; } frame_tests[STREAM_BUFFER_RING][VISIBILITY_FRAME_TESTS]; unsigned long frame_test_count[STREAM_BUFFER_RING]; GLuint visibility_latest[VISIBILITY_TEST_SLOTS]; @@ -855,6 +862,124 @@ static GLuint framebuffer_get(GLuint color, GLuint depth) return entry->framebuffer; } +#ifdef HALO_ANDROID +/* ---------- multiview (VR: both eyes in one draw, GL_OVR_multiview2) + +Between d3d8_multiview_begin and _end, what the game draws into the back +buffer goes into two layers at once, one per eye: every vertex program is +compiled for two views (nv2a_vsh.c) and moves each view's positions to its +eye (rows, from vr.c). The game draws the scene once, from between the eyes. +A texture read of the back buffer meanwhile (a screen effect) gets the left +eye's picture. */ + +static struct +{ + BOOL active; + /* the back buffer's target, which the layers stand in for */ + struct render_target_entry *back_buffer; + /* drawn since the back buffer last got the left eye's picture */ + BOOL drawn; + GLuint color, depth, framebuffer; + unsigned long width, height; + float rows[4][4]; + unsigned long serial; +} multiview; + +/* the bound targets are the layers */ +static BOOL targets_multiview; + +static GLuint framebuffer_get(GLuint color, GLuint depth); + +/* the left eye's picture into the back buffer's own texture, for a draw +that reads it; the bindings it changes are put back */ +static void multiview_resolve(void) +{ + static GLuint read_framebuffer; + GLboolean scissor = glIsEnabled(GL_SCISSOR_TEST); + + if (!multiview.drawn) + return; + multiview.drawn = FALSE; + if (!read_framebuffer) + glGenFramebuffers(1, &read_framebuffer); + glBindFramebuffer(GL_READ_FRAMEBUFFER, read_framebuffer); + glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, multiview.color, 0, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer_get(multiview.back_buffer->target.texture, 0)); + if (scissor) + glDisable(GL_SCISSOR_TEST); + glBlitFramebuffer(0, 0, (GLint)multiview.width, (GLint)multiview.height, 0, 0, (GLint)multiview.width, + (GLint)multiview.height, GL_COLOR_BUFFER_BIT, GL_NEAREST); + if (scissor) + glEnable(GL_SCISSOR_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, gl_state.framebuffer != (GLuint)-1 ? gl_state.framebuffer : 0); +} + +int d3d8_multiview_supported(void) +{ + return device.gl_ready && xgpu_capabilities.multiview; +} + +/* rows: per view, the correction of x and of y (nv2a_vsh.c) */ +int d3d8_multiview_begin(const float rows[4][4]) +{ + struct render_target_entry *back_buffer; + + if (!d3d8_multiview_supported() || !(back_buffer = render_target_get(&device.back_buffer))) + return FALSE; + if (!multiview.framebuffer || multiview.width != back_buffer->target.gl_width || + multiview.height != back_buffer->target.gl_height) + { + static const GLenum draw_buffer = GL_COLOR_ATTACHMENT0; + GLenum status; + + if (multiview.framebuffer) + { + glDeleteFramebuffers(1, &multiview.framebuffer); + glDeleteTextures(1, &multiview.color); + glDeleteTextures(1, &multiview.depth); + } + multiview.width = back_buffer->target.gl_width; + multiview.height = back_buffer->target.gl_height; + glGenTextures(1, &multiview.color); + glBindTexture(GL_TEXTURE_2D_ARRAY, multiview.color); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, (GLsizei)multiview.width, (GLsizei)multiview.height, 2); + glGenTextures(1, &multiview.depth); + glBindTexture(GL_TEXTURE_2D_ARRAY, multiview.depth); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH24_STENCIL8, (GLsizei)multiview.width, + (GLsizei)multiview.height, 2); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + glGenFramebuffers(1, &multiview.framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, multiview.framebuffer); + glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, multiview.color, 0, 0, 2); + glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, multiview.depth, 0, 0, 2); + glDrawBuffers(1, &draw_buffer); + status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + xgpu_gl_state_invalidate(); + platform_log("multiview: %lux%lu layers, framebuffer %s", multiview.width, multiview.height, + status == GL_FRAMEBUFFER_COMPLETE ? "complete" : "INCOMPLETE"); + if (status != GL_FRAMEBUFFER_COMPLETE) + { + xgpu_capabilities.multiview = FALSE; + return FALSE; + } + } + memcpy(multiview.rows, rows, sizeof(multiview.rows)); + multiview.serial++; + multiview.back_buffer = back_buffer; + multiview.drawn = FALSE; + multiview.active = TRUE; + return TRUE; +} + +/* the eyes' layers (GL_TEXTURE_2D_ARRAY), after d3d8_multiview_end */ +unsigned int d3d8_multiview_end(void) +{ + multiview.active = FALSE; + targets_multiview = FALSE; + return multiview.color; +} + +#endif /* the pixels per unit of the bound targets (render_target_get) */ static float target_scale[2] = { 1.0f, 1.0f }; @@ -880,6 +1005,18 @@ static BOOL bind_targets(BOOL *has_depth) /* viewports and clears are in the targets' units (render_target_get) */ target_scale[0] = color ? color->target.scale[0] : depth->target.scale[0]; target_scale[1] = color ? color->target.scale[1] : depth->target.scale[1]; +#ifdef HALO_ANDROID + /* VR: the back buffer, while both eyes are drawn at once, is their + layers (d3d8_multiview_begin), with a depth buffer of their own */ + targets_multiview = multiview.active && color == multiview.back_buffer; + if (targets_multiview) + { + multiview.drawn = TRUE; + state_framebuffer(multiview.framebuffer); + *has_depth = depth != NULL; + return TRUE; + } +#endif state_framebuffer(framebuffer_get(color ? color->target.texture : 0, depth ? depth->target.texture : 0)); *has_depth = depth != NULL; return TRUE; @@ -917,9 +1054,11 @@ static void gl_initialize(void) (host_gl_has_extension("GL_EXT_texture_compression_dxt1") && host_gl_has_extension("GL_ANGLE_texture_compression_dxt3") && host_gl_has_extension("GL_ANGLE_texture_compression_dxt5")); - platform_log("OpenGL ES %d.%d: copy image %d, border clamp %d, anisotropy %d, S3TC %d, sample counting %d", - (int)major, (int)minor, xgpu_capabilities.copy_image, xgpu_capabilities.border_clamp, - xgpu_capabilities.anisotropy, xgpu_capabilities.s3tc, xgpu_capabilities.atomic_counters); + xgpu_capabilities.multiview = host_gl_has_extension("GL_OVR_multiview2"); + platform_log("OpenGL ES %d.%d: copy image %d, border clamp %d, anisotropy %d, S3TC %d, sample counting %d, " + "multiview %d", (int)major, (int)minor, xgpu_capabilities.copy_image, xgpu_capabilities.border_clamp, + xgpu_capabilities.anisotropy, xgpu_capabilities.s3tc, xgpu_capabilities.atomic_counters, + xgpu_capabilities.multiview); } #else if (config_boolean("debug.gl_debug")) @@ -1414,6 +1553,7 @@ HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) { device.frame_tests[device.buffer_ring][*count].slot = (unsigned short)index; device.frame_tests[device.buffer_ring][*count].counter = (unsigned short)device.counter_active; + device.frame_tests[device.buffer_ring][*count].views = (unsigned short)(multiview.active ? 2 : 1); (*count)++; } return S_OK; @@ -1473,7 +1613,7 @@ static void visibility_results_update(void) { const struct visibility_frame_test *entry = &device.frame_tests[ring][test]; - device.visibility_latest[entry->slot] = counts[entry->counter]; + device.visibility_latest[entry->slot] = counts[entry->counter] / (entry->views ? entry->views : 1); } } device.frame_test_count[ring] = 0; @@ -1855,14 +1995,14 @@ static unsigned long hash_words(const void *data, unsigned long size) return hash; } -static GLuint vertex_shader_get(struct vertex_shader_object *program, BOOL immediate) +static GLuint vertex_shader_get(struct vertex_shader_object *program, BOOL immediate, BOOL multiview_view) { - int variant = immediate ? 1 : 0; + int variant = (immediate ? 1 : 0) + (multiview_view ? 2 : 0); if (!program->shader[variant]) { char *source = nv2a_vertex_shader_to_glsl(program->instructions, program->instruction_count, - immediate ? 0 : device.vertex_shader->packed_mask); + immediate ? 0 : device.vertex_shader->packed_mask, multiview_view); program->shader[variant] = compile_shader(GL_VERTEX_SHADER, source, "vertex"); if (debug_settings.dump_shaders) @@ -2018,6 +2158,7 @@ static struct program_entry *program_get(GLuint vertex_shader, GLuint fragment_s entry->texture_scale = glGetUniformLocation(entry->program, "texture_scale"); entry->texture_lod_bias = glGetUniformLocation(entry->program, "texture_lod_bias"); entry->screen_offset = glGetUniformLocation(entry->program, "screen_offset"); + entry->mv_rows = glGetUniformLocation(entry->program, "mv_rows"); for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++) { char name[8]; @@ -2241,6 +2382,10 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale if (target) { +#ifdef HALO_ANDROID + if (multiview.active && multiview.back_buffer && target == &multiview.back_buffer->target) + multiview_resolve(); +#endif xgpu_texture_describe(texture->Format, texture->Size, &description); gl_texture = target->texture; gl_target = GL_TEXTURE_2D; @@ -2563,7 +2708,7 @@ static struct program_entry *prepare_draw(BOOL immediate) key.count_samples = device.visibility_test_active && xgpu_capabilities.atomic_counters; #endif - entry = program_get(vertex_shader_get(program, immediate), fragment_shader_get(&key)); + entry = program_get(vertex_shader_get(program, immediate, targets_multiview), fragment_shader_get(&key)); if (!entry) { stats.skipped_link++; @@ -2577,6 +2722,11 @@ static struct program_entry *prepare_draw(BOOL immediate) stats.draws++; state_program(entry->program); #ifdef HALO_ANDROID + if (targets_multiview && entry->mv_rows >= 0 && entry->mv_serial != multiview.serial) + { + glUniform4fv(entry->mv_rows, 4, &multiview.rows[0][0]); + entry->mv_serial = multiview.serial; + } if (key.count_samples) glBindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, device.visibility_counters, (GLintptr)(device.counter_active * sizeof(GLuint)), sizeof(GLuint)); diff --git a/port/linux/src/gl.h b/port/linux/src/gl.h index 578e0567..006baf43 100644 --- a/port/linux/src/gl.h +++ b/port/linux/src/gl.h @@ -128,6 +128,10 @@ this list to generate the guest's entry points */ X(glGenQueries) \ X(glBeginQuery) \ X(glEndQuery) \ + X(glTexStorage3D) \ + X(glFramebufferTextureLayer) \ + X(glFramebufferTextureMultiviewOVR) \ + X(glIsEnabled) \ X(glGetQueryObjectuiv) /* ANDROID_GL_FUNCTIONS_END */ #else @@ -343,6 +347,10 @@ pointers, sees the declarations without these aliases */ #define glGenQueries halo_glGenQueries #define glBeginQuery halo_glBeginQuery #define glEndQuery halo_glEndQuery +#define glTexStorage3D halo_glTexStorage3D +#define glFramebufferTextureLayer halo_glFramebufferTextureLayer +#define glFramebufferTextureMultiviewOVR halo_glFramebufferTextureMultiviewOVR +#define glIsEnabled halo_glIsEnabled #define glGetQueryObjectuiv halo_glGetQueryObjectuiv #else #define glGetString halo_glGetString diff --git a/port/linux/src/nv2a_vsh.c b/port/linux/src/nv2a_vsh.c index bfb1a7b1..bbfe7a51 100644 --- a/port/linux/src/nv2a_vsh.c +++ b/port/linux/src/nv2a_vsh.c @@ -195,13 +195,27 @@ static const char shader_prologue[] = "}\n"; char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count, - unsigned long packed_attribute_mask) + unsigned long packed_attribute_mask, int multiview) { struct xgpu_text text = { 0 }; unsigned long index; #ifdef HALO_ANDROID xgpu_text_append(&text, "#version %s\n", xgpu_capabilities.shading_language); + if (multiview) + { + /* VR: one draw into both eyes' layers. The scene is posed for a + camera between the eyes whose view covers both; each view moves the + position to its eye (half the eyes' distance aside) and onto its + own view's edges. As x' = a x + b w + c and y' likewise, with w the + depth along the view: mv_rows[2 * view] and [2 * view + 1]. */ + xgpu_text_append(&text, + "#extension GL_OVR_multiview2 : require\n" + "layout(num_views = 2) in;\n" + "uniform vec4 mv_rows[4];\n"); + } +#else + (void)multiview; #endif xgpu_text_append(&text, "%s", shader_prologue); for (index = 0; index < XGPU_VERTEX_ATTRIBUTE_COUNT; index++) @@ -360,6 +374,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru GL: rows from the top, depth 0..1 */ "\tgl_Position.y = -gl_Position.y;\n" "\tgl_Position.z = 2.0 * gl_Position.z - gl_Position.w;\n" + "%s" #endif "\tgl_PointSize = oPts.x;\n" "\txD0 = clamp(oD0, 0.0, 1.0);\n" @@ -373,7 +388,16 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru "\txFog = oFog.x;\n" "}\n" #ifdef HALO_ANDROID - , XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37 + , XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37, + /* (what is drawn in screen space, which kept no clip position, is + the same in both views) */ + multiview ? + "\tif (clip_captured)\n\t{\n" + "\t\tint row = int(gl_ViewID_OVR) * 2;\n" + "\t\tgl_Position.xy = vec2(dot(vec3(gl_Position.xw, 1.0), mv_rows[row].xyz),\n" + "\t\t\tdot(vec3(gl_Position.yw, 1.0), mv_rows[row + 1].xyz));\n" + "\t}\n" : + "" #endif ); return text.buffer; diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index adca8320..46f1702d 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -97,6 +97,9 @@ static const struct config_setting config_settings[] = "away from the player's body (walls are not checked)." }, { "vr.crouch_limit", _config_real, "0.9", "HALO_VR_CROUCH_LIMIT", _environment_value, _platform_android, "How far, in metres, the view follows your head down." }, + { "vr.multiview", _config_boolean, "true", "HALO_VR_MULTIVIEW", _environment_value, _platform_android, + "Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n" + "half the drawing's cost on the processor. false: one eye after the other." }, { "vr.log_performance", _config_boolean, "false", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android, "Log the frame rate and where the frame time goes every five seconds\n" "(adb logcat -s halo)." }, diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index f0b22a66..65693b8b 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -91,6 +91,9 @@ static struct float seat_frame_yaw; int seat_frame_valid; float seat_turn; + int multiview; /* vr.multiview: both eyes in one draw */ + int multiview_logged; + int multiview_logged_views; int vehicle_first_person; int turret_handles; /* a gun's barrel rises as its handles go down */ float gunner_camera_back; /* metres */ @@ -201,6 +204,9 @@ static struct /* d3d8_gl.c */ unsigned int d3d8_back_buffer_texture(void); +int d3d8_multiview_supported(void); +int d3d8_multiview_begin(const float rows[4][4]); +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); @@ -423,6 +429,7 @@ void halo_vr_initialize(void) vr.two_hand_reach = (float)config_real("vr.two_hand_reach"); vr.melee_swing_speed = (float)config_real("vr.melee_swing_speed"); vr.grenade_throw_speed = (float)config_real("vr.grenade_throw_speed"); + vr.multiview = config_boolean("vr.multiview"); vr.vehicle_first_person = config_boolean("vr.vehicle_first_person"); vr.vehicle_spawn = config_boolean("debug.vehicle_spawn"); vr.turret_handles = config_boolean("vr.turret_handles"); @@ -1125,6 +1132,131 @@ void halo_vr_eye_camera(int eye, const float game_position[3], frustum_bounds[3] = view->tangent_up / half_tangent; } +/* ---------- both eyes at once (vr.multiview) + +The scene is drawn once, from a camera between the eyes whose view takes in +both eyes' (with a margin, for what is near: the eyes stand apart), into two +layers; each layer's positions are moved from that camera's picture to its +eye's (d3d8_multiview_begin, nv2a_vsh.c). With d the depth along the view, +a point's picture across, x, is (2 tx - (L + R)) / (R - L) times d for the +tangent tx = X / d of the view's edges L, R; up, y, likewise, but for rows +from the top. An eye that stands t aside sees X - t: x' = a x + b d + c. */ + +#define MULTIVIEW_MARGIN 0.1f + +static float multiview_bounds[4]; /* the shared view's L, R, D, U */ +static int multiview_drawing; + +int halo_vr_multiview(void) +{ + int available = vr.active && vr.multiview && d3d8_multiview_supported(); + + if (vr.active && vr.multiview && !vr.multiview_logged) + { + 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(); +} + +void halo_vr_center_camera(const float game_position[3], float position[3], float forward[3], float up[3], + float *vertical_field_of_view, float frustum_bounds[4]) +{ + 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 const float local_right[3] = { 1.0f, 0.0f, 0.0f }; + const struct halo_xr_view *views = vr.frame.views; + float centre[3], offset[3], direction[3], right_local[3], up_local[3], rows[4][4]; + float left, right, down, top, half_tangent; + int axis, eye; + + vr.eyes_start_us = now_us(); + for (axis = 0; axis < 3; axis++) + centre[axis] = (views[0].pose.position[axis] + views[1].pose.position[axis]) * 0.5f; + tracked_offset(centre, offset); + memcpy(vr.game_camera, game_position, sizeof(vr.game_camera)); + for (axis = 0; axis < 3; axis++) + position[axis] = game_position[axis] + offset[axis] / METRES_PER_WORLD_UNIT; + /* the eyes look alike (parallel views): the left one's way */ + quaternion_rotate(views[0].pose.orientation, local_forward, direction); + local_to_game(direction, vr.body_yaw, forward); + quaternion_rotate(views[0].pose.orientation, local_up, direction); + local_to_game(direction, vr.body_yaw, up); + quaternion_rotate(views[0].pose.orientation, local_right, right_local); + quaternion_rotate(views[0].pose.orientation, local_up, up_local); + + left = fminf(views[0].tangent_left, views[1].tangent_left) - MULTIVIEW_MARGIN; + right = fmaxf(views[0].tangent_right, views[1].tangent_right) + MULTIVIEW_MARGIN; + down = fminf(views[0].tangent_down, views[1].tangent_down) - MULTIVIEW_MARGIN; + top = fmaxf(views[0].tangent_up, views[1].tangent_up) + MULTIVIEW_MARGIN; + multiview_bounds[0] = left; + multiview_bounds[1] = right; + multiview_bounds[2] = down; + multiview_bounds[3] = top; + half_tangent = (top - down) * 0.5f; + *vertical_field_of_view = 2.0f * atanf(half_tangent); + frustum_bounds[0] = left / (half_tangent * (4.0f / 3.0f)); + frustum_bounds[1] = right / (half_tangent * (4.0f / 3.0f)); + frustum_bounds[2] = down / half_tangent; + frustum_bounds[3] = top / half_tangent; + + for (eye = 0; eye < HALO_XR_EYE_COUNT; eye++) + { + const struct halo_xr_view *view = &views[eye]; + float aside[3], tx, ty; + float eye_width = view->tangent_right - view->tangent_left; + float eye_height = view->tangent_up - view->tangent_down; + + /* the eye's place from the shared camera, in its view's axes (world + units) */ + for (axis = 0; axis < 3; axis++) + aside[axis] = view->pose.position[axis] - centre[axis]; + tx = (aside[0] * right_local[0] + aside[1] * right_local[1] + aside[2] * right_local[2]) / + METRES_PER_WORLD_UNIT; + ty = (aside[0] * up_local[0] + aside[1] * up_local[1] + aside[2] * up_local[2]) / METRES_PER_WORLD_UNIT; + rows[eye * 2][0] = (right - left) / eye_width; + rows[eye * 2][1] = (left + right - view->tangent_left - view->tangent_right) / eye_width; + rows[eye * 2][2] = -2.0f * tx / eye_width; + rows[eye * 2][3] = 0.0f; + /* (rows from the top: y runs against the tangents) */ + rows[eye * 2 + 1][0] = (top - down) / eye_height; + rows[eye * 2 + 1][1] = -(down + top - view->tangent_down - view->tangent_up) / eye_height; + rows[eye * 2 + 1][2] = 2.0f * ty / eye_height; + rows[eye * 2 + 1][3] = 0.0f; + } + if (!vr.multiview_logged_views) + { + /* (the correction assumes parallel views: say if they are not) */ + float other[3], apart[3]; + + quaternion_rotate(views[1].pose.orientation, local_forward, other); + quaternion_rotate(views[0].pose.orientation, local_forward, direction); + for (axis = 0; axis < 3; axis++) + apart[axis] = views[1].pose.position[axis] - views[0].pose.position[axis]; + platform_log("VR multiview: views %.2f degrees apart, eyes %.1f mm apart; shared view %.2f..%.2f, %.2f..%.2f", + acosf(fminf(1.0f, direction[0] * other[0] + direction[1] * other[1] + direction[2] * other[2])) * 180.0f / PI, + sqrtf(apart[0] * apart[0] + apart[1] * apart[1] + apart[2] * apart[2]) * 1000.0f, left, right, down, top); + vr.multiview_logged_views = 1; + } + multiview_drawing = d3d8_multiview_begin(rows); +} + +/* after the shared draw: its layers become the eyes' images */ +void halo_vr_capture_multiview(void) +{ + unsigned int layers = d3d8_multiview_end(); + + if (!multiview_drawing) + return; + multiview_drawing = 0; + if (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)) + vr.eyes_captured |= 2; + if (vr.eyes_start_us) + vr.eyes_us += now_us() - vr.eyes_start_us; +} + void halo_vr_capture_eye(int eye) { if (!vr.active || eye < 0 || eye >= HALO_XR_EYE_COUNT) @@ -1872,6 +2004,13 @@ void halo_vr_eye_camera(int eye, const float game_position[3], float position[3] (void)vertical_field_of_view; (void)frustum_bounds; } void halo_vr_capture_eye(int eye) { (void)eye; } +int halo_vr_multiview(void) { return 0; } +void halo_vr_center_camera(const float game_position[3], float position[3], float forward[3], float up[3], + float *vertical_field_of_view, float frustum_bounds[4]) +{ + (void)game_position; (void)position; (void)forward; (void)up; (void)vertical_field_of_view; (void)frustum_bounds; +} +void halo_vr_capture_multiview(void) {} void halo_vr_begin_overlay(int menus_active) { (void)menus_active; } int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch, float *yaw, float *pitch) { diff --git a/port/linux/src/xgpu.h b/port/linux/src/xgpu.h index bbc92c3f..38d44937 100644 --- a/port/linux/src/xgpu.h +++ b/port/linux/src/xgpu.h @@ -27,6 +27,8 @@ struct xgpu_capabilities BOOL atomic_counters; /* "300 es" or "310 es" */ const char *shading_language; + /* GL_OVR_multiview2: both eyes in one draw (VR, d3d8_multiview_begin) */ + BOOL multiview; }; extern struct xgpu_capabilities xgpu_capabilities; @@ -73,8 +75,10 @@ void xgpu_text_append(struct xgpu_text *text, const char *format, ...) __attribu header). Attributes whose bit is set in packed_attribute_mask are fed as NORMPACKED3 32-bit integers and unpacked in the shader. Returns a malloc'd string. */ +/* multiview: for a two-view (GL_OVR_multiview2) target, each view's +position corrected from the shared one by mv_rows (d3d8_gl.c, VR) */ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count, - unsigned long packed_attribute_mask); + unsigned long packed_attribute_mask, int multiview); /* ---------- pixel shaders */ diff --git a/source/render/render.c b/source/render/render.c index a314626d..dbc003b7 100644 --- a/source/render/render.c +++ b/source/render/render.c @@ -700,7 +700,29 @@ static void render_vr_player_frame( if (nodes) halo_vr_set_seat_frame(window->local_player_index, (float)atan2(nodes[0].forward.j, nodes[0].forward.i)); } - for (eye = 0; eye < 2; eye++) + if (halo_vr_multiview()) + { + /* both eyes at once, from between them */ + struct render_window eye_window = *window; + float position[3], forward[3], up[3], bounds[4]; + float vertical_field_of_view; + real_rectangle2d frustum_bounds; + + halo_vr_center_camera(game_position, position, forward, up, &vertical_field_of_view, bounds); + render_vr_camera_set(&eye_window.render_camera, position, forward, up, vertical_field_of_view); + render_vr_camera_set(&eye_window.rasterizer_camera, position, forward, up, vertical_field_of_view); + frustum_bounds.x0 = bounds[0]; + frustum_bounds.x1 = bounds[1]; + frustum_bounds.y0 = bounds[2]; + frustum_bounds.y1 = bounds[3]; + render_vr_eye_pass = TRUE; + render_vr_frustum_bounds = &frustum_bounds; + render_player_frame(&eye_window, NULL); + render_vr_frustum_bounds = NULL; + render_vr_eye_pass = FALSE; + halo_vr_capture_multiview(); + } + else for (eye = 0; eye < 2; eye++) { struct render_window eye_window = *window; float position[3], forward[3], up[3], bounds[4]; From df45daed55bc7d0905511e5b1389e37c68fefd88 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:52:40 +0200 Subject: [PATCH 02/10] VR multiview: the sky as though infinitely far The sky is drawn about the camera (its view has no position), so no eye stands aside of it; the eye's offset made it a dome a few metres away, flat and swimming (the sea in 'Halo' is the sky's). Its draws take rows without the offset. Co-Authored-By: Claude Opus 5.5 --- port/linux/include/halo_vr.h | 2 ++ port/linux/src/d3d8_gl.c | 9 +++++++++ port/linux/src/port_config.c | 2 +- port/linux/src/vr.c | 16 ++++++++++++++++ source/render/render.c | 6 ++++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/port/linux/include/halo_vr.h b/port/linux/include/halo_vr.h index 8b7e628a..7afdc19b 100644 --- a/port/linux/include/halo_vr.h +++ b/port/linux/include/halo_vr.h @@ -58,6 +58,8 @@ int halo_vr_multiview(void); void halo_vr_center_camera(const float game_position[3], float position[3], float forward[3], float up[3], float *vertical_field_of_view, float frustum_bounds[4]); void halo_vr_capture_multiview(void); +/* the sky, drawn about the camera, is being drawn: infinitely far */ +void halo_vr_multiview_sky(int sky); /* clears the back buffer to transparent black for the overlay, after the eyes; menus_active chooses where the overlay is shown */ diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 9debc929..49187907 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -971,6 +971,15 @@ int d3d8_multiview_begin(const float rows[4][4]) return TRUE; } +/* new rows for the draws that follow (the sky's, vr.c) */ +void d3d8_multiview_rows(const float rows[4][4]) +{ + if (!multiview.active) + return; + memcpy(multiview.rows, rows, sizeof(multiview.rows)); + multiview.serial++; +} + /* the eyes' layers (GL_TEXTURE_2D_ARRAY), after d3d8_multiview_end */ unsigned int d3d8_multiview_end(void) { diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 46f1702d..e8809eeb 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -100,7 +100,7 @@ static const struct config_setting config_settings[] = { "vr.multiview", _config_boolean, "true", "HALO_VR_MULTIVIEW", _environment_value, _platform_android, "Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n" "half the drawing's cost on the processor. false: one eye after the other." }, - { "vr.log_performance", _config_boolean, "false", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android, + { "vr.log_performance", _config_boolean, "true", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android, "Log the frame rate and where the frame time goes every five seconds\n" "(adb logcat -s halo)." }, { "vr.aim_with", _config_string, "\"controller\"", "HALO_VR_AIM_WITH", _environment_value, _platform_android, diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index 65693b8b..b6812468 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -206,6 +206,7 @@ static struct unsigned int d3d8_back_buffer_texture(void); int d3d8_multiview_supported(void); int d3d8_multiview_begin(const float rows[4][4]); +void d3d8_multiview_rows(const float rows[4][4]); 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); @@ -1146,6 +1147,9 @@ from the top. An eye that stands t aside sees X - t: x' = a x + b d + c. */ static float multiview_bounds[4]; /* the shared view's L, R, D, U */ static int multiview_drawing; +/* the rows for what is drawn in the world, and for the sky, which is drawn +about the camera, as though infinitely far: no eye stands aside of it */ +static float multiview_rows[2][4][4]; int halo_vr_multiview(void) { @@ -1224,6 +1228,10 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa rows[eye * 2 + 1][2] = 2.0f * ty / eye_height; rows[eye * 2 + 1][3] = 0.0f; } + memcpy(multiview_rows[0], rows, sizeof(rows)); + memcpy(multiview_rows[1], rows, sizeof(rows)); + for (eye = 0; eye < HALO_XR_EYE_COUNT; eye++) + multiview_rows[1][eye * 2][2] = multiview_rows[1][eye * 2 + 1][2] = 0.0f; if (!vr.multiview_logged_views) { /* (the correction assumes parallel views: say if they are not) */ @@ -1241,6 +1249,13 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa multiview_drawing = d3d8_multiview_begin(rows); } +/* the sky is being drawn (render.c): as though infinitely far */ +void halo_vr_multiview_sky(int sky) +{ + if (multiview_drawing) + d3d8_multiview_rows(multiview_rows[sky ? 1 : 0]); +} + /* after the shared draw: its layers become the eyes' images */ void halo_vr_capture_multiview(void) { @@ -2011,6 +2026,7 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa (void)game_position; (void)position; (void)forward; (void)up; (void)vertical_field_of_view; (void)frustum_bounds; } void halo_vr_capture_multiview(void) {} +void halo_vr_multiview_sky(int sky) { (void)sky; } void halo_vr_begin_overlay(int menus_active) { (void)menus_active; } int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch, float *yaw, float *pitch) { diff --git a/source/render/render.c b/source/render/render.c index dbc003b7..75f21307 100644 --- a/source/render/render.c +++ b/source/render/render.c @@ -389,7 +389,13 @@ static void render_window( if (!bink_playback_in_progress()) { build_sprite_prepare_for_window(); +#ifdef HALO_ANDROID + halo_vr_multiview_sky(TRUE); +#endif render_sky(); +#ifdef HALO_ANDROID + halo_vr_multiview_sky(FALSE); +#endif first_person_weapon_render_update(); lights_preprocess_scene(); render_objects(); From 1302c8613ca83f739c88b317d3c93cebe5d9eb66 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:13:47 +0200 Subject: [PATCH 03/10] VR multiview: screen-space draws to each view as though far A lens flare is drawn in screen space, at where the sun is in the shared view, which is wider than an eye's: drawn alike in both layers, the sun and its occlusion test landed elsewhere in each eye (the sky seemed to tear open onto the sun). Screen-space positions now take each view's mapping without the eye's offset. Co-Authored-By: Claude Opus 5.5 --- port/linux/src/nv2a_vsh.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/port/linux/src/nv2a_vsh.c b/port/linux/src/nv2a_vsh.c index bbfe7a51..da81d86d 100644 --- a/port/linux/src/nv2a_vsh.c +++ b/port/linux/src/nv2a_vsh.c @@ -208,7 +208,8 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru camera between the eyes whose view covers both; each view moves the position to its eye (half the eyes' distance aside) and onto its own view's edges. As x' = a x + b w + c and y' likewise, with w the - depth along the view: mv_rows[2 * view] and [2 * view + 1]. */ + depth along the view: mv_rows[2 * view] and [2 * view + 1]; + what is drawn in screen space goes without c, as though far. */ xgpu_text_append(&text, "#extension GL_OVR_multiview2 : require\n" "layout(num_views = 2) in;\n" @@ -390,12 +391,14 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru #ifdef HALO_ANDROID , XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37, /* (what is drawn in screen space, which kept no clip position, is - the same in both views) */ + taken to each view as though infinitely far: a lens flare's sun + lands where the sky's does) */ multiview ? - "\tif (clip_captured)\n\t{\n" + "\t{\n" "\t\tint row = int(gl_ViewID_OVR) * 2;\n" - "\t\tgl_Position.xy = vec2(dot(vec3(gl_Position.xw, 1.0), mv_rows[row].xyz),\n" - "\t\t\tdot(vec3(gl_Position.yw, 1.0), mv_rows[row + 1].xyz));\n" + "\t\tfloat near = clip_captured ? 1.0 : 0.0;\n" + "\t\tgl_Position.xy = vec2(dot(vec3(gl_Position.xw, near), mv_rows[row].xyz),\n" + "\t\t\tdot(vec3(gl_Position.yw, near), mv_rows[row + 1].xyz));\n" "\t}\n" : "" #endif From 2104fb230eba746862b434e105e548fbe42f44b3 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:21:32 +0200 Subject: [PATCH 04/10] VR multiview: the back buffer read gets the left eye where it lies The sun's glow measures the picture about the sun where the shared camera has it; the left eye's picture, copied as it is, put the sun elsewhere (a black patch on the rock, the sun through a gap as the head turned). It is now laid where it lies in the shared camera's picture. Logs, once each, the render targets drawing into both eyes reads. Co-Authored-By: Claude Opus 5.5 --- port/linux/src/d3d8_gl.c | 40 ++++++++++++++++++++++++++++++++++------ port/linux/src/vr.c | 14 ++++++++++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 49187907..0bf59b1e 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -869,8 +869,9 @@ Between d3d8_multiview_begin and _end, what the game draws into the back buffer goes into two layers at once, one per eye: every vertex program is compiled for two views (nv2a_vsh.c) and moves each view's positions to its eye (rows, from vr.c). The game draws the scene once, from between the eyes. -A texture read of the back buffer meanwhile (a screen effect) gets the left -eye's picture. */ +A texture read of the back buffer meanwhile (a screen effect: the sun's +glow measures the picture about the sun) gets the left eye's picture, laid +where it lies in the shared camera's (resolve_rect). */ static struct { @@ -883,6 +884,9 @@ static struct unsigned long width, height; float rows[4][4]; unsigned long serial; + /* where the left eye's picture lies in the shared camera's: left, top, + right, bottom, 0..1 from its top left */ + float resolve_rect[4]; } multiview; /* the bound targets are the layers */ @@ -907,8 +911,12 @@ static void multiview_resolve(void) glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer_get(multiview.back_buffer->target.texture, 0)); if (scissor) glDisable(GL_SCISSOR_TEST); - glBlitFramebuffer(0, 0, (GLint)multiview.width, (GLint)multiview.height, 0, 0, (GLint)multiview.width, - (GLint)multiview.height, GL_COLOR_BUFFER_BIT, GL_NEAREST); + /* (rows from the top in both: GL's y 0 is the picture's top) */ + glBlitFramebuffer(0, 0, (GLint)multiview.width, (GLint)multiview.height, + (GLint)(multiview.resolve_rect[0] * multiview.width + 0.5f), + (GLint)(multiview.resolve_rect[1] * multiview.height + 0.5f), + (GLint)(multiview.resolve_rect[2] * multiview.width + 0.5f), + (GLint)(multiview.resolve_rect[3] * multiview.height + 0.5f), GL_COLOR_BUFFER_BIT, GL_LINEAR); if (scissor) glEnable(GL_SCISSOR_TEST); glBindFramebuffer(GL_FRAMEBUFFER, gl_state.framebuffer != (GLuint)-1 ? gl_state.framebuffer : 0); @@ -919,8 +927,9 @@ int d3d8_multiview_supported(void) return device.gl_ready && xgpu_capabilities.multiview; } -/* rows: per view, the correction of x and of y (nv2a_vsh.c) */ -int d3d8_multiview_begin(const float rows[4][4]) +/* rows: per view, the correction of x and of y (nv2a_vsh.c); resolve_rect: +where the left eye's picture lies in the shared camera's */ +int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[4]) { struct render_target_entry *back_buffer; @@ -964,6 +973,7 @@ int d3d8_multiview_begin(const float rows[4][4]) } } memcpy(multiview.rows, rows, sizeof(multiview.rows)); + memcpy(multiview.resolve_rect, resolve_rect, sizeof(multiview.resolve_rect)); multiview.serial++; multiview.back_buffer = back_buffer; multiview.drawn = FALSE; @@ -2394,6 +2404,24 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale #ifdef HALO_ANDROID if (multiview.active && multiview.back_buffer && target == &multiview.back_buffer->target) multiview_resolve(); + if (multiview.active) + { + /* (diagnostics: what a draw into both eyes reads) */ + static unsigned long logged[16]; + int slot, known = FALSE; + + for (slot = 0; slot < 16 && logged[slot]; slot++) + known |= logged[slot] == target->data; + if (!known && slot < 16) + { + logged[slot] = target->data; + platform_log("multiview: a draw reads render target %08lx (%lux%lu%s%s), vertex program %lu", + target->data, target->width, target->height, target->depth ? ", depth" : "", + target == &multiview.back_buffer->target ? ", the back buffer" : + target->data == device.depth_buffer.Data ? ", the depth buffer" : "", + current_program() ? current_program()->id : 0); + } + } #endif xgpu_texture_describe(texture->Format, texture->Size, &description); gl_texture = target->texture; diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index b6812468..712ed91b 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -205,7 +205,7 @@ static struct /* d3d8_gl.c */ unsigned int d3d8_back_buffer_texture(void); int d3d8_multiview_supported(void); -int d3d8_multiview_begin(const float rows[4][4]); +int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[4]); void d3d8_multiview_rows(const float rows[4][4]); unsigned int d3d8_multiview_end(void); void d3d8_clear_back_buffer(float red, float green, float blue, float alpha); @@ -1246,7 +1246,17 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa sqrtf(apart[0] * apart[0] + apart[1] * apart[1] + apart[2] * apart[2]) * 1000.0f, left, right, down, top); vr.multiview_logged_views = 1; } - multiview_drawing = d3d8_multiview_begin(rows); + { + /* the left eye's picture's place in the shared one (rows from the + top) */ + float resolve_rect[4]; + + resolve_rect[0] = (views[0].tangent_left - left) / (right - left); + resolve_rect[1] = (top - views[0].tangent_up) / (top - down); + resolve_rect[2] = (views[0].tangent_right - left) / (right - left); + resolve_rect[3] = (top - views[0].tangent_down) / (top - down); + multiview_drawing = d3d8_multiview_begin(rows, resolve_rect); + } } /* the sky is being drawn (render.c): as though infinitely far */ From 353bd19d94d4d134e5eb42de68af290454c111fe Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:23:59 +0200 Subject: [PATCH 05/10] VR multiview: the back buffer read gets both eyes, the left over the right The shared view reaches further right than the left eye: the sun there was measured on a stale picture and its glow shrank as the head turned. Co-Authored-By: Claude Opus 5.5 --- port/linux/src/d3d8_gl.c | 36 +++++++++++++++++++++--------------- port/linux/src/vr.c | 18 ++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 0bf59b1e..11009312 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -870,8 +870,9 @@ buffer goes into two layers at once, one per eye: every vertex program is compiled for two views (nv2a_vsh.c) and moves each view's positions to its eye (rows, from vr.c). The game draws the scene once, from between the eyes. A texture read of the back buffer meanwhile (a screen effect: the sun's -glow measures the picture about the sun) gets the left eye's picture, laid -where it lies in the shared camera's (resolve_rect). */ +glow measures the picture about the sun) gets the eyes' pictures, each laid +where it lies in the shared camera's (resolve_rect), the left one over the +right. */ static struct { @@ -884,9 +885,9 @@ static struct unsigned long width, height; float rows[4][4]; unsigned long serial; - /* where the left eye's picture lies in the shared camera's: left, top, + /* where each eye's picture lies in the shared camera's: left, top, right, bottom, 0..1 from its top left */ - float resolve_rect[4]; + float resolve_rect[2][4]; } multiview; /* the bound targets are the layers */ @@ -894,12 +895,13 @@ static BOOL targets_multiview; static GLuint framebuffer_get(GLuint color, GLuint depth); -/* the left eye's picture into the back buffer's own texture, for a draw -that reads it; the bindings it changes are put back */ +/* the eyes' pictures into the back buffer's own texture, for a draw that +reads it; the bindings it changes are put back */ static void multiview_resolve(void) { static GLuint read_framebuffer; GLboolean scissor = glIsEnabled(GL_SCISSOR_TEST); + int eye; if (!multiview.drawn) return; @@ -907,16 +909,20 @@ static void multiview_resolve(void) if (!read_framebuffer) glGenFramebuffers(1, &read_framebuffer); glBindFramebuffer(GL_READ_FRAMEBUFFER, read_framebuffer); - glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, multiview.color, 0, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer_get(multiview.back_buffer->target.texture, 0)); if (scissor) glDisable(GL_SCISSOR_TEST); - /* (rows from the top in both: GL's y 0 is the picture's top) */ - glBlitFramebuffer(0, 0, (GLint)multiview.width, (GLint)multiview.height, - (GLint)(multiview.resolve_rect[0] * multiview.width + 0.5f), - (GLint)(multiview.resolve_rect[1] * multiview.height + 0.5f), - (GLint)(multiview.resolve_rect[2] * multiview.width + 0.5f), - (GLint)(multiview.resolve_rect[3] * multiview.height + 0.5f), GL_COLOR_BUFFER_BIT, GL_LINEAR); + for (eye = 1; eye >= 0; eye--) + { + const float *rect = multiview.resolve_rect[eye]; + + glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, multiview.color, 0, eye); + /* (rows from the top in both: GL's y 0 is the picture's top) */ + glBlitFramebuffer(0, 0, (GLint)multiview.width, (GLint)multiview.height, + (GLint)(rect[0] * multiview.width + 0.5f), (GLint)(rect[1] * multiview.height + 0.5f), + (GLint)(rect[2] * multiview.width + 0.5f), (GLint)(rect[3] * multiview.height + 0.5f), + GL_COLOR_BUFFER_BIT, GL_LINEAR); + } if (scissor) glEnable(GL_SCISSOR_TEST); glBindFramebuffer(GL_FRAMEBUFFER, gl_state.framebuffer != (GLuint)-1 ? gl_state.framebuffer : 0); @@ -928,8 +934,8 @@ int d3d8_multiview_supported(void) } /* rows: per view, the correction of x and of y (nv2a_vsh.c); resolve_rect: -where the left eye's picture lies in the shared camera's */ -int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[4]) +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]) { struct render_target_entry *back_buffer; diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index 712ed91b..9f0ab764 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -205,7 +205,7 @@ static struct /* d3d8_gl.c */ unsigned int d3d8_back_buffer_texture(void); int d3d8_multiview_supported(void); -int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[4]); +int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4]); void d3d8_multiview_rows(const float rows[4][4]); unsigned int d3d8_multiview_end(void); void d3d8_clear_back_buffer(float red, float green, float blue, float alpha); @@ -1247,14 +1247,16 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa vr.multiview_logged_views = 1; } { - /* the left eye's picture's place in the shared one (rows from the - top) */ - float resolve_rect[4]; + /* each eye's picture's place in the shared one (rows from the top) */ + float resolve_rect[2][4]; - resolve_rect[0] = (views[0].tangent_left - left) / (right - left); - resolve_rect[1] = (top - views[0].tangent_up) / (top - down); - resolve_rect[2] = (views[0].tangent_right - left) / (right - left); - resolve_rect[3] = (top - views[0].tangent_down) / (top - down); + for (eye = 0; eye < HALO_XR_EYE_COUNT; eye++) + { + resolve_rect[eye][0] = (views[eye].tangent_left - left) / (right - left); + resolve_rect[eye][1] = (top - views[eye].tangent_up) / (top - down); + resolve_rect[eye][2] = (views[eye].tangent_right - left) / (right - left); + resolve_rect[eye][3] = (top - views[eye].tangent_down) / (top - down); + } multiview_drawing = d3d8_multiview_begin(rows, resolve_rect); } } From 19681e60d577dcb5ed54afff29090a8cce8ceb12 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:31:02 +0200 Subject: [PATCH 06/10] VR multiview: drop the test logging; performance log off again Measured at one spot in 'Halo': both eyes 6.3 ms one by one, 4.0 ms at once; the frame's work 7.9 ms against 5.8 ms (90 Hz). Co-Authored-By: Claude Opus 5.5 --- port/linux/src/d3d8_gl.c | 18 ------------------ port/linux/src/port_config.c | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 11009312..334121ca 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -2410,24 +2410,6 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale #ifdef HALO_ANDROID if (multiview.active && multiview.back_buffer && target == &multiview.back_buffer->target) multiview_resolve(); - if (multiview.active) - { - /* (diagnostics: what a draw into both eyes reads) */ - static unsigned long logged[16]; - int slot, known = FALSE; - - for (slot = 0; slot < 16 && logged[slot]; slot++) - known |= logged[slot] == target->data; - if (!known && slot < 16) - { - logged[slot] = target->data; - platform_log("multiview: a draw reads render target %08lx (%lux%lu%s%s), vertex program %lu", - target->data, target->width, target->height, target->depth ? ", depth" : "", - target == &multiview.back_buffer->target ? ", the back buffer" : - target->data == device.depth_buffer.Data ? ", the depth buffer" : "", - current_program() ? current_program()->id : 0); - } - } #endif xgpu_texture_describe(texture->Format, texture->Size, &description); gl_texture = target->texture; diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index e8809eeb..46f1702d 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -100,7 +100,7 @@ static const struct config_setting config_settings[] = { "vr.multiview", _config_boolean, "true", "HALO_VR_MULTIVIEW", _environment_value, _platform_android, "Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n" "half the drawing's cost on the processor. false: one eye after the other." }, - { "vr.log_performance", _config_boolean, "true", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android, + { "vr.log_performance", _config_boolean, "false", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android, "Log the frame rate and where the frame time goes every five seconds\n" "(adb logcat -s halo)." }, { "vr.aim_with", _config_string, "\"controller\"", "HALO_VR_AIM_WITH", _environment_value, _platform_android, From ba3997e2f4f22fd2355d798950e0966de3cd89ab Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:24:49 +0200 Subject: [PATCH 07/10] VR multiview: what the game projects itself gets its depth's parallax Sprites, light volumes (a plasma bolt's glow) and lens flare tests are projected on the CPU into screen pixels with w 1 and their depth in z, and the widgets' vertex program takes them to clip space: taken for a point 1 world unit away, a bolt 30 away stood 3 m off in stereo and seemed to be swallowed half way. Where w is 1 and depth is tested, the depth comes from z by the shared camera's projection (mv_depth), and the eye's offset is by it. (Also logs, once each, the other targets the pass draws into.) Co-Authored-By: Claude Opus 5.5 --- port/linux/include/halo_vr.h | 3 +++ port/linux/src/d3d8_gl.c | 50 +++++++++++++++++++++++++++++++++++- port/linux/src/nv2a_vsh.c | 16 ++++++++++-- port/linux/src/vr.c | 12 +++++++++ source/render/render.c | 7 +++++ 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/port/linux/include/halo_vr.h b/port/linux/include/halo_vr.h index 7afdc19b..c021f1c4 100644 --- a/port/linux/include/halo_vr.h +++ b/port/linux/include/halo_vr.h @@ -60,6 +60,9 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa void halo_vr_capture_multiview(void); /* the sky, drawn about the camera, is being drawn: infinitely far */ 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); /* clears the back buffer to transparent black for the overlay, after the eyes; menus_active chooses where the overlay is shown */ diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 334121ca..dec0f546 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -226,9 +226,12 @@ struct program_entry GLint texture_lod_bias; GLint screen_offset; /* multiview: each view's correction (nv2a_vsh.c), and multiview.serial - when it was last uploaded */ + when it was last uploaded; the projection's depth, and whether the draw + tests depth (what the uniform holds) */ GLint mv_rows; unsigned long mv_serial; + GLint mv_depth; + float mv_depth_value[4]; /* the vertex constants c[0..constant_count) the program uses; with consecutive locations, a changed range is uploaded by itself */ @@ -888,11 +891,16 @@ static struct /* where each eye's picture lies in the shared camera's: left, top, right, bottom, 0..1 from its top left */ float resolve_rect[2][4]; + /* 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]; } multiview; /* the bound targets are the layers */ static BOOL targets_multiview; +static struct vertex_shader_object *current_program(void); + static GLuint framebuffer_get(GLuint color, GLuint depth); /* the eyes' pictures into the back buffer's own texture, for a draw that @@ -987,6 +995,14 @@ int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4]) return TRUE; } +/* 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) +{ + multiview.projection_depth[0] = a; + multiview.projection_depth[1] = b; +} + /* new rows for the draws that follow (the sky's, vr.c) */ void d3d8_multiview_rows(const float rows[4][4]) { @@ -1034,6 +1050,26 @@ static BOOL bind_targets(BOOL *has_depth) /* VR: the back buffer, while both eyes are drawn at once, is their layers (d3d8_multiview_begin), with a depth buffer of their own */ targets_multiview = multiview.active && color == multiview.back_buffer; + if (multiview.active && !targets_multiview) + { + /* (diagnostics: what else a draw into both eyes' pass draws into) */ + static unsigned long logged[16][2]; + unsigned long color_data = color ? color->target.data : 0; + unsigned long depth_data = depth ? depth->target.data : 0; + int slot, known = FALSE; + + for (slot = 0; slot < 16 && (logged[slot][0] || logged[slot][1]); slot++) + known |= logged[slot][0] == color_data && logged[slot][1] == depth_data; + if (!known && slot < 16) + { + logged[slot][0] = color_data; + logged[slot][1] = depth_data; + platform_log("multiview: the pass also draws into %08lx (%lux%lu) with depth %08lx%s, vertex program %lu", + color_data, color ? color->target.width : 0, color ? color->target.height : 0, depth_data, + depth_data && depth_data == device.depth_buffer.Data ? " (the screen's depth buffer)" : "", + current_program() ? current_program()->id : 0); + } + } if (targets_multiview) { multiview.drawn = TRUE; @@ -2184,6 +2220,8 @@ static struct program_entry *program_get(GLuint vertex_shader, GLuint fragment_s entry->texture_lod_bias = glGetUniformLocation(entry->program, "texture_lod_bias"); entry->screen_offset = glGetUniformLocation(entry->program, "screen_offset"); entry->mv_rows = glGetUniformLocation(entry->program, "mv_rows"); + entry->mv_depth = glGetUniformLocation(entry->program, "mv_depth"); + entry->mv_depth_value[0] = entry->mv_depth_value[1] = -1.0e30f; for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++) { char name[8]; @@ -2752,6 +2790,16 @@ static struct program_entry *prepare_draw(BOOL immediate) glUniform4fv(entry->mv_rows, 4, &multiview.rows[0][0]); entry->mv_serial = multiview.serial; } + if (targets_multiview && entry->mv_depth >= 0) + { + float depth[4]; + + 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[3] = 0.0f; + uniform_vec4(entry->mv_depth, entry->mv_depth_value, depth, 1); + } if (key.count_samples) glBindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, device.visibility_counters, (GLintptr)(device.counter_active * sizeof(GLuint)), sizeof(GLuint)); diff --git a/port/linux/src/nv2a_vsh.c b/port/linux/src/nv2a_vsh.c index da81d86d..bee469c9 100644 --- a/port/linux/src/nv2a_vsh.c +++ b/port/linux/src/nv2a_vsh.c @@ -213,7 +213,9 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru xgpu_text_append(&text, "#extension GL_OVR_multiview2 : require\n" "layout(num_views = 2) in;\n" - "uniform vec4 mv_rows[4];\n"); + "uniform vec4 mv_rows[4];\n" + /* the projection's depth: z = x + y / depth; z: depth tested */ + "uniform vec4 mv_depth;\n"); } #else (void)multiview; @@ -393,10 +395,20 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru /* (what is drawn in screen space, which kept no clip position, is taken to each view as though infinitely far: a lens flare's sun lands where the sky's does) */ + /* (what the game projects itself, a sprite or a lens flare's + test, comes as a screen position with w 1 and its depth in z: its + eye's offset is by that depth, where it is depth tested) */ multiview ? "\t{\n" "\t\tint row = int(gl_ViewID_OVR) * 2;\n" - "\t\tfloat near = clip_captured ? 1.0 : 0.0;\n" + "\t\tfloat near = 0.0;\n" + "\t\tif (clip_captured && abs(clip_position.w - 1.0) > 1.0e-5)\n" + "\t\t\tnear = 1.0;\n" + "\t\telse if (clip_captured && mv_depth.z > 0.5)\n" + "\t\t{\n" + "\t\t\tfloat depth = mv_depth.y / (clip_position.z - mv_depth.x);\n" + "\t\t\tnear = depth > 0.0 ? 1.0 / depth : 0.0;\n" + "\t\t}\n" "\t\tgl_Position.xy = vec2(dot(vec3(gl_Position.xw, near), mv_rows[row].xyz),\n" "\t\t\tdot(vec3(gl_Position.yw, near), mv_rows[row + 1].xyz));\n" "\t}\n" : diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index 9f0ab764..9c57f6e4 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -207,6 +207,7 @@ unsigned int d3d8_back_buffer_texture(void); 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); 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); @@ -1261,6 +1262,13 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa } } +/* the shared camera's projection's depth (render.c: its [2][2] and [3][2]): +what the game projects itself carries z = -[2][2] + [3][2] / depth */ +void halo_vr_multiview_projection(float projection_22, float projection_32) +{ + d3d8_multiview_depth(-projection_22, projection_32); +} + /* the sky is being drawn (render.c): as though infinitely far */ void halo_vr_multiview_sky(int sky) { @@ -2039,6 +2047,10 @@ 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_projection(float projection_22, float projection_32) +{ + (void)projection_22; (void)projection_32; +} void halo_vr_begin_overlay(int menus_active) { (void)menus_active; } int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch, float *yaw, float *pitch) { diff --git a/source/render/render.c b/source/render/render.c index 75f21307..5ceb73fd 100644 --- a/source/render/render.c +++ b/source/render/render.c @@ -721,6 +721,13 @@ static void render_vr_player_frame( frustum_bounds.x1 = bounds[1]; frustum_bounds.y0 = bounds[2]; frustum_bounds.y1 = bounds[3]; + { + /* the depth of what the game projects itself (its sprites) */ + struct render_frustum frustum; + + render_camera_build_frustum(&eye_window.rasterizer_camera, &frustum_bounds, &frustum, TRUE); + halo_vr_multiview_projection(frustum.projection_matrix[2][2], frustum.projection_matrix[3][2]); + } render_vr_eye_pass = TRUE; render_vr_frustum_bounds = &frustum_bounds; render_player_frame(&eye_window, NULL); From 1713082296b868b5f37722acb8c53850724d36f3 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:31:41 +0200 Subject: [PATCH 08/10] VR multiview: log what may still be off (screen-space depth-tested draws, picture reads) Co-Authored-By: Claude Opus 5.5 --- port/linux/src/d3d8_gl.c | 51 ++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index dec0f546..78a448d7 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -178,6 +178,9 @@ struct vertex_shader_object /* [0] streams per the declaration, [1] immediate mode (all floats); [2] and [3] the same for both eyes at once (multiview) */ GLuint shader[4]; + /* its position is a projected one (nv2a_vsh.c's clip capture): known, + and so; and whether a multiview draw with it was looked at (diagnostics) */ + BOOL projection_known, projected, multiview_checked; }; /* ---------- programs */ @@ -1050,26 +1053,6 @@ static BOOL bind_targets(BOOL *has_depth) /* VR: the back buffer, while both eyes are drawn at once, is their layers (d3d8_multiview_begin), with a depth buffer of their own */ targets_multiview = multiview.active && color == multiview.back_buffer; - if (multiview.active && !targets_multiview) - { - /* (diagnostics: what else a draw into both eyes' pass draws into) */ - static unsigned long logged[16][2]; - unsigned long color_data = color ? color->target.data : 0; - unsigned long depth_data = depth ? depth->target.data : 0; - int slot, known = FALSE; - - for (slot = 0; slot < 16 && (logged[slot][0] || logged[slot][1]); slot++) - known |= logged[slot][0] == color_data && logged[slot][1] == depth_data; - if (!known && slot < 16) - { - logged[slot][0] = color_data; - logged[slot][1] = depth_data; - platform_log("multiview: the pass also draws into %08lx (%lux%lu) with depth %08lx%s, vertex program %lu", - color_data, color ? color->target.width : 0, color ? color->target.height : 0, depth_data, - depth_data && depth_data == device.depth_buffer.Data ? " (the screen's depth buffer)" : "", - current_program() ? current_program()->id : 0); - } - } if (targets_multiview) { multiview.drawn = TRUE; @@ -2066,6 +2049,11 @@ static GLuint vertex_shader_get(struct vertex_shader_object *program, BOOL immed immediate ? 0 : device.vertex_shader->packed_mask, multiview_view); program->shader[variant] = compile_shader(GL_VERTEX_SHADER, source, "vertex"); + if (!program->projection_known) + { + program->projected = strstr(source, "clip_captured = true") != NULL; + program->projection_known = TRUE; + } if (debug_settings.dump_shaders) { char path[512]; @@ -2447,7 +2435,21 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale { #ifdef HALO_ANDROID if (multiview.active && multiview.back_buffer && target == &multiview.back_buffer->target) + { + static unsigned long logged[16]; + unsigned long id = current_program() ? current_program()->id : 0; + int slot, known = FALSE; + + /* (diagnostics: each vertex program that reads the picture) */ + for (slot = 0; slot < 16 && logged[slot]; slot++) + known |= logged[slot] == id + 1; + if (!known && slot < 16) + { + logged[slot] = id + 1; + platform_log("multiview: vertex program %lu reads the picture", id); + } multiview_resolve(); + } #endif xgpu_texture_describe(texture->Format, texture->Size, &description); gl_texture = target->texture; @@ -2790,6 +2792,15 @@ static struct program_entry *prepare_draw(BOOL immediate) glUniform4fv(entry->mv_rows, 4, &multiview.rows[0][0]); entry->mv_serial = multiview.serial; } + if (targets_multiview && !program->multiview_checked) + { + /* (diagnostics: a vertex program whose position is not a projected + one, drawn depth tested into both eyes, is something the game + placed in screen space itself, which multiview puts at infinity) */ + program->multiview_checked = TRUE; + if (!program->projected && has_depth && D3D__RenderState[D3DRS_ZENABLE]) + platform_log("multiview: vertex program %lu draws in screen space, depth tested", program->id); + } if (targets_multiview && entry->mv_depth >= 0) { float depth[4]; From ee35524e6584465409026ce92b393ef9150eae50 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:40:58 +0200 Subject: [PATCH 09/10] VR: scripted cameras on the flat screen; hands held still under the system menu A scripted camera outside a cinematic (a level's intro) was drawn in stereo from the head, with the weapon at the controllers in it; it now goes to the screen like a cinematic. Under the system menu the headset reports no hands and the weapon fell back to the head; the hands last seen in focus stand in meanwhile, their buttons let go. Co-Authored-By: Claude Opus 5.5 --- port/linux/src/vr.c | 23 +++++++++++++++++++++++ source/render/render.c | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index 9c57f6e4..b1618b1b 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -42,6 +42,10 @@ static struct struct halo_xr_info info; struct halo_xr_frame frame; int frame_begun; + /* the hands as last seen with the app in focus (the system menu over + it reports none): held still meanwhile */ + struct halo_xr_hand focused_hands[HALO_XR_HAND_COUNT]; + int focused_hands_valid; int eyes_captured; /* bit per eye, this frame */ int overlay_mode; /* HALO_XR_OVERLAY_*, this frame */ int overlay_blend; @@ -991,6 +995,25 @@ void halo_vr_frame_begin(void) waited = now_us(); if (!host_xr_begin_frame(&vr.frame)) memset(&vr.frame, 0, sizeof(vr.frame)); + if (vr.frame.focused) + { + memcpy(vr.focused_hands, vr.frame.hands, sizeof(vr.focused_hands)); + vr.focused_hands_valid = 1; + } + else if (vr.focused_hands_valid && vr.frame.session_running) + { + /* the system menu is up: the hands stay where they were, their + buttons let go (the weapon does not fall back to the head) */ + int hand; + + memcpy(vr.frame.hands, vr.focused_hands, sizeof(vr.frame.hands)); + for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++) + { + vr.frame.hands[hand].trigger = vr.frame.hands[hand].squeeze = 0.0f; + vr.frame.hands[hand].thumbstick[0] = vr.frame.hands[hand].thumbstick[1] = 0.0f; + vr.frame.hands[hand].buttons = 0; + } + } vr.frame_start_us = now_us(); vr.wait_us += vr.frame_start_us - waited; vr.frame_begun = 1; diff --git a/source/render/render.c b/source/render/render.c index 5ceb73fd..7a78afd2 100644 --- a/source/render/render.c +++ b/source/render/render.c @@ -86,6 +86,7 @@ symbols in this file: #include "game/players.h" #include "game/player_control.h" #include "interface/hud.h" +#include "camera/director.h" #include "physics/collisions.h" #include "units/units.h" #include "halo_vr.h" @@ -938,7 +939,8 @@ void render_frame( } #ifdef HALO_ANDROID else if (window->local_player_index != NONE && screenshot_index == NULL && - halo_vr_stereo_player(window->local_player_index, cinematic_in_progress())) + halo_vr_stereo_player(window->local_player_index, cinematic_in_progress() || + director_get_perspective(window->local_player_index) == _director_perspective_scripted)) { render_vr_player_frame(window); continue; From 792531577c8cdeacd5c06bae7d500c77bf39c3e3 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:47:12 +0200 Subject: [PATCH 10/10] VR: no facing from the headset or controller during a cinematic Seated in a level's opening cinematic (a passenger), the right controller turned the game's camera the flat screen shows. The scripted-camera rule went back out: the main menu is a scripted camera and was immersive. Co-Authored-By: Claude Opus 5.5 --- source/game/player_control.c | 6 ++++-- source/render/render.c | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/game/player_control.c b/source/game/player_control.c index 523e9f0f..a5a7dca5 100644 --- a/source/game/player_control.c +++ b/source/game/player_control.c @@ -775,11 +775,13 @@ static void handle_one_player_input( #endif #ifdef HALO_ANDROID { - /* VR: the player faces where the head does (vr.c) */ + /* VR: the player faces where the head does (vr.c); not in a + cinematic, whose camera the game has (seated, the controller + would turn it on the screen) */ real vr_yaw; real vr_pitch; - if (halo_vr_aim( + if (!cinematic_in_progress() && halo_vr_aim( local_player_index, player->desired_angles.yaw, player->desired_angles.pitch, diff --git a/source/render/render.c b/source/render/render.c index 7a78afd2..5ceb73fd 100644 --- a/source/render/render.c +++ b/source/render/render.c @@ -86,7 +86,6 @@ symbols in this file: #include "game/players.h" #include "game/player_control.h" #include "interface/hud.h" -#include "camera/director.h" #include "physics/collisions.h" #include "units/units.h" #include "halo_vr.h" @@ -939,8 +938,7 @@ void render_frame( } #ifdef HALO_ANDROID else if (window->local_player_index != NONE && screenshot_index == NULL && - halo_vr_stereo_player(window->local_player_index, cinematic_in_progress() || - director_get_perspective(window->local_player_index) == _director_perspective_scripted)) + halo_vr_stereo_player(window->local_player_index, cinematic_in_progress())) { render_vr_player_frame(window); continue;