ref:main
/**
* @file src/platform/linux/graphics.cpp
* @brief Definitions for graphics related functions.
*/
// standard includes
#include <fcntl.h>
// local includes
#include "graphics.h"
#include "src/file_handler.h"
#include "src/logging.h"
#include "src/video.h"
// platform includes
#if !defined(__FreeBSD__)
#include <sys/capability.h>
#endif
extern "C" {
#include <libavutil/pixdesc.h>
}
// I want to have as little build dependencies as possible
// There aren't that many DRM_FORMAT I need to use, so define them here
//
// They aren't likely to change any time soon.
/**
* @def fourcc_code(a, b, c, d)
* @brief Macro for fourcc code.
*/
#define fourcc_code(a, b, c, d) ((std::uint32_t) (a) | ((std::uint32_t) (b) << 8) | ((std::uint32_t) (c) << 16) | ((std::uint32_t) (d) << 24))
/**
* @def fourcc_mod_code(vendor, val)
* @brief Macro for fourcc mod code.
*/
#define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) & 0x00ffffffffffffffULL))
/**
* @def DRM_FORMAT_MOD_INVALID
* @brief Macro for DRM FORMAT MOD INVALID.
*/
#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1))
#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install
/**
* @def SUNSHINE_SHADERS_DIR
* @brief Macro for SUNSHINE SHADERS DIR.
*/
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl"
#endif
using namespace std::literals;
namespace gl {
GladGLContext ctx; ///< Loaded OpenGL function table for the active context.
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC egl_image_target_texture_2d_fn = nullptr;
/**
* @brief Bind an EGL image to the current OpenGL texture target.
*/
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC egl_image_target_texture_2d() {
return egl_image_target_texture_2d_fn;
}
/**
* @brief Drain and log pending OpenGL errors.
*/
void drain_errors(const std::string_view &prefix) {
GLenum err;
while ((err = ctx.GetError()) != GL_NO_ERROR) {
BOOST_LOG(error) << "GL: "sv << prefix << ": ["sv << util::hex(err).to_string_view() << ']';
}
}
tex_t::~tex_t() {
if (size() != 0) {
ctx.DeleteTextures(size(), begin());
}
}
tex_t tex_t::make(std::size_t count) {
tex_t textures {count};
ctx.GenTextures(textures.size(), textures.begin());
float color[] = {0.0f, 0.0f, 0.0f, 1.0f};
for (auto tex : textures) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex);
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // x
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // y
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
gl::ctx.TexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color);
}
return textures;
}
frame_buf_t::~frame_buf_t() {
if (begin()) {
ctx.DeleteFramebuffers(size(), begin());
}
}
frame_buf_t frame_buf_t::make(std::size_t count) {
frame_buf_t frame_buf {count};
ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin());
return frame_buf;
}
void frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]);
gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id);
gl::ctx.BindTexture(GL_TEXTURE_2D, texture);
gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height);
}
std::string shader_t::err_str() {
int length;
ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length);
std::string string;
string.resize(length);
ctx.GetShaderInfoLog(handle(), length, &length, string.data());
string.resize(length - 1);
return string;
}
util::Either<shader_t, std::string> shader_t::compile(const std::string_view &source, GLenum type) {
shader_t shader;
auto data = source.data();
GLint length = source.length();
shader._shader.el = ctx.CreateShader(type);
ctx.ShaderSource(shader.handle(), 1, &data, &length);
ctx.CompileShader(shader.handle());
int status = 0;
ctx.GetShaderiv(shader.handle(), GL_COMPILE_STATUS, &status);
if (!status) {
return shader.err_str();
}
return shader;
}
GLuint shader_t::handle() const {
return _shader.el;
}
buffer_t buffer_t::make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data) {
buffer_t buffer;
buffer._block = block;
buffer._size = data.size();
buffer._offsets = std::move(offsets);
ctx.GenBuffers(1, &buffer._buffer.el);
ctx.BindBuffer(GL_UNIFORM_BUFFER, buffer.handle());
ctx.BufferData(GL_UNIFORM_BUFFER, data.size(), (const std::uint8_t *) data.data(), GL_DYNAMIC_DRAW);
return buffer;
}
GLuint buffer_t::handle() const {
return _buffer.el;
}
const char *buffer_t::block() const {
return _block;
}
void buffer_t::update(const std::string_view &view, std::size_t offset) {
ctx.BindBuffer(GL_UNIFORM_BUFFER, handle());
ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *) view.data());
}
void buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) {
util::buffer_t<std::uint8_t> buffer {_size};
for (int x = 0; x < count; ++x) {
auto val = members[x];
std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[_offsets[x]]);
}
update(util::view(buffer.begin(), buffer.end()), offset);
}
std::string program_t::err_str() {
int length;
ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length);
std::string string;
string.resize(length);
ctx.GetShaderInfoLog(handle(), length, &length, string.data());
string.resize(length - 1);
return string;
}
util::Either<program_t, std::string> program_t::link(const shader_t &vert, const shader_t &frag) {
program_t program;
program._program.el = ctx.CreateProgram();
ctx.AttachShader(program.handle(), vert.handle());
ctx.AttachShader(program.handle(), frag.handle());
// p_handle stores a copy of the program handle, since program will be moved before
// the fail guard function is called.
auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() {
ctx.DetachShader(p_handle, vert.handle());
ctx.DetachShader(p_handle, frag.handle());
});
ctx.LinkProgram(program.handle());
int status = 0;
ctx.GetProgramiv(program.handle(), GL_LINK_STATUS, &status);
if (!status) {
return program.err_str();
}
return program;
}
void program_t::bind(const buffer_t &buffer) {
ctx.UseProgram(handle());
auto i = ctx.GetUniformBlockIndex(handle(), buffer.block());
ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle());
}
std::optional<buffer_t> program_t::uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count) {
auto i = ctx.GetUniformBlockIndex(handle(), block);
if (i == GL_INVALID_INDEX) {
BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']';
return std::nullopt;
}
int size;
ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size);
bool error_flag = false;
util::buffer_t<GLint> offsets {count};
auto indices = (std::uint32_t *) alloca(count * sizeof(std::uint32_t));
auto names = (const char **) alloca(count * sizeof(const char *));
auto names_p = names;
std::for_each_n(members, count, [names_p](auto &member) mutable {
*names_p++ = std::get<0>(member);
});
std::fill_n(indices, count, GL_INVALID_INDEX);
ctx.GetUniformIndices(handle(), count, names, indices);
for (int x = 0; x < count; ++x) {
if (indices[x] == GL_INVALID_INDEX) {
error_flag = true;
BOOST_LOG(error) << "Couldn't find ["sv << block << '.' << members[x].first << ']';
}
}
if (error_flag) {
return std::nullopt;
}
ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin());
util::buffer_t<std::uint8_t> buffer {(std::size_t) size};
for (int x = 0; x < count; ++x) {
auto val = std::get<1>(members[x]);
std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[offsets[x]]);
}
return buffer_t::make(std::move(offsets), block, std::string_view {(char *) buffer.begin(), buffer.size()});
}
GLuint program_t::handle() const {
return _program.el;
}
} // namespace gl
namespace gbm {
device_destroy_fn device_destroy; ///< Device destroy.
create_device_fn create_device; ///< Create device.
/**
* @brief Load GBM symbols required for EGL device creation.
*/
int init() {
static void *handle {nullptr};
static bool funcs_loaded = false;
if (funcs_loaded) {
return 0;
}
if (!handle) {
handle = dyn::handle({"libgbm.so.1", "libgbm.so"});
if (!handle) {
return -1;
}
}
std::vector<std::tuple<GLADapiproc *, const char *>> funcs {
{(GLADapiproc *) &device_destroy, "gbm_device_destroy"},
{(GLADapiproc *) &create_device, "gbm_create_device"},
};
if (dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
} // namespace gbm
namespace egl {
/**
* @brief Log EGL failure details and return an error code.
*/
bool fail() {
return eglGetError() != EGL_SUCCESS;
}
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display) {
int egl_platform;
void *native_display_p;
switch (native_display.index()) {
case 0:
egl_platform = EGL_PLATFORM_GBM_MESA;
native_display_p = std::get<0>(native_display);
break;
case 1:
egl_platform = EGL_PLATFORM_WAYLAND_KHR;
native_display_p = std::get<1>(native_display);
break;
case 2:
egl_platform = EGL_PLATFORM_X11_KHR;
native_display_p = std::get<2>(native_display);
break;
default:
BOOST_LOG(error) << "egl::make_display(): Index ["sv << native_display.index() << "] not implemented"sv;
return nullptr;
}
// native_display.left() equals native_display.right()
EGLDisplay raw_display = EGL_NO_DISPLAY;
if (eglGetPlatformDisplayEXT) {
raw_display = eglGetPlatformDisplayEXT(egl_platform, native_display_p, nullptr);
} else if (eglGetPlatformDisplay) {
raw_display = eglGetPlatformDisplay(egl_platform, native_display_p, nullptr);
}
if (raw_display == EGL_NO_DISPLAY) {
BOOST_LOG(error) << "Couldn't open EGL display: ["sv
<< util::hex(eglGetError()).to_string_view() << ']';
return nullptr;
}
display_t display {raw_display};
int major;
int minor;
if (!eglInitialize(display.get(), &major, &minor)) {
BOOST_LOG(error) << "Couldn't initialize EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return nullptr;
}
if (!gladLoaderLoadEGL(display.get())) {
BOOST_LOG(error) << "Failed to reload EGL for initialized display"sv;
return nullptr;
}
const char *extension_st = eglQueryString(display.get(), EGL_EXTENSIONS);
const char *version = eglQueryString(display.get(), EGL_VERSION);
const char *vendor = eglQueryString(display.get(), EGL_VENDOR);
const char *apis = eglQueryString(display.get(), EGL_CLIENT_APIS);
BOOST_LOG(debug) << "EGL: ["sv << vendor << "]: version ["sv << version << ']';
BOOST_LOG(debug) << "API's supported: ["sv << apis << ']';
const char *extensions[] {
"EGL_KHR_create_context",
"EGL_KHR_surfaceless_context",
"EGL_EXT_image_dma_buf_import",
"EGL_EXT_image_dma_buf_import_modifiers",
};
for (auto ext : extensions) {
if (!std::strstr(extension_st, ext)) {
BOOST_LOG(error) << "Missing extension: ["sv << ext << ']';
return nullptr;
}
}
return display;
}
/**
* @brief Create an EGL/OpenGL context for capture or conversion.
*/
std::optional<ctx_t> make_ctx(display_t::pointer display) {
bool nice_warning = false;
#if !defined(__FreeBSD__)
cap_t caps = cap_get_proc();
cap_value_t sys_nice = CAP_SYS_NICE;
if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_nice, CAP_SET) || cap_set_proc(caps)) {
BOOST_LOG(debug) << "Failed to gain CAP_SYS_NICE"sv;
nice_warning = true;
}
cap_free(caps);
#endif
constexpr int conf_attr[] {
EGL_RENDERABLE_TYPE,
EGL_OPENGL_BIT,
EGL_NONE
};
int count;
EGLConfig conf;
if (!eglChooseConfig(display, conf_attr, &conf, 1, &count)) {
BOOST_LOG(error) << "Couldn't set config attributes: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return std::nullopt;
}
if (!eglBindAPI(EGL_OPENGL_API)) {
BOOST_LOG(error) << "Couldn't bind API: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return std::nullopt;
}
const char *extension_st = eglQueryString(display, EGL_EXTENSIONS);
std::vector<EGLint> attr;
attr.push_back(EGL_CONTEXT_CLIENT_VERSION);
attr.push_back(3);
// Only add the high priority attribute if the driver explicitly supports it
if (extension_st && std::string_view(extension_st).contains("EGL_IMG_context_priority"sv)) {
BOOST_LOG(debug) << "EGL: High priority context supported"sv;
attr.push_back(EGL_CONTEXT_PRIORITY_LEVEL_IMG);
attr.push_back(EGL_CONTEXT_PRIORITY_HIGH_IMG);
}
attr.push_back(EGL_NONE);
EGLContext raw_ctx = eglCreateContext(display, conf, EGL_NO_CONTEXT, attr.data());
if (raw_ctx == EGL_NO_CONTEXT) {
BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return std::nullopt;
}
ctx_t ctx {display, raw_ctx};
EGLint actual_priority = EGL_CONTEXT_PRIORITY_MEDIUM_IMG;
std::string actual_priority_str = "MEDIUM";
if (eglQueryContext(display, raw_ctx, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &actual_priority)) {
if (actual_priority == EGL_CONTEXT_PRIORITY_HIGH_IMG) {
actual_priority_str = "HIGH";
}
if (nice_warning) {
BOOST_LOG(warning) << "EGL: context priority set to "sv << actual_priority_str << " but CAP_SYS_NICE capability is missing"sv;
} else {
BOOST_LOG(info) << "EGL: context priority set to "sv << actual_priority_str;
}
}
if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, raw_ctx)) {
BOOST_LOG(error) << "Couldn't make current display"sv;
return std::nullopt;
}
if (!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) {
BOOST_LOG(error) << "Couldn't load OpenGL library"sv;
return std::nullopt;
}
gl::egl_image_target_texture_2d_fn =
(gl::PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) (GLADapiproc) eglGetProcAddress("glEGLImageTargetTexture2DOES");
if (!gl::egl_image_target_texture_2d_fn) {
BOOST_LOG(warning) << "GL: glEGLImageTargetTexture2DOES not available; DMA-BUF import will fail"sv;
}
// GetString returns const GLubyte* (unsigned char*); convert to std::string safely (avoids sonar cpp:S6996).
auto gl_string = [](const GLubyte *s) {
std::string result;
while (s && *s) {
result += static_cast<char>(*s++);
}
return result;
};
const auto gl_vendor = gl_string(gl::ctx.GetString(GL_VENDOR));
const auto gl_renderer = gl_string(gl::ctx.GetString(GL_RENDERER));
const auto gl_version = gl_string(gl::ctx.GetString(GL_VERSION));
const auto gl_shader = gl_string(gl::ctx.GetString(GL_SHADING_LANGUAGE_VERSION));
BOOST_LOG(debug) << "GL: vendor: "sv << gl_vendor;
BOOST_LOG(debug) << "GL: renderer: "sv << gl_renderer;
BOOST_LOG(debug) << "GL: version: "sv << gl_version;
BOOST_LOG(debug) << "GL: shader: "sv << gl_shader;
gl::ctx.PixelStorei(GL_UNPACK_ALIGNMENT, 1);
#if !defined(__FreeBSD__)
caps = cap_get_proc();
if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_nice, CAP_CLEAR) || cap_set_proc(caps)) {
BOOST_LOG(debug) << "Failed to drop CAP_SYS_NICE"sv;
}
cap_free(caps);
#endif
return ctx;
}
/**
* @brief EGL attribute pair describing one DMA-BUF plane.
*/
struct plane_attr_t {
EGLAttrib fd; ///< EGL attribute key for a plane file descriptor.
EGLAttrib offset; ///< Offset.
EGLAttrib pitch; ///< Pitch.
EGLAttrib lo; ///< Lo.
EGLAttrib hi; ///< Hi.
};
/**
* @brief Build EGL attributes for one DMA-BUF plane.
*
* @param plane_indice Zero-based plane index in the DMA-BUF descriptor.
* @return EGL attribute keys for that plane's file descriptor, offset, pitch, and modifier.
*/
inline plane_attr_t get_plane(std::uint32_t plane_indice) {
switch (plane_indice) {
case 0:
return {
EGL_DMA_BUF_PLANE0_FD_EXT,
EGL_DMA_BUF_PLANE0_OFFSET_EXT,
EGL_DMA_BUF_PLANE0_PITCH_EXT,
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
};
case 1:
return {
EGL_DMA_BUF_PLANE1_FD_EXT,
EGL_DMA_BUF_PLANE1_OFFSET_EXT,
EGL_DMA_BUF_PLANE1_PITCH_EXT,
EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT,
};
case 2:
return {
EGL_DMA_BUF_PLANE2_FD_EXT,
EGL_DMA_BUF_PLANE2_OFFSET_EXT,
EGL_DMA_BUF_PLANE2_PITCH_EXT,
EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT,
};
case 3:
return {
EGL_DMA_BUF_PLANE3_FD_EXT,
EGL_DMA_BUF_PLANE3_OFFSET_EXT,
EGL_DMA_BUF_PLANE3_PITCH_EXT,
EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT,
};
}
// Avoid warning
return {};
}
/**
* @brief Get EGL attributes for eglCreateImage() to import the provided surface.
* @param surface The surface descriptor.
* @return Vector of EGL attributes.
*/
std::vector<EGLAttrib> surface_descriptor_to_egl_attribs(const surface_descriptor_t &surface) {
std::vector<EGLAttrib> attribs;
attribs.emplace_back(EGL_WIDTH);
attribs.emplace_back(surface.width);
attribs.emplace_back(EGL_HEIGHT);
attribs.emplace_back(surface.height);
attribs.emplace_back(EGL_LINUX_DRM_FOURCC_EXT);
attribs.emplace_back(surface.fourcc);
for (auto x = 0; x < 4; ++x) {
auto fd = surface.fds[x];
if (fd < 0) {
continue;
}
auto plane_attr = get_plane(x);
attribs.emplace_back(plane_attr.fd);
attribs.emplace_back(fd);
attribs.emplace_back(plane_attr.offset);
attribs.emplace_back(surface.offsets[x]);
attribs.emplace_back(plane_attr.pitch);
attribs.emplace_back(surface.pitches[x]);
if (surface.modifier != DRM_FORMAT_MOD_INVALID) {
attribs.emplace_back(plane_attr.lo);
attribs.emplace_back(surface.modifier & 0xFFFFFFFF);
attribs.emplace_back(plane_attr.hi);
attribs.emplace_back(surface.modifier >> 32);
}
}
attribs.emplace_back(EGL_NONE);
return attribs;
}
/**
* @brief Import the source frame texture for EGL/OpenGL conversion.
*
* @param egl_display EGL display used to create the image.
* @param xrgb XRGB surface descriptor to import.
* @return Imported RGB image, or empty when import fails.
*/
std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
auto attribs = surface_descriptor_to_egl_attribs(xrgb);
rgb_t rgb {
egl_display,
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()),
gl::tex_t::make(1)
};
if (!rgb->xrgb8) {
BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view();
return std::nullopt;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
if (!gl::egl_image_target_texture_2d()) {
BOOST_LOG(error) << "glEGLImageTargetTexture2DOES is not available; cannot import RGB DMA-BUF"sv;
return std::nullopt;
}
gl::egl_image_target_texture_2d()(GL_TEXTURE_2D, rgb->xrgb8);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
gl_drain_errors;
return rgb;
}
/**
* @brief Create a black RGB texture of the specified image size.
* @param img The image to use for texture sizing.
* @return The new RGB texture.
*/
rgb_t create_blank(platf::img_t &img) {
rgb_t rgb {
EGL_NO_DISPLAY,
EGL_NO_IMAGE,
gl::tex_t::make(1)
};
gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, img.width, img.height);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
auto framebuf = gl::frame_buf_t::make(1);
framebuf.bind(&rgb->tex[0], &rgb->tex[0] + 1);
GLenum attachment = GL_COLOR_ATTACHMENT0;
gl::ctx.DrawBuffers(1, &attachment);
const GLuint rgb_black[] = {0, 0, 0, 0};
gl::ctx.ClearBufferuiv(GL_COLOR, 0, rgb_black);
gl_drain_errors;
return rgb;
}
// Constants for clear black color Y, U, V. U & V are same so:
const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; ///< Y black.
const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; ///< Uv black.
/**
* @brief Bind NV12 target framebuffers to their Y and UV plane textures.
*
* @param nv12 Imported NV12 target whose textures receive rendered output.
*/
void nv12_bind_framebuffers(nv12_t &nv12) {
constexpr std::array<GLenum, 2> attachments {{GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}};
for (size_t x = 0; x < attachments.size(); ++x) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]);
gl::ctx.DrawBuffers(1, &attachments[x]);
gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black);
}
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);
gl_drain_errors;
}
/**
* @brief Bind YUV444 target framebuffers to their Y, U, and V plane textures.
*
* @param yuv444 Imported YUV444 target whose textures receive rendered output.
*/
void yuv44_bind_framebuffers(yuv444_t &yuv444) {
constexpr std::array<GLenum, 3> attachments {{GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2}};
for (size_t x = 0; x < attachments.size(); ++x) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, yuv444->buf[x]);
gl::ctx.DrawBuffers(1, &attachments[x]);
gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black);
}
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);
gl_drain_errors;
}
/**
* @brief Import the output frame target for EGL/OpenGL conversion.
*
* @param egl_display EGL display used to create the images.
* @param fds File descriptors backing the NV12 image.
* @param y Luma plane descriptor.
* @param uv Chroma plane descriptor.
* @return Imported NV12 image, or empty when import fails.
*/
std::optional<nv12_t> import_target(display_t::pointer egl_display, std::array<file_t, nv12_img_t::num_fds> &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) {
auto y_attribs = surface_descriptor_to_egl_attribs(y);
auto uv_attribs = surface_descriptor_to_egl_attribs(uv);
nv12_t nv12 {
egl_display,
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.data()),
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, uv_attribs.data()),
gl::tex_t::make(2),
gl::frame_buf_t::make(2),
std::move(fds)
};
if (!nv12->r8 || !nv12->bg88) {
BOOST_LOG(error) << "Couldn't import YUV target: "sv << util::hex(eglGetError()).to_string_view();
return std::nullopt;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]);
if (!gl::egl_image_target_texture_2d()) {
BOOST_LOG(error) << "glEGLImageTargetTexture2DOES is not available; cannot import YUV DMA-BUF"sv;
return std::nullopt;
}
gl::egl_image_target_texture_2d()(GL_TEXTURE_2D, nv12->r8);
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]);
gl::egl_image_target_texture_2d()(GL_TEXTURE_2D, nv12->bg88);
nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex));
nv12_bind_framebuffers(nv12);
return nv12;
}
/**
* @brief Import a YUV444 target image from DMA-BUF descriptors.
*
* @param egl_display EGL display used to create the images.
* @param fds File descriptors backing the YUV444 image.
* @param y Luma plane descriptor.
* @param u U chroma plane descriptor.
* @param v V chroma plane descriptor.
* @return Imported YUV444 image, or empty when import fails.
*/
std::optional<yuv444_t> import_target_yuv444(
display_t::pointer egl_display,
std::array<file_t, yuv444_img_t::num_fds> &&fds,
const surface_descriptor_t &y,
const surface_descriptor_t &u,
const surface_descriptor_t &v
) {
auto y_attribs = surface_descriptor_to_egl_attribs(y);
auto u_attribs = surface_descriptor_to_egl_attribs(u);
auto v_attribs = surface_descriptor_to_egl_attribs(v);
yuv444_t yuv444 {
egl_display,
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.data()),
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, u_attribs.data()),
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, v_attribs.data()),
gl::tex_t::make(3),
gl::frame_buf_t::make(3),
std::move(fds)
};
if (!yuv444->r8 || !yuv444->g8 || !yuv444->b8) {
BOOST_LOG(error) << "Couldn't import YUV target: "sv << util::hex(eglGetError()).to_string_view();
return std::nullopt;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, yuv444->tex[0]);
if (!gl::egl_image_target_texture_2d()) {
BOOST_LOG(error) << "glEGLImageTargetTexture2DOES is not available; cannot import YUV DMA-BUF"sv;
return std::nullopt;
}
gl::egl_image_target_texture_2d()(GL_TEXTURE_2D, yuv444->r8);
gl::ctx.BindTexture(GL_TEXTURE_2D, yuv444->tex[1]);
gl::egl_image_target_texture_2d()(GL_TEXTURE_2D, yuv444->g8);
gl::ctx.BindTexture(GL_TEXTURE_2D, yuv444->tex[2]);
gl::egl_image_target_texture_2d()(GL_TEXTURE_2D, yuv444->b8);
yuv444->buf.bind(std::begin(yuv444->tex), std::end(yuv444->tex));
yuv44_bind_framebuffers(yuv444);
return yuv444;
}
/**
* @brief Create biplanar YUV textures to render into.
* @param width Width of the target frame.
* @param height Height of the target frame.
* @param format Format of the target frame.
* @return The new RGB texture.
*/
std::optional<nv12_t> create_nv12_target(int width, int height, AVPixelFormat format) {
nv12_t nv12 {
nv12_img_t {
.display = EGL_NO_DISPLAY,
.r8 = EGL_NO_IMAGE,
.bg88 = EGL_NO_IMAGE,
.tex = gl::tex_t::make(2),
.buf = gl::frame_buf_t::make(2),
},
};
GLint y_format;
GLint uv_format;
// Determine the size of each plane element
auto fmt_desc = av_pix_fmt_desc_get(format);
if (fmt_desc->comp[0].depth <= 8) {
y_format = GL_R8;
uv_format = GL_RG8;
} else if (fmt_desc->comp[0].depth <= 16) {
y_format = GL_R16;
uv_format = GL_RG16;
} else {
BOOST_LOG(error) << "Unsupported target pixel format: "sv << format;
return std::nullopt;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, y_format, width, height);
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, uv_format, width >> fmt_desc->log2_chroma_w, height >> fmt_desc->log2_chroma_h);
nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex));
nv12_bind_framebuffers(nv12);
return nv12;
}
/**
* @brief Create YUV444 target.
*/
std::optional<yuv444_t> create_yuv444_target(int width, int height, AVPixelFormat format) {
yuv444_t yuv444 {
yuv444_img_t {
.display = EGL_NO_DISPLAY,
.r8 = EGL_NO_IMAGE,
.g8 = EGL_NO_IMAGE,
.b8 = EGL_NO_IMAGE,
.tex = gl::tex_t::make(3),
.buf = gl::frame_buf_t::make(3),
},
};
GLint y_format;
GLint u_format;
GLint v_format;
// Determine the size of each plane element
auto fmt_desc = av_pix_fmt_desc_get(format);
if (fmt_desc->comp[0].depth <= 8) {
y_format = GL_R8;
u_format = GL_R8;
v_format = GL_R8;
} else if (fmt_desc->comp[0].depth <= 16) {
y_format = GL_R16;
u_format = GL_R16;
v_format = GL_R16;
} else {
BOOST_LOG(error) << "Unsupported target pixel format: "sv << format;
return std::nullopt;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, yuv444->tex[0]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, y_format, width, height);
gl::ctx.BindTexture(GL_TEXTURE_2D, yuv444->tex[1]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, u_format, width, height);
gl::ctx.BindTexture(GL_TEXTURE_2D, yuv444->tex[2]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, v_format, width, height);
yuv444->buf.bind(std::begin(yuv444->tex), std::end(yuv444->tex));
yuv44_bind_framebuffers(yuv444);
return yuv444;
}
void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace, bool is_yuv444) {
auto color_p = video::color_vectors_from_colorspace(colorspace, true);
std::string_view members[] {
util::view(color_p->color_vec_y),
util::view(color_p->color_vec_u),
util::view(color_p->color_vec_v),
util::view(color_p->range_y),
util::view(color_p->range_uv),
};
color_matrix.update(members, sizeof(members) / sizeof(decltype(members[0])));
int planesCount = is_yuv444 ? 3 : 2;
for (int i = 0; i < planesCount; i++) {
program[i].bind(color_matrix);
}
}
/**
* @brief Configure the EGL/OpenGL scaling and colorspace conversion pipeline.
*
* @param sws Software-scaling pipeline to configure.
* @param color_p Color p.
* @param tex Texture resource used by the converter.
* @param is_yuv444 Is YUV444.
* @return 0 when shaders, framebuffers, and color uniforms are ready; nonzero on failure.
*/
int configure_sws_pipeline(sws_t &sws, const video::color_t *color_p, gl::tex_t &&tex, bool is_yuv444) {
std::array<std::pair<const char *, std::string_view>, 5> members {{
std::make_pair("color_vec_y", util::view(color_p->color_vec_y)),
std::make_pair("color_vec_u", util::view(color_p->color_vec_u)),
std::make_pair("color_vec_v", util::view(color_p->color_vec_v)),
std::make_pair("range_y", util::view(color_p->range_y)),
std::make_pair("range_uv", util::view(color_p->range_uv)),
}};
auto color_matrix = sws.program[0].uniform("ColorMatrix", members.data(), members.size());
if (!color_matrix) {
return -1;
}
sws.color_matrix = std::move(*color_matrix);
sws.tex = std::move(tex);
sws.cursor_framebuffer = gl::frame_buf_t::make(1);
sws.cursor_framebuffer.bind(&sws.tex[0], &sws.tex[1]);
int programCount = is_yuv444 ? 3 : 2;
for (int i = 0; i < programCount; i++) {
sws.program[i].bind(sws.color_matrix);
}
gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
gl_drain_errors;
return 0;
}
std::optional<sws_t> sws_t::make_nv12(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) {
sws_t sws;
sws.serial = std::numeric_limits<std::uint64_t>::max();
// Ensure aspect ratio is maintained
auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height);
auto out_width_f = in_width * scalar;
auto out_height_f = in_height * scalar;
// result is always positive
auto offsetX_f = (out_width - out_width_f) / 2;
auto offsetY_f = (out_height - out_height_f) / 2;
sws.out_width = out_width_f;
sws.out_height = out_height_f;
sws.in_width = in_width;
sws.in_height = in_height;
sws.offsetX = offsetX_f;
sws.offsetY = offsetY_f;
auto width_i = 1.0f / sws.out_width;
{
constexpr std::array<const char *, 5> sources {{
SUNSHINE_SHADERS_DIR "/ConvertUV.frag",
SUNSHINE_SHADERS_DIR "/ConvertUV.vert",
SUNSHINE_SHADERS_DIR "/ConvertY.frag",
SUNSHINE_SHADERS_DIR "/Scene.vert",
SUNSHINE_SHADERS_DIR "/Scene.frag",
}};
constexpr std::array<GLenum, 2> shader_type {{
GL_FRAGMENT_SHADER,
GL_VERTEX_SHADER,
}};
constexpr auto count = sources.size();
std::array<util::Either<gl::shader_t, std::string>, count> compiled_sources;
bool error_flag = false;
for (size_t x = 0; x < count; ++x) {
auto &compiled_source = compiled_sources[x];
compiled_source = gl::shader_t::compile(file_handler::read_file(sources[x]), shader_type[x % 2]);
gl_drain_errors;
if (compiled_source.has_right()) {
BOOST_LOG(error) << sources[x] << ": "sv << compiled_source.right();
error_flag = true;
}
}
if (error_flag) {
return std::nullopt;
}
auto program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[4].left());
if (program.has_right()) {
BOOST_LOG(error) << "GL linker: "sv << program.right();
return std::nullopt;
}
// Cursor - shader
sws.program[2] = std::move(program.left());
program = gl::program_t::link(compiled_sources[1].left(), compiled_sources[0].left());
if (program.has_right()) {
BOOST_LOG(error) << "GL linker: "sv << program.right();
return std::nullopt;
}
// UV - shader
sws.program[1] = std::move(program.left());
program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[2].left());
if (program.has_right()) {
BOOST_LOG(error) << "GL linker: "sv << program.right();
return std::nullopt;
}
// Y - shader
sws.program[0] = std::move(program.left());
}
auto loc_width_i = gl::ctx.GetUniformLocation(sws.program[1].handle(), "width_i");
if (loc_width_i < 0) {
BOOST_LOG(error) << "Couldn't find uniform [width_i]"sv;
return std::nullopt;
}
gl::ctx.UseProgram(sws.program[1].handle());
gl::ctx.Uniform1fv(loc_width_i, 1, &width_i);
auto color_p = video::color_vectors_from_colorspace({video::colorspace_e::rec601, false, 8}, true);
int pipeline = configure_sws_pipeline(sws, color_p, std::move(tex), false);
if (pipeline < 0) {
return std::nullopt;
}
return sws;
}
std::optional<sws_t> sws_t::make_yuv444(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) {
sws_t sws;
sws.serial = std::numeric_limits<std::uint64_t>::max();
// Ensure aspect ratio is maintained
auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height);
auto out_width_f = in_width * scalar;
auto out_height_f = in_height * scalar;
// result is always positive
auto offsetX_f = out_width - out_width_f;
auto offsetY_f = out_height - out_height_f;
sws.out_width = out_width_f;
sws.out_height = out_height_f;
sws.in_width = in_width;
sws.in_height = in_height;
sws.offsetX = offsetX_f;
sws.offsetY = offsetY_f;
{
constexpr std::array<const char *, 5> sources {{
SUNSHINE_SHADERS_DIR "/Scene.vert",
SUNSHINE_SHADERS_DIR "/ConvertV.frag",
SUNSHINE_SHADERS_DIR "/ConvertU.frag",
SUNSHINE_SHADERS_DIR "/ConvertY.frag",
SUNSHINE_SHADERS_DIR "/Scene.frag",
}};
constexpr std::array<GLenum, 2> shader_type {{
GL_FRAGMENT_SHADER,
GL_VERTEX_SHADER,
}};
constexpr auto count = sources.size();
std::array<util::Either<gl::shader_t, std::string>, count> compiled_sources;
bool error_flag = false;
for (int x = 0; x < count; ++x) {
auto &compiled_source = compiled_sources[x];
int num = x == 0 ? 1 : 0;
compiled_source = gl::shader_t::compile(file_handler::read_file(sources[x]), shader_type[num]);
gl_drain_errors;
if (compiled_source.has_right()) {
BOOST_LOG(error) << sources[x] << ": "sv << compiled_source.right();
error_flag = true;
}
}
if (error_flag) {
return std::nullopt;
}
auto program = gl::program_t::link(compiled_sources[0].left(), compiled_sources[4].left());
if (program.has_right()) {
BOOST_LOG(error) << "GL linker (cursor shader): "sv << program.right();
return std::nullopt;
}
// Cursor - shader
sws.program[3] = std::move(program.left());
program = gl::program_t::link(compiled_sources[0].left(), compiled_sources[1].left());
if (program.has_right()) {
BOOST_LOG(error) << "GL linker (V - shader): "sv << program.right();
return std::nullopt;
}
// V - shader
sws.program[2] = std::move(program.left());
program = gl::program_t::link(compiled_sources[0].left(), compiled_sources[2].left());
if (program.has_right()) {
BOOST_LOG(error) << "GL linker (U - shader): "sv << program.right();
return std::nullopt;
}
// U - shader
sws.program[1] = std::move(program.left());
program = gl::program_t::link(compiled_sources[0].left(), compiled_sources[3].left());
if (program.has_right()) {
BOOST_LOG(error) << "GL linker (Y - shader): "sv << program.right();
return std::nullopt;
}
// Y - shader
sws.program[0] = std::move(program.left());
}
auto color_p = video::color_vectors_from_colorspace({video::colorspace_e::rec709, true, 8}, false);
int pipeline = configure_sws_pipeline(sws, color_p, std::move(tex), true);
if (pipeline < 0) {
return std::nullopt;
}
return sws;
}
int sws_t::blank(gl::frame_buf_t &fb, int offsetX_, int offsetY_, int width, int height, bool is_yuv444) {
auto f = [&]() {
std::swap(offsetX_, this->offsetX);
std::swap(offsetY_, this->offsetY);
std::swap(width, this->out_width);
std::swap(height, this->out_height);
};
f();
auto fg = util::fail_guard(f);
if (is_yuv444) {
return convert_yuv444(fb);
}
return convert_nv12(fb);
}
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format, bool is_yuv444) {
GLint gl_format;
// Decide the bit depth format of the backing texture based the target frame format
auto fmt_desc = av_pix_fmt_desc_get(format);
switch (fmt_desc->comp[0].depth) {
case 8:
gl_format = GL_RGBA8;
break;
case 10:
gl_format = GL_RGB10_A2;
break;
case 12:
gl_format = GL_RGBA12;
break;
case 16:
gl_format = GL_RGBA16;
break;
default:
BOOST_LOG(error) << "Unsupported pixel format for EGL frame: "sv << (int) format;
return std::nullopt;
}
auto tex = gl::tex_t::make(2);
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, gl_format, in_width, in_height);
if (is_yuv444) {
return make_yuv444(in_width, in_height, out_width, out_height, std::move(tex));
}
return make_nv12(in_width, in_height, out_width, out_height, std::move(tex));
}
void sws_t::load_ram(platf::img_t &img) {
loaded_texture = tex[0];
gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);
gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
}
void sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture, bool is_yuv444) {
// When only a sub-part of the image must be encoded...
const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height;
if (copy) {
auto framebuf = gl::frame_buf_t::make(1);
framebuf.bind(&texture, &texture + 1);
loaded_texture = tex[0];
framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height);
} else {
loaded_texture = texture;
}
if (img.data) {
GLenum attachment = GL_COLOR_ATTACHMENT0;
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]);
// For NV12 cursor program index is 2, for YUV444 it's 3
const int cursor_program = is_yuv444 ? 3 : 2;
gl::ctx.UseProgram(program[cursor_program].handle());
// When a copy has already been made...
if (!copy) {
gl::ctx.BindTexture(GL_TEXTURE_2D, texture);
gl::ctx.DrawBuffers(1, &attachment);
gl::ctx.Viewport(0, 0, in_width, in_height);
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
loaded_texture = tex[0];
}
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]);
if (serial != img.serial) {
serial = img.serial;
gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.src_w, img.src_h, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
}
gl::ctx.Enable(GL_BLEND);
gl::ctx.DrawBuffers(1, &attachment);
#ifndef NDEBUG
auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
BOOST_LOG(error) << "Pass Cursor: CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
return;
}
#endif
gl::ctx.Viewport(img.x, img.y, img.width, img.height);
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
gl::ctx.Disable(GL_BLEND);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
int sws_t::draw_programs_to_buffers(GLenum attachments[], gl::frame_buf_t &fb, int count, bool is_yuv444) {
for (int x = 0; x < count; ++x) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, fb[x]);
gl::ctx.DrawBuffers(1, &attachments[x]);
#ifndef NDEBUG
auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
BOOST_LOG(error) << "Pass "sv << x << ": CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
#endif
int sizeCoef = is_yuv444 ? 1 : x + 1;
gl::ctx.UseProgram(program[x].handle());
gl::ctx.Viewport(offsetX / sizeCoef, offsetY / sizeCoef, out_width / sizeCoef, out_height / sizeCoef);
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
}
return 0;
}
int sws_t::convert_nv12(gl::frame_buf_t &fb) {
gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);
GLenum attachments[] {
GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1
};
int attachmentsCount = sizeof(attachments) / sizeof(decltype(attachments[0]));
int drawBuffers = draw_programs_to_buffers(attachments, fb, attachmentsCount, false);
if (drawBuffers < 0) {
return -1;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
gl::ctx.Flush();
return 0;
}
int sws_t::convert_yuv444(gl::frame_buf_t &fb) {
gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);
GLenum attachments[] {
GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1,
GL_COLOR_ATTACHMENT2
};
int attachmentsCount = sizeof(attachments) / sizeof(decltype(attachments[0]));
int drawBuffers = draw_programs_to_buffers(attachments, fb, attachmentsCount, true);
if (drawBuffers < 0) {
return -1;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
gl::ctx.Flush();
return 0;
}
} // namespace egl
/**
* @brief Release an FFmpeg frame allocated by the capture or conversion backend.
*
* @param frame Video or graphics frame being processed.
*/
void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}