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..c021f1c4 100644 --- a/port/linux/include/halo_vr.h +++ b/port/linux/include/halo_vr.h @@ -50,6 +50,20 @@ 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); +/* 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 */ 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..78a448d7 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -175,8 +175,12 @@ 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]; + /* 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 */ @@ -224,6 +228,13 @@ 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; 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 */ @@ -370,6 +381,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 +868,162 @@ 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: the sun's +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 +{ + 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; + /* 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 +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; + multiview.drawn = FALSE; + if (!read_framebuffer) + glGenFramebuffers(1, &read_framebuffer); + glBindFramebuffer(GL_READ_FRAMEBUFFER, read_framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer_get(multiview.back_buffer->target.texture, 0)); + if (scissor) + glDisable(GL_SCISSOR_TEST); + 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); +} + +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); 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]) +{ + 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)); + memcpy(multiview.resolve_rect, resolve_rect, sizeof(multiview.resolve_rect)); + multiview.serial++; + multiview.back_buffer = back_buffer; + multiview.drawn = FALSE; + multiview.active = TRUE; + 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]) +{ + 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) +{ + 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 +1049,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 +1098,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 +1597,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 +1657,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,16 +2039,21 @@ 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 (!program->projection_known) + { + program->projected = strstr(source, "clip_captured = true") != NULL; + program->projection_known = TRUE; + } if (debug_settings.dump_shaders) { char path[512]; @@ -2018,6 +2207,9 @@ 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"); + 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]; @@ -2241,6 +2433,24 @@ 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) + { + 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; gl_target = GL_TEXTURE_2D; @@ -2563,7 +2773,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 +2787,30 @@ 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 (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]; + + 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/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..bee469c9 100644 --- a/port/linux/src/nv2a_vsh.c +++ b/port/linux/src/nv2a_vsh.c @@ -195,13 +195,30 @@ 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]; + 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" + "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; #endif xgpu_text_append(&text, "%s", shader_prologue); for (index = 0; index < XGPU_VERTEX_ATTRIBUTE_COUNT; index++) @@ -360,6 +377,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 +391,28 @@ 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 + 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 = 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" : + "" #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..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; @@ -91,6 +95,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 +208,11 @@ 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[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); @@ -423,6 +435,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"); @@ -982,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; @@ -1125,6 +1157,164 @@ 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; +/* 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) +{ + 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; + } + 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) */ + 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; + } + { + /* each eye's picture's place in the shared one (rows from the top) */ + float resolve_rect[2][4]; + + 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); + } +} + +/* 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) +{ + 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) +{ + 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 +2062,18 @@ 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_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/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/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 a314626d..5ceb73fd 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(); @@ -700,7 +706,36 @@ 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]; + { + /* 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); + 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];