/* * Copyright (C) 2023 Mikhail Burakov. This file is part of streamer. * * streamer is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * streamer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with streamer. If not, see . */ #include "gpu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // Must be included last #include #include "util.h" // TODO(mburakov): It should be theoretically possible to do everything in a // single pass using a compute shader and GLES3. Unfortunately my test machine // reports the primary framebuffer as multiplane. This is probably the reason // why texture created from it can not be sampled using imageLoad in a compute // shader even though it's still RGB. Fallback to GLES2 and per-plane textures // for now, and figure out details later. extern const char _binary_vertex_glsl_start[]; extern const char _binary_vertex_glsl_end[]; extern const char _binary_luma_glsl_start[]; extern const char _binary_luma_glsl_end[]; extern const char _binary_chroma_glsl_start[]; extern const char _binary_chroma_glsl_end[]; struct GpuContext { EGLDisplay display; EGLContext context; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES; GLuint program_luma; GLuint program_chroma; GLint chroma_offsets; GLuint framebuffer; GLuint vertices; }; struct GpuFrame { struct GpuContext* gpu_context; uint32_t width, height; int dmabuf_fds[4]; EGLImage images[2]; GLuint textures[2]; }; static const char* EglErrorString(EGLint error) { static const char* const egl_error_strings[] = { "EGL_SUCCESS", "EGL_NOT_INITIALIZED", "EGL_BAD_ACCESS", "EGL_BAD_ALLOC", "EGL_BAD_ATTRIBUTE", "EGL_BAD_CONFIG", "EGL_BAD_CONTEXT", "EGL_BAD_CURRENT_SURFACE", "EGL_BAD_DISPLAY", "EGL_BAD_MATCH", "EGL_BAD_NATIVE_PIXMAP", "EGL_BAD_NATIVE_WINDOW", "EGL_BAD_PARAMETER", "EGL_BAD_SURFACE", "EGL_CONTEXT_LOST", }; return EGL_SUCCESS <= error && error <= EGL_CONTEXT_LOST ? egl_error_strings[error - EGL_SUCCESS] : "???"; } static const char* GlErrorString(GLenum error) { static const char* const gl_error_strings[] = { "GL_INVALID_ENUM", "GL_INVALID_VALUE", "GL_INVALID_OPERATION", "GL_STACK_OVERFLOW", "GL_STACK_UNDERFLOW", "GL_OUT_OF_MEMORY", "GL_INVALID_FRAMEBUFFER_OPERATION", "GL_CONTEXT_LOST", }; if (error == GL_NO_ERROR) return "GL_NO_ERROR"; return GL_INVALID_ENUM <= error && error <= GL_CONTEXT_LOST ? gl_error_strings[error - GL_INVALID_ENUM] : "???"; } #define DEFINE_CHECK_BUILDABLE_FUNCTION(postfix, err_msg, getter_fn, \ status_enum, logger_fn) \ static bool CheckBuildable##postfix(GLuint buildable) { \ GLenum error = glGetError(); \ if (error != GL_NO_ERROR) { \ LOG(err_msg " (%s)", GlErrorString(error)); \ return false; \ } \ GLint status; \ getter_fn(buildable, status_enum, &status); \ if (status != GL_TRUE) { \ GLint log_length; \ getter_fn(buildable, GL_INFO_LOG_LENGTH, &log_length); \ char message[log_length]; \ memset(message, 0, sizeof(message)); \ logger_fn(buildable, log_length, NULL, message); \ LOG("%s", message); \ return false; \ } \ return true; \ } DEFINE_CHECK_BUILDABLE_FUNCTION(Shader, "Failed to compile shader", glGetShaderiv, GL_COMPILE_STATUS, glGetShaderInfoLog) DEFINE_CHECK_BUILDABLE_FUNCTION(Program, "Failed to link program", glGetProgramiv, GL_LINK_STATUS, glGetProgramInfoLog) static bool HasExtension(const char* haystack, const char* needle) { bool result = !!strstr(haystack, needle); if (!result) LOG("Unsupported extension %s", needle); return result; } static GLuint CreateGlProgram(const char* vs_begin, const char* vs_end, const char* fs_begin, const char* fs_end) { GLuint program = 0; GLuint vertex = glCreateShader(GL_VERTEX_SHADER); if (!vertex) { LOG("Failed to create vertex shader (%s)", GlErrorString(glGetError())); goto bail_out; } GLsizei size = (GLsizei)(vs_end - vs_begin); glShaderSource(vertex, 1, &vs_begin, &size); glCompileShader(vertex); if (!CheckBuildableShader(vertex)) goto delete_vs; GLuint fragment = glCreateShader(GL_FRAGMENT_SHADER); if (!fragment) { LOG("Failed to create fragment shader (%s)", GlErrorString(glGetError())); goto delete_vs; } size = (GLsizei)(fs_end - fs_begin); glShaderSource(fragment, 1, &fs_begin, &size); glCompileShader(fragment); if (!CheckBuildableShader(fragment)) goto delete_fs; program = glCreateProgram(); if (!program) { LOG("Failed to create shader program (%s)", GlErrorString(glGetError())); goto delete_fs; } glAttachShader(program, vertex); glAttachShader(program, fragment); glLinkProgram(program); if (!CheckBuildableProgram(program)) { glDeleteProgram(program); program = 0; goto delete_fs; } delete_fs: glDeleteShader(fragment); delete_vs: glDeleteShader(vertex); bail_out: return program; } static bool SetupImgInputUniform(GLuint program) { GLint img_input = glGetUniformLocation(program, "img_input"); if (img_input == -1) { LOG("Failed to find img_input uniform (%s)", GlErrorString(glGetError())); return false; } glUseProgram(program); glUniform1i(img_input, 0); GLenum error = glGetError(); if (error != GL_NO_ERROR) { LOG("Failed to set img_input uniform (%s)", GlErrorString(glGetError())); return false; } return true; } struct GpuContext* GpuContextCreate(void) { struct AUTO(GpuContext)* gpu_context = malloc(sizeof(struct GpuContext)); if (!gpu_context) { LOG("Failed to allocate gpu context (%s)", strerror(errno)); return NULL; } *gpu_context = (struct GpuContext){ .display = EGL_NO_DISPLAY, .context = EGL_NO_CONTEXT, .glEGLImageTargetTexture2DOES = NULL, .program_luma = 0, .program_chroma = 0, .chroma_offsets = -1, .framebuffer = 0, .vertices = 0, }; const char* egl_ext = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); if (!egl_ext) { LOG("Failed to query platformless egl extensions (%s)", EglErrorString(eglGetError())); return NULL; } LOG("EGL_EXTENSIONS: %s", egl_ext); if (!HasExtension(egl_ext, "EGL_MESA_platform_surfaceless")) return NULL; gpu_context->display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, NULL, NULL); if (gpu_context->display == EGL_NO_DISPLAY) { LOG("Failed to get egl display (%s)", EglErrorString(eglGetError())); return NULL; } EGLint major, minor; if (!eglInitialize(gpu_context->display, &major, &minor)) { LOG("Failed to initialize egl display (%s)", EglErrorString(eglGetError())); return NULL; } LOG("Initialized EGL %d.%d", major, minor); egl_ext = eglQueryString(gpu_context->display, EGL_EXTENSIONS); if (!egl_ext) { LOG("Failed to query egl extensions (%s)", EglErrorString(eglGetError())); return NULL; } LOG("EGL_EXTENSIONS: %s", egl_ext); if (!HasExtension(egl_ext, "EGL_KHR_surfaceless_context") || !HasExtension(egl_ext, "EGL_KHR_no_config_context") || !HasExtension(egl_ext, "EGL_EXT_image_dma_buf_import") || !HasExtension(egl_ext, "EGL_EXT_image_dma_buf_import_modifiers")) return NULL; if (!eglBindAPI(EGL_OPENGL_ES_API)) { LOG("Failed to bind egl api (%s)", EglErrorString(eglGetError())); return NULL; } static const EGLint context_attribs[] = { #define _(...) __VA_ARGS__ _(EGL_CONTEXT_MAJOR_VERSION, 3), _(EGL_CONTEXT_MINOR_VERSION, 1), EGL_NONE, #undef _ }; gpu_context->context = eglCreateContext( gpu_context->display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, context_attribs); if (gpu_context->context == EGL_NO_CONTEXT) { LOG("Failed to create egl context (%s)", EglErrorString(eglGetError())); return NULL; } if (!eglMakeCurrent(gpu_context->display, EGL_NO_SURFACE, EGL_NO_SURFACE, gpu_context->context)) { LOG("Failed to make egl context current (%s)", EglErrorString(eglGetError())); return NULL; } const char* gl_ext = (const char*)glGetString(GL_EXTENSIONS); if (!gl_ext) { LOG("Failed to get gl extensions (%s)", GlErrorString(glGetError())); return NULL; } LOG("GL_EXTENSIONS: %s", gl_ext); if (!HasExtension(gl_ext, "GL_OES_EGL_image")) return NULL; gpu_context->glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress( "glEGLImageTargetTexture2DOES"); if (!gpu_context->glEGLImageTargetTexture2DOES) { LOG("Failed to lookup glEGLImageTargetTexture2DOES function"); return NULL; } gpu_context->program_luma = CreateGlProgram(_binary_vertex_glsl_start, _binary_vertex_glsl_end, _binary_luma_glsl_start, _binary_luma_glsl_end); if (!gpu_context->program_luma || !SetupImgInputUniform(gpu_context->program_luma)) { LOG("Failed to create luma program"); return NULL; } gpu_context->program_chroma = CreateGlProgram(_binary_vertex_glsl_start, _binary_vertex_glsl_end, _binary_chroma_glsl_start, _binary_chroma_glsl_end); if (!gpu_context->program_chroma || !SetupImgInputUniform(gpu_context->program_luma)) { LOG("Failed to create chroma program"); return NULL; } gpu_context->chroma_offsets = glGetUniformLocation(gpu_context->program_chroma, "chroma_offsets"); if (gpu_context->chroma_offsets == -1) { LOG("Failed to find chroma_offsets uniform (%s)", GlErrorString(glGetError())); return NULL; } glGenFramebuffers(1, &gpu_context->framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, gpu_context->framebuffer); glGenBuffers(1, &gpu_context->vertices); glBindBuffer(GL_ARRAY_BUFFER, gpu_context->vertices); static const GLfloat vertices[] = {0, 0, 1, 0, 1, 1, 0, 1}; glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, NULL); glEnableVertexAttribArray(0); GLenum error = glGetError(); if (error != GL_NO_ERROR) { LOG("Failed to create gl objects (%s)", GlErrorString(glGetError())); return NULL; } return RELEASE(gpu_context); } bool GpuContextSync(struct GpuContext* gpu_context) { EGLSync sync = eglCreateSync(gpu_context->display, EGL_SYNC_FENCE, NULL); if (sync == EGL_NO_SYNC) { LOG("Failed to create egl fence sync (%s)", EglErrorString(eglGetError())); return false; } eglClientWaitSync(gpu_context->display, sync, 0, EGL_FOREVER); eglDestroySync(gpu_context->display, sync); return true; } void GpuContextDestroy(struct GpuContext** gpu_context) { if (!gpu_context || !*gpu_context) return; if ((*gpu_context)->vertices) glDeleteBuffers(1, &(*gpu_context)->vertices); if ((*gpu_context)->framebuffer) glDeleteFramebuffers(1, &(*gpu_context)->framebuffer); if ((*gpu_context)->program_chroma) glDeleteProgram((*gpu_context)->program_chroma); if ((*gpu_context)->program_luma) glDeleteProgram((*gpu_context)->program_luma); if ((*gpu_context)->context != EGL_NO_CONTEXT) { eglMakeCurrent((*gpu_context)->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext((*gpu_context)->display, (*gpu_context)->context); } if ((*gpu_context)->display != EGL_NO_DISPLAY) eglTerminate((*gpu_context)->display); free(*gpu_context); *gpu_context = NULL; } static EGLImage CreateEglImage(struct GpuContext* gpu_context, uint32_t width, uint32_t height, uint32_t fourcc, size_t nplanes, const struct GpuFramePlane* planes) { static const EGLAttrib attrib_keys[] = { EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE0_PITCH_EXT, EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT, EGL_DMA_BUF_PLANE1_PITCH_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE2_FD_EXT, EGL_DMA_BUF_PLANE2_OFFSET_EXT, EGL_DMA_BUF_PLANE2_PITCH_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE3_FD_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT, EGL_DMA_BUF_PLANE3_PITCH_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT, }; EGLAttrib attrib_list[7 + LENGTH(attrib_keys) * 2] = { #define _(...) __VA_ARGS__ _(EGL_WIDTH, width), _(EGL_HEIGHT, height), _(EGL_LINUX_DRM_FOURCC_EXT, fourcc), #undef _ }; EGLAttrib* pairs = &attrib_list[6]; const EGLAttrib* key = &attrib_keys[0]; for (size_t i = 0; i < nplanes; i++) { if (planes[i].dmabuf_fd == -1) break; *pairs++ = *key++; *pairs++ = planes[i].dmabuf_fd; *pairs++ = *key++; *pairs++ = planes[i].offset; *pairs++ = *key++; *pairs++ = planes[i].pitch; *pairs++ = *key++; *pairs++ = planes[i].modifier >> 32; *pairs++ = *key++; *pairs++ = planes[i].modifier & UINT32_MAX; } *pairs = EGL_NONE; EGLImage image = eglCreateImage(gpu_context->display, EGL_NO_CONFIG_KHR, EGL_LINUX_DMA_BUF_EXT, NULL, attrib_list); if (image == EGL_NO_IMAGE) LOG("Failed to create egl image (%s)", EglErrorString(eglGetError())); return image; } static GLuint CreateTexture(struct GpuContext* gpu_context, EGLImage image) { GLuint texture = 0; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); gpu_context->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); GLenum error = glGetError(); if (error != GL_NO_ERROR) { LOG("Failed to create texture (%s)", GlErrorString(error)); glDeleteTextures(1, &texture); return 0; } return texture; } struct GpuFrame* GpuFrameCreate(struct GpuContext* gpu_context, uint32_t width, uint32_t height, uint32_t fourcc, size_t nplanes, const struct GpuFramePlane* planes) { struct AUTO(GpuFrame)* gpu_frame = malloc(sizeof(struct GpuFrame)); if (!gpu_frame) { LOG("Failed to allocate gpu frame (%s)", strerror(errno)); return NULL; } *gpu_frame = (struct GpuFrame){ .gpu_context = gpu_context, .width = width, .height = height, .dmabuf_fds = {-1, -1, -1, -1}, .images = {EGL_NO_IMAGE, EGL_NO_IMAGE}, .textures = {0, 0}, }; for (size_t i = 0; i < nplanes; i++) { gpu_frame->dmabuf_fds[i] = dup(planes[i].dmabuf_fd); if (gpu_frame->dmabuf_fds[i] == -1) { LOG("Failed to dup dmabuf fd (%s)", strerror(errno)); return NULL; } } struct GpuFramePlane dummy_planes[nplanes]; for (size_t i = 0; i < nplanes; i++) { dummy_planes[i] = (struct GpuFramePlane){ .dmabuf_fd = gpu_frame->dmabuf_fds[i], .offset = planes[i].offset, .pitch = planes[i].pitch, .modifier = planes[i].modifier, }; } if (fourcc == DRM_FORMAT_NV12) { gpu_frame->images[0] = CreateEglImage(gpu_context, width, height, DRM_FORMAT_R8, 1, &dummy_planes[0]); if (gpu_frame->images[0] == EGL_NO_IMAGE) { LOG("Failed to create luma plane image"); return NULL; } gpu_frame->images[1] = CreateEglImage(gpu_context, width / 2, height / 2, DRM_FORMAT_GR88, 1, &dummy_planes[1]); if (gpu_frame->images[1] == EGL_NO_IMAGE) { LOG("Failed to create chroma plane image"); return NULL; } } else { gpu_frame->images[0] = CreateEglImage(gpu_context, width, height, fourcc, nplanes, dummy_planes); if (gpu_frame->images[0] == EGL_NO_IMAGE) { LOG("Failed to create multiplanar image"); return NULL; } } for (size_t i = 0; i < LENGTH(gpu_frame->images); i++) { if (gpu_frame->images[i] == EGL_NO_IMAGE) break; gpu_frame->textures[i] = CreateTexture(gpu_context, gpu_frame->images[i]); if (!gpu_frame->textures[i]) { LOG("Failed to create texture"); return NULL; } } return RELEASE(gpu_frame); } void GpuFrameGetSize(const struct GpuFrame* gpu_frame, uint32_t* width, uint32_t* height) { *width = gpu_frame->width; *height = gpu_frame->height; } static bool GpuFrameConvertImpl(GLuint from, GLuint to) { glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, to, 0); GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER); if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) { LOG("Framebuffer is incomplete (0x%x)", framebuffer_status); return false; } glBindTexture(GL_TEXTURE_2D, from); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); GLenum error = glGetError(); if (error != GL_NO_ERROR) { LOG("Failed to convert plane (%s)", GlErrorString(error)); return false; } return true; } bool GpuFrameConvert(const struct GpuFrame* from, const struct GpuFrame* to) { glUseProgram(from->gpu_context->program_luma); glViewport(0, 0, (GLsizei)to->width, (GLsizei)to->height); if (!GpuFrameConvertImpl(from->textures[0], to->textures[0])) { LOG("Failed to convert luma plane"); return false; } glUseProgram(from->gpu_context->program_chroma); glUniform2f(from->gpu_context->chroma_offsets, 1.f / (GLfloat)from->width, 1.f / (GLfloat)from->height); glViewport(0, 0, (GLsizei)to->width / 2, (GLsizei)to->height / 2); if (!GpuFrameConvertImpl(from->textures[0], to->textures[1])) { LOG("Failed to convert chroma plane"); return false; } return true; } void GpuFrameDestroy(struct GpuFrame** gpu_frame) { if (!gpu_frame || !*gpu_frame) return; for (size_t i = LENGTH((*gpu_frame)->textures); i; i--) { if ((*gpu_frame)->textures[i - 1]) glDeleteTextures(1, &(*gpu_frame)->textures[i - 1]); } for (size_t i = LENGTH((*gpu_frame)->images); i; i--) { if ((*gpu_frame)->images[i - 1] != EGL_NO_IMAGE) eglDestroyImage((*gpu_frame)->gpu_context->display, (*gpu_frame)->images[i - 1]); } for (size_t i = LENGTH((*gpu_frame)->dmabuf_fds); i; i--) { if ((*gpu_frame)->dmabuf_fds[i - 1] != -1) close((*gpu_frame)->dmabuf_fds[i - 1]); } free(*gpu_frame); *gpu_frame = NULL; }