Files
mitch030504--AstroQuest-Frame/quest-host/cpp/gl_frames.cpp
T
bigmak94andClaude Opus 5.5 d96618af1d Quest app 0.14 to 0.17: a field-of-view menu at the start, counted from the headset's own view
Work of 2026-10-04, committed as it was (README-QUEST-VR.md tells each step):

- 0.14: every start shows a panel in the headset to choose how much of the field of view
  the game draws (left and right on the D-pad or the left stick, X to play). Less is the
  same pixels over fewer degrees, a sharper picture; the choice is remembered. Below all of
  it, each eye gets a soft-edged squircle border, drawn where the host copies the frame.
- 0.15: the percent is of the headset's own view, read from OpenXR before Play is offered
  and handed to the emulator before the game starts (SHADPS4_VR_HEADSET_FOV_TAN), 50 to 100.
- 0.16: the border sits around each eye's straight ahead, which a headset's uneven view
  does not have in the middle of the picture.
- 0.17: both eyes draw the same, even view around straight ahead that takes in the
  headset's own (SHADPS4_VR_FOV_SYMMETRIC), so that what one eye shows the other shows too
  when the view is narrowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:15:43 +02:00

283 lines
11 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
#include "gl_frames.h"
#include <cstring>
#include <android/hardware_buffer.h>
#include <dlfcn.h>
#include "core_process.h"
#include "log.h"
namespace {
/// How gralloc describes a buffer; the first file descriptor is the dma-buf with the pixels.
struct NativeHandle {
int version;
int num_fds;
int num_ints;
int data[1];
};
GLuint CompileShader(GLenum type, const char* source) {
const GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (ok != GL_TRUE) {
char log[512];
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
LOGE("shader compilation failed: %s", log);
}
return shader;
}
const char* const BlitVertexShader = R"(#version 300 es
out vec2 uv;
void main() {
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
// The frame's first row is its top row, GL's first row is the bottom one.
uv = vec2(corner.x, 1.0 - corner.y);
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
)";
const char* const BlitFragmentShader = R"(#version 300 es
precision highp float;
uniform sampler2D frame;
uniform int swap_red_blue;
uniform int decode_srgb;
uniform int reduced_fov;
uniform vec4 fov_tan;
in vec2 uv;
out vec4 color;
void main() {
vec3 c = texture(frame, uv).rgb;
if (swap_red_blue != 0) {
c = c.bgr;
}
if (reduced_fov != 0) {
// Each eye's aperture is centred on straight ahead, not on the texture midpoint.
// With asymmetric headset optics those midpoints point in opposite directions.
vec2 eye_uv = vec2(fract(uv.x * 2.0), uv.y);
vec2 axis = vec2((uv.x < 0.5 ? fov_tan.x : fov_tan.y) /
(fov_tan.x + fov_tan.y),
fov_tan.z / (fov_tan.z + fov_tan.w));
vec2 radius = mix(axis, vec2(1.0) - axis, step(axis, eye_uv));
// The pillowed rectangle reaches each frustum edge, with its curved corners around
// the optical axis. Only visibility changes; the scene and lens warp are untouched.
vec2 q = abs((eye_uv - axis) / radius);
vec2 q2 = q * q;
float edge = dot(q2, q2);
float feather = max(0.10, 1.5 * fwidth(edge));
// Finish the fade just inside the image, so filtering never leaks a colored seam
// from the other eye or leaves a hard line at the projection's rectangular bounds.
c *= 1.0 - smoothstep(0.98 - feather, 0.98, edge);
}
// The emulator delivers display-ready values. An sRGB target encodes whatever is written to
// it, so those have to be made linear first.
if (decode_srgb != 0) {
c = mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));
}
color = vec4(c, 1.0);
}
)";
} // namespace
bool GlContext::Create() {
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY || !eglInitialize(display, nullptr, nullptr)) {
LOGE("eglInitialize failed");
return false;
}
const EGLint config_attributes[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 0, EGL_NONE,
};
EGLint num_configs = 0;
if (!eglChooseConfig(display, config_attributes, &config, 1, &num_configs) ||
num_configs < 1) {
LOGE("eglChooseConfig failed");
return false;
}
const EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attributes);
// Nothing is drawn to this surface, the context just needs one to be current.
const EGLint surface_attributes[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
surface = eglCreatePbufferSurface(display, config, surface_attributes);
if (context == EGL_NO_CONTEXT || surface == EGL_NO_SURFACE ||
!eglMakeCurrent(display, surface, surface, context)) {
LOGE("unable to create the GL context: 0x%x", eglGetError());
return false;
}
return true;
}
void GlContext::Destroy() {
if (display == EGL_NO_DISPLAY) {
return;
}
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (surface != EGL_NO_SURFACE) {
eglDestroySurface(display, surface);
}
if (context != EGL_NO_CONTEXT) {
eglDestroyContext(display, context);
}
eglTerminate(display);
display = EGL_NO_DISPLAY;
surface = EGL_NO_SURFACE;
context = EGL_NO_CONTEXT;
}
bool FrameBuffers::Create(EGLDisplay display, uint32_t width, uint32_t height,
CoreProcess& core) {
using GetNativeHandle = const NativeHandle* (*)(const AHardwareBuffer*);
const auto get_native_handle = reinterpret_cast<GetNativeHandle>(
dlsym(RTLD_DEFAULT, "AHardwareBuffer_getNativeHandle"));
if (get_native_handle == nullptr) {
LOGE("AHardwareBuffer_getNativeHandle is not available");
return false;
}
AHardwareBuffer_Desc description{};
description.width = width;
description.height = height;
description.layers = 1;
description.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
// The CPU bits keep the buffer in a plain row-by-row layout, which is what the emulator's
// GPU driver can attach to and what its fallback path writes.
description.usage =
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER |
AHARDWAREBUFFER_USAGE_CPU_READ_RARELY | AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY;
std::vector<int> fds;
uint32_t stride = 0;
for (uint32_t i = 0; i < Count; ++i) {
AHardwareBuffer* buffer = nullptr;
if (AHardwareBuffer_allocate(&description, &buffer) != 0) {
LOGE("unable to allocate frame buffer %u", i);
return false;
}
buffers.push_back(buffer);
AHardwareBuffer_Desc actual{};
AHardwareBuffer_describe(buffer, &actual);
stride = actual.stride;
const NativeHandle* handle = get_native_handle(buffer);
if (handle == nullptr || handle->num_fds < 1) {
LOGE("frame buffer %u has no file descriptor", i);
return false;
}
fds.push_back(handle->data[0]);
const EGLClientBuffer client_buffer = eglGetNativeClientBufferANDROID(buffer);
const EGLint image_attributes[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
const EGLImageKHR image = eglCreateImageKHR(display, EGL_NO_CONTEXT,
EGL_NATIVE_BUFFER_ANDROID, client_buffer,
image_attributes);
if (image == EGL_NO_IMAGE_KHR) {
LOGE("unable to wrap frame buffer %u in an EGL image: 0x%x", i, eglGetError());
return false;
}
images.push_back(image);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
textures.push_back(texture);
}
LOGI("frame buffers: %u x %ux%u, stride %u", Count, width, height, stride);
core.SetBuffers(fds, width, height, stride);
return true;
}
void FrameBuffers::Destroy(EGLDisplay display) {
for (const GLuint texture : textures) {
glDeleteTextures(1, &texture);
}
for (const EGLImageKHR image : images) {
eglDestroyImageKHR(display, image);
}
for (AHardwareBuffer* buffer : buffers) {
AHardwareBuffer_release(buffer);
}
textures.clear();
images.clear();
buffers.clear();
}
bool FrameBlitter::Create() {
const GLuint vertex = CompileShader(GL_VERTEX_SHADER, BlitVertexShader);
const GLuint fragment = CompileShader(GL_FRAGMENT_SHADER, BlitFragmentShader);
program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
glDeleteShader(vertex);
glDeleteShader(fragment);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
LOGE("unable to link the blit program");
return false;
}
uniform_swap = glGetUniformLocation(program, "swap_red_blue");
uniform_decode = glGetUniformLocation(program, "decode_srgb");
uniform_mask = glGetUniformLocation(program, "reduced_fov");
uniform_fov = glGetUniformLocation(program, "fov_tan");
glGenVertexArrays(1, &vertex_array);
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
write_control =
extensions != nullptr && std::strstr(extensions, "GL_EXT_sRGB_write_control") != nullptr;
return true;
}
void FrameBlitter::Destroy() {
if (program != 0) {
glDeleteProgram(program);
program = 0;
}
if (vertex_array != 0) {
glDeleteVertexArrays(1, &vertex_array);
vertex_array = 0;
}
}
void FrameBlitter::Draw(GLuint texture, uint32_t width, uint32_t height, bool swap_red_blue,
bool srgb_target, bool reduced_fov, const float* fov) const {
glViewport(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height));
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glDisable(GL_SCISSOR_TEST);
const bool unencoded = srgb_target && write_control;
if (unencoded) {
glDisable(GL_FRAMEBUFFER_SRGB_EXT);
}
glUseProgram(program);
glUniform1i(uniform_swap, swap_red_blue ? 1 : 0);
glUniform1i(uniform_decode, srgb_target && !write_control ? 1 : 0);
glUniform1i(uniform_mask, reduced_fov ? 1 : 0);
glUniform4f(uniform_fov, fov != nullptr ? fov[0] : 1.0f,
fov != nullptr ? fov[1] : 1.0f, fov != nullptr ? fov[2] : 1.0f,
fov != nullptr ? fov[3] : 1.0f);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(vertex_array);
glDrawArrays(GL_TRIANGLES, 0, 3);
if (unencoded) {
// As it is by default: everything else drawn to such a target counts on it.
glEnable(GL_FRAMEBUFFER_SRGB_EXT);
}
}