diff --git a/app/app.pro b/app/app.pro
index d47ca829..844fb112 100644
--- a/app/app.pro
+++ b/app/app.pro
@@ -90,6 +90,10 @@ unix:!macx {
CONFIG += libva
}
+ packagesExist(egl) {
+ CONFIG += egl
+ }
+
packagesExist(vdpau) {
CONFIG += libvdpau
}
@@ -264,6 +268,15 @@ libdrm {
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
HEADERS += streaming/video/ffmpeg-renderers/drm.h
}
+egl {
+ message(EGL renderer selected)
+
+ DEFINES += HAVE_EGL
+ SOURCES += \
+ streaming/video/ffmpeg-renderers/eglvid.cpp \
+ streaming/video/ffmpeg-renderers/egl_extensions.cpp
+ HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
+}
config_SL {
message(Steam Link build configuration selected)
diff --git a/app/resources.qrc b/app/resources.qrc
index eed55f6b..84fc8c45 100644
--- a/app/resources.qrc
+++ b/app/resources.qrc
@@ -19,5 +19,7 @@
SDL_GameControllerDB/gamecontrollerdb.txt
ModeSeven.ttf
+ shaders/egl.frag
+ shaders/egl.vert
diff --git a/app/shaders/egl.frag b/app/shaders/egl.frag
new file mode 100644
index 00000000..cd1f064b
--- /dev/null
+++ b/app/shaders/egl.frag
@@ -0,0 +1,21 @@
+#version 300 es
+#extension GL_OES_EGL_image_external : require
+precision mediump float;
+out vec4 FragColor;
+
+in vec2 vTextCoord;
+
+uniform mat3 yuvmat;
+uniform vec3 offset;
+uniform samplerExternalOES plane1;
+uniform samplerExternalOES plane2;
+
+void main() {
+ vec3 YCbCr = vec3(
+ texture2D(plane1, vTextCoord)[0],
+ texture2D(plane2, vTextCoord).xy
+ );
+
+ YCbCr -= offset;
+ FragColor = vec4(clamp(yuvmat * YCbCr, 0.0, 1.0), 1.0f);
+}
diff --git a/app/shaders/egl.vert b/app/shaders/egl.vert
new file mode 100644
index 00000000..bf0848d9
--- /dev/null
+++ b/app/shaders/egl.vert
@@ -0,0 +1,10 @@
+#version 300 es
+
+layout (location = 0) in vec2 aPosition; // 2D: X,Y
+layout (location = 1) in vec2 aTexCoord;
+out vec2 vTextCoord;
+
+void main() {
+ vTextCoord = aTexCoord;
+ gl_Position = vec4(aPosition, 0, 1);
+}
diff --git a/app/streaming/video/ffmpeg-renderers/egl_extensions.cpp b/app/streaming/video/ffmpeg-renderers/egl_extensions.cpp
new file mode 100644
index 00000000..571531e8
--- /dev/null
+++ b/app/streaming/video/ffmpeg-renderers/egl_extensions.cpp
@@ -0,0 +1,23 @@
+// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
+#define GL_GLEXT_PROTOTYPES
+
+#include "renderer.h"
+
+#include
+
+static QStringList egl_get_extensions(EGLDisplay dpy) {
+ const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
+ if (!EGLExtensionsStr) {
+ SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
+ return QStringList();
+ }
+ return QString(EGLExtensionsStr).split(" ");
+}
+
+EGLExtensions::EGLExtensions(EGLDisplay dpy) :
+ m_Extensions(egl_get_extensions(dpy))
+{}
+
+bool EGLExtensions::isSupported(const QString &extension) const {
+ return m_Extensions.contains(extension);
+}
diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.cpp b/app/streaming/video/ffmpeg-renderers/eglvid.cpp
new file mode 100644
index 00000000..d2ab2ddc
--- /dev/null
+++ b/app/streaming/video/ffmpeg-renderers/eglvid.cpp
@@ -0,0 +1,480 @@
+// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
+#define GL_GLEXT_PROTOTYPES
+
+#include "eglvid.h"
+
+#include "path.h"
+#include "streaming/session.h"
+#include "streaming/streamutils.h"
+
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* TODO:
+ * - handle more pixel formats
+ * - handle software decoding
+ */
+
+/* DOC/misc:
+ * - https://kernel-recipes.org/en/2016/talks/video-and-colorspaces/
+ * - http://www.brucelindbloom.com/
+ * - https://learnopengl.com/Getting-started/Shaders
+ * - https://github.com/stunpix/yuvit
+ * - https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
+ * - https://www.renesas.com/eu/en/www/doc/application-note/an9717.pdf
+ * - https://www.xilinx.com/support/documentation/application_notes/xapp283.pdf
+ * - https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
+ * - https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt
+ * - https://gist.github.com/rexguo/6696123
+ * - https://wiki.libsdl.org/CategoryVideo
+ */
+
+#define EGL_LOG(Category, ...) SDL_Log ## Category(\
+ SDL_LOG_CATEGORY_APPLICATION, \
+ "EGLRenderer: " __VA_ARGS__)
+
+EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
+ :
+ m_SwPixelFormat(AV_PIX_FMT_NONE),
+ m_EGLDisplay(nullptr),
+ m_Textures{0},
+ m_ShaderProgram(0),
+ m_Context(0),
+ m_Window(nullptr),
+ m_Backend(backendRenderer),
+ m_VAO(0),
+ m_ColorSpace(AVCOL_SPC_NB),
+ m_ColorFull(false),
+ EGLImageTargetTexture2DOES(nullptr),
+ m_DummyRenderer(nullptr)
+{
+ SDL_assert(backendRenderer);
+ SDL_assert(backendRenderer->canExportEGL());
+}
+
+EGLRenderer::~EGLRenderer()
+{
+ if (m_Context) {
+ if (m_ShaderProgram)
+ glDeleteProgram(m_ShaderProgram);
+ if (m_VAO)
+ glDeleteVertexArrays(1, &m_VAO);
+ if (m_DummyRenderer)
+ SDL_DestroyRenderer(m_DummyRenderer);
+ SDL_GL_DeleteContext(m_Context);
+ }
+}
+
+bool EGLRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
+{
+ /* Nothing to do */
+
+ EGL_LOG(Info, "Using EGL renderer");
+
+ return true;
+}
+
+void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType)
+{
+ // TODO: FIXME
+}
+
+bool EGLRenderer::isRenderThreadSupported()
+{
+ // TODO: can we use DMA-BUF in multithreaded context ?
+ return false;
+}
+
+bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
+{
+ // Remember to keep this in sync with EGLRenderer::renderFrame()!
+ switch (pixelFormat)
+ {
+ case AV_PIX_FMT_NV12:
+ return true;
+ default:
+ return false;
+ }
+}
+
+int EGLRenderer::loadAndBuildShader(int shaderType,
+ const char *file) {
+ GLuint shader = glCreateShader(shaderType);
+ if (!shader || shader == GL_INVALID_ENUM) {
+ EGL_LOG(Error, "Can't create shader: %d", glGetError());
+ return 0;
+ }
+
+ auto sourceData = Path::readDataFile(file);
+ GLint len = sourceData.size();
+ const char *buf = sourceData.data();
+
+ glShaderSource(shader, 1, &buf, &len);
+ glCompileShader(shader);
+ GLint status;
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
+ if (!status) {
+ char shaderLog[512];
+ glGetShaderInfoLog(shader, sizeof (shaderLog), nullptr, shaderLog);
+ EGL_LOG(Error, "Cannot load shader \"%s\": %s", file, shaderLog);
+ return 0;
+ }
+
+ return shader;
+}
+
+bool EGLRenderer::compileShader() {
+ SDL_assert(!m_ShaderProgram);
+ SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
+
+ // XXX: TODO: other formats
+ SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
+
+ bool ret = false;
+
+ GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, "egl.vert");
+ if (!vertexShader)
+ return false;
+
+ GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, "egl.frag");
+ if (!fragmentShader)
+ goto fragError;
+
+ m_ShaderProgram = glCreateProgram();
+ if (!m_ShaderProgram) {
+ EGL_LOG(Error, "Cannot create shader program");
+ goto progFailCreate;
+ }
+
+ glAttachShader(m_ShaderProgram, vertexShader);
+ glAttachShader(m_ShaderProgram, fragmentShader);
+ glLinkProgram(m_ShaderProgram);
+ int status;
+ glGetProgramiv(m_ShaderProgram, GL_LINK_STATUS, &status);
+ if (status) {
+ ret = true;
+ } else {
+ char shader_log[512];
+ glGetProgramInfoLog(m_ShaderProgram, sizeof (shader_log), nullptr, shader_log);
+ EGL_LOG(Error, "Cannot link shader program: %s", shader_log);
+ glDeleteProgram(m_ShaderProgram);
+ m_ShaderProgram = 0;
+ }
+
+progFailCreate:
+ glDeleteShader(fragmentShader);
+fragError:
+ glDeleteShader(vertexShader);
+ return ret;
+}
+
+bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
+{
+ m_Window = params->window;
+
+ if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
+ // EGL doesn't support rendering YUV 10-bit textures yet
+ return false;
+ }
+
+ /*
+ * Create a dummy renderer in order to craft an accelerated SDL Window.
+ * Request opengl ES 3.0 context, otherwise it will SIGSEGV
+ * https://gitlab.freedesktop.org/mesa/mesa/issues/1011
+ */
+ SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
+
+ int renderIndex;
+ int maxRenderers = SDL_GetNumRenderDrivers();
+ SDL_assert(maxRenderers >= 0);
+
+ SDL_RendererInfo renderInfo;
+ for (renderIndex = 0; renderIndex < maxRenderers; ++renderIndex) {
+ if (SDL_GetRenderDriverInfo(renderIndex, &renderInfo))
+ continue;
+ if (!strcmp(renderInfo.name, "opengles2")) {
+ SDL_assert(renderInfo.flags & SDL_RENDERER_ACCELERATED);
+ break;
+ }
+ }
+ if (renderIndex == maxRenderers) {
+ EGL_LOG(Error, "Could not find a suitable SDL_Renderer");
+ return false;
+ }
+
+ if (!(m_DummyRenderer = SDL_CreateRenderer(m_Window, renderIndex, SDL_RENDERER_ACCELERATED))) {
+ EGL_LOG(Error, "SDL_CreateRenderer() failed: %s", SDL_GetError());
+ return false;
+ }
+
+ SDL_SysWMinfo info;
+ SDL_VERSION(&info.version);
+ if (!SDL_GetWindowWMInfo(params->window, &info)) {
+ EGL_LOG(Error, "SDL_GetWindowWMInfo() failed: %s", SDL_GetError());
+ return false;
+ }
+ switch (info.subsystem) {
+ case SDL_SYSWM_WAYLAND:
+ m_EGLDisplay = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR,
+ info.info.wl.display, nullptr);
+ break;
+ case SDL_SYSWM_X11:
+ m_EGLDisplay = eglGetPlatformDisplay(EGL_PLATFORM_X11_KHR,
+ info.info.x11.display, nullptr);
+ break;
+ default:
+ EGL_LOG(Error, "not compatible with SYSWM");
+ return false;
+ }
+
+ if (!m_EGLDisplay) {
+ EGL_LOG(Error, "Cannot get EGL display: %d", eglGetError());
+ return false;
+ }
+
+ if (!(m_Context = SDL_GL_CreateContext(params->window))) {
+ EGL_LOG(Error, "Cannot create OpenGL context: %s", SDL_GetError());
+ return false;
+ }
+ if (SDL_GL_MakeCurrent(params->window, m_Context)) {
+ EGL_LOG(Error, "Cannot use created EGL context: %s", SDL_GetError());
+ return false;
+ }
+
+ const EGLExtensions egl_extensions(m_EGLDisplay);
+ if (!egl_extensions.isSupported("EGL_KHR_image_base") &&
+ !egl_extensions.isSupported("EGL_KHR_image")) {
+ EGL_LOG(Error, "KHR_image unsupported");
+ return false;
+ }
+
+ if (!m_Backend->initializeEGL(m_EGLDisplay, egl_extensions))
+ return false;
+
+ if (!(EGLImageTargetTexture2DOES = (EGLImageTargetTexture2DOES_t)eglGetProcAddress("glEGLImageTargetTexture2DOES"))) {
+ EGL_LOG(Error,
+ "EGL: cannot retrieve `EGLImageTargetTexture2DOES` address");
+ return false;
+ }
+
+ /* Compute the video region size in order to keep the aspect ratio of the
+ * video stream.
+ */
+ SDL_Rect src, dst;
+ src.x = src.y = dst.x = dst.y = 0;
+ src.w = params->width;
+ src.h = params->height;
+ SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
+ StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
+
+ glViewport(dst.x, dst.y, dst.w, dst.h);
+
+ glClearColor(0, 0, 0, 1);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ if (params->enableVsync) {
+ /* Try to use adaptive VSYNC */
+ if (SDL_GL_SetSwapInterval(-1))
+ SDL_GL_SetSwapInterval(1);
+ } else {
+ SDL_GL_SetSwapInterval(0);
+ }
+
+ SDL_GL_SwapWindow(params->window);
+
+ glGenTextures(EGL_MAX_PLANES, m_Textures);
+ for (size_t i = 0; i < EGL_MAX_PLANES; ++i) {
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ }
+
+ GLenum err = glGetError();
+ if (err != GL_NO_ERROR)
+ EGL_LOG(Error, "OpenGL error: %d", err);
+
+ return err == GL_NO_ERROR;
+}
+
+const float *EGLRenderer::getColorMatrix() {
+ /* The conversion matrices are shamelessly stolen from linux:
+ * drivers/media/platform/imx-pxp.c:pxp_setup_csc
+ */
+ static const float bt601Lim[] = {
+ 1.1644f, 1.1644f, 1.1644f,
+ 0.0f, -0.3917f, 2.0172f,
+ 1.5960f, -0.8129f, 0.0f
+ };
+ static const float bt601Full[] = {
+ 1.0f, 1.0f, 1.0f,
+ 0.0f, -0.3441f, 1.7720f,
+ 1.4020f, -0.7141f, 0.0f
+ };
+ static const float bt709Lim[] = {
+ 1.1644f, 1.1644f, 1.1644f,
+ 0.0f, -0.2132f, 2.1124f,
+ 1.7927f, -0.5329f, 0.0f
+ };
+ static const float bt709Full[] = {
+ 1.0f, 1.0f, 1.0f,
+ 0.0f, -0.1873f, 1.8556f,
+ 1.5748f, -0.4681f, 0.0f
+ };
+ static const float bt2020Lim[] = {
+ 1.1644f, 1.1644f, 1.1644f,
+ 0.0f, -0.1874f, 2.1418f,
+ 1.6781f, -0.6505f, 0.0f
+ };
+ static const float bt2020Full[] = {
+ 1.0f, 1.0f, 1.0f,
+ 0.0f, -0.1646f, 1.8814f,
+ 1.4746f, -0.5714f, 0.0f
+ };
+
+ switch (m_ColorSpace) {
+ case AVCOL_SPC_SMPTE170M:
+ case AVCOL_SPC_BT470BG:
+ EGL_LOG(Info, "BT-601 pixels");
+ return m_ColorFull ? bt601Full : bt601Lim;
+ case AVCOL_SPC_BT709:
+ EGL_LOG(Info, "BT-709 pixels");
+ return m_ColorFull ? bt709Full : bt709Lim;
+ case AVCOL_SPC_BT2020_NCL:
+ case AVCOL_SPC_BT2020_CL:
+ EGL_LOG(Info, "BT-2020 pixels");
+ return m_ColorFull ? bt2020Full : bt2020Lim;
+ };
+ EGL_LOG(Warn, "unknown color space: %d, falling back to BT-601",
+ m_ColorSpace);
+ return bt601Lim;
+}
+
+bool EGLRenderer::specialize() {
+ SDL_assert(!m_VAO);
+
+ if (!compileShader())
+ return false;
+
+ // The viewport should have the aspect ratio of the video stream
+ static const float vertices[] = {
+ // pos .... // tex coords
+ 1.0f, 1.0f, 1.0f, 0.0f,
+ 1.0f, -1.0f, 1.0f, 1.0f,
+ -1.0f, -1.0f, 0.0f, 1.0f,
+ -1.0f, 1.0f, 0.0f, 0.0f,
+
+ };
+ static const unsigned int indices[] = {
+ 0, 1, 3,
+ 1, 2, 3,
+ };
+
+ glUseProgram(m_ShaderProgram);
+
+ unsigned int VBO, EBO;
+ glGenVertexArrays(1, &m_VAO);
+ glGenBuffers(1, &VBO);
+ glGenBuffers(1, &EBO);
+
+ glBindVertexArray(m_VAO);
+
+ glBindBuffer(GL_ARRAY_BUFFER, VBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof (indices), indices, GL_STATIC_DRAW);
+
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof (float)));
+ glEnableVertexAttribArray(1);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+
+ int yuvmatLocation = glGetUniformLocation(m_ShaderProgram, "yuvmat");
+ glUniformMatrix3fv(yuvmatLocation, 1, GL_FALSE, getColorMatrix());
+
+ static const float limitedOffsets[] = { 16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f };
+ static const float fullOffsets[] = { 0.0f, 128.0f / 255.0f, 128.0f / 255.0f };
+
+ int offLocation = glGetUniformLocation(m_ShaderProgram, "offset");
+ glUniform3fv(offLocation, 1, m_ColorFull ? fullOffsets : limitedOffsets);
+
+ int colorPlane = glGetUniformLocation(m_ShaderProgram, "plane1");
+ glUniform1i(colorPlane, 0);
+ colorPlane = glGetUniformLocation(m_ShaderProgram, "plane2");
+ glUniform1i(colorPlane, 1);
+
+ glDeleteBuffers(1, &VBO);
+ glDeleteBuffers(1, &EBO);
+
+ GLenum err = glGetError();
+ if (err != GL_NO_ERROR) {
+ EGL_LOG(Error, "OpenGL error: %d", err);
+ }
+
+ return err == GL_NO_ERROR;
+}
+
+void EGLRenderer::renderFrame(AVFrame* frame)
+{
+ EGLImage imgs[EGL_MAX_PLANES];
+ if (frame->hw_frames_ctx != nullptr) {
+ // Find the native read-back format and load the shader
+ if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
+ auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
+
+ m_SwPixelFormat = hwFrameCtx->sw_format;
+ SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
+
+ EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
+
+ // XXX: TODO: Handle other pixel formats
+ SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
+ m_ColorSpace = frame->colorspace;
+ m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
+
+ if (!specialize()) {
+ m_SwPixelFormat = AV_PIX_FMT_NONE;
+ return;
+ }
+ }
+
+ ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
+ if (plane_count < 0)
+ return;
+ for (ssize_t i = 0; i < plane_count; ++i) {
+ glActiveTexture(GL_TEXTURE0 + i);
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
+ EGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
+ }
+ } else {
+ // TODO: load texture for SW decoding ?
+ EGL_LOG(Error, "EGL rendering only supports hw frames");
+ return;
+ }
+
+ glClear(GL_COLOR_BUFFER_BIT);
+ glUseProgram(m_ShaderProgram);
+ glBindVertexArray(m_VAO);
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+
+ SDL_GL_SwapWindow(m_Window);
+ if (frame->hw_frames_ctx != nullptr)
+ m_Backend->freeEGLImages(m_EGLDisplay, imgs);
+}
diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.h b/app/streaming/video/ffmpeg-renderers/eglvid.h
new file mode 100644
index 00000000..b217017a
--- /dev/null
+++ b/app/streaming/video/ffmpeg-renderers/eglvid.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "renderer.h"
+
+class EGLRenderer : public IFFmpegRenderer {
+public:
+ EGLRenderer(IFFmpegRenderer *backendRenderer);
+ virtual ~EGLRenderer() override;
+ virtual bool initialize(PDECODER_PARAMETERS params) override;
+ virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
+ virtual void renderFrame(AVFrame* frame) override;
+ virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
+ virtual bool isRenderThreadSupported() override;
+ virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
+
+private:
+ using EGLImageTargetTexture2DOES_t = void (*)(int, void *);
+
+ bool compileShader();
+ bool specialize();
+ const float *getColorMatrix();
+ static int loadAndBuildShader(int shaderType, const char *filename);
+
+ int m_SwPixelFormat;
+ void *m_EGLDisplay;
+ unsigned m_Textures[EGL_MAX_PLANES];
+ unsigned m_ShaderProgram;
+ SDL_GLContext m_Context;
+ SDL_Window *m_Window;
+ IFFmpegRenderer *m_Backend;
+ unsigned int m_VAO;
+ int m_ColorSpace;
+ bool m_ColorFull;
+ EGLImageTargetTexture2DOES_t EGLImageTargetTexture2DOES;
+ SDL_Renderer *m_DummyRenderer;
+};
diff --git a/app/streaming/video/ffmpeg-renderers/renderer.h b/app/streaming/video/ffmpeg-renderers/renderer.h
index 291321e4..e7a36a2e 100644
--- a/app/streaming/video/ffmpeg-renderers/renderer.h
+++ b/app/streaming/video/ffmpeg-renderers/renderer.h
@@ -9,6 +9,23 @@ extern "C" {
#include
}
+#ifdef HAVE_EGL
+// SDL_egl.h have too many conflicts, we will do without
+typedef void *EGLDisplay;
+typedef void *EGLImage;
+#define EGL_MAX_PLANES 4
+
+class EGLExtensions {
+public:
+ EGLExtensions(EGLDisplay dpy);
+ ~EGLExtensions() {}
+ bool isSupported(const QString &extension) const;
+private:
+ const QStringList m_Extensions;
+};
+
+#endif
+
#define RENDERER_ATTRIBUTE_FULLSCREEN_ONLY 0x01
#define RENDERER_ATTRIBUTE_1080P_MAX 0x02
@@ -68,4 +85,25 @@ public:
virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
// Nothing
}
+
+#ifdef HAVE_EGL
+ // By default we can't do EGL
+ virtual bool canExportEGL() {
+ return false;
+ }
+
+ virtual bool initializeEGL(EGLDisplay,
+ const EGLExtensions &) {
+ return false;
+ }
+
+ virtual ssize_t exportEGLImages(AVFrame *,
+ EGLDisplay,
+ EGLImage[EGL_MAX_PLANES]) {
+ return -1;
+ }
+
+ // Free the ressources allocated during the last `exportEGLImages` call
+ virtual void freeEGLImages(EGLDisplay, EGLImage[EGL_MAX_PLANES]) {}
+#endif
};
diff --git a/app/streaming/video/ffmpeg-renderers/vaapi.cpp b/app/streaming/video/ffmpeg-renderers/vaapi.cpp
index a696751d..526c2d81 100644
--- a/app/streaming/video/ffmpeg-renderers/vaapi.cpp
+++ b/app/streaming/video/ffmpeg-renderers/vaapi.cpp
@@ -5,6 +5,9 @@
#include
#include
+#ifdef HAVE_EGL
+#include
+#endif
#include
#include
@@ -14,7 +17,11 @@ VAAPIRenderer::VAAPIRenderer()
m_DrmFd(-1),
m_BlacklistedForDirectRendering(false)
{
-
+#ifdef HAVE_EGL
+ m_PrimeDescriptor.num_layers = 0;
+ m_PrimeDescriptor.num_objects = 0;
+ m_EGLExtDmaBuf = false;
+#endif
}
VAAPIRenderer::~VAAPIRenderer()
@@ -442,3 +449,106 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
SDL_assert(false);
}
}
+
+#ifdef HAVE_EGL
+
+bool
+VAAPIRenderer::canExportEGL() {
+ return true;
+}
+
+bool
+VAAPIRenderer::initializeEGL(EGLDisplay,
+ const EGLExtensions &ext) {
+ if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+ "VAAPI-EGL: DMABUF unsupported");
+ return false;
+ }
+ m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
+ return true;
+}
+
+ssize_t
+VAAPIRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
+ EGLImage images[EGL_MAX_PLANES]) {
+ ssize_t count = 0;
+ auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
+ AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)hwFrameCtx->device_ctx->hwctx;
+
+ VASurfaceID surface_id = (VASurfaceID)(uintptr_t)frame->data[3];
+ VAStatus st = vaExportSurfaceHandle(vaDeviceContext->display,
+ surface_id,
+ VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
+ &m_PrimeDescriptor);
+ if (st != VA_STATUS_SUCCESS) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+ "vaExportSurfaceHandle failed: %d", st);
+ return -1;
+ }
+
+ SDL_assert(m_PrimeDescriptor.num_layers <= EGL_MAX_PLANES);
+
+ st = vaSyncSurface(vaDeviceContext->display, surface_id);
+ if (st != VA_STATUS_SUCCESS) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+ "vaSyncSurface failed: %d", st);
+ goto sync_fail;
+ }
+
+ for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
+ const auto &layer = m_PrimeDescriptor.layers[i];
+ const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
+
+ EGLAttrib attribs[17] = {
+ EGL_LINUX_DRM_FOURCC_EXT, (EGLint)layer.drm_format,
+ EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
+ EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
+ EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)layer.offset[0],
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)layer.pitch[0],
+ EGL_NONE,
+ };
+ if (m_EGLExtDmaBuf) {
+ const EGLAttrib extra[] = {
+ EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
+ (EGLint)object.drm_format_modifier,
+ EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
+ (EGLint)(object.drm_format_modifier >> 32),
+ EGL_NONE,
+ };
+ memcpy((void *)(&attribs[12]), (void *)extra, sizeof (extra));
+ }
+ images[i] = eglCreateImage(dpy, EGL_NO_CONTEXT,
+ EGL_LINUX_DMA_BUF_EXT,
+ nullptr, attribs);
+ if (!images[i]) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+ "eglCreateImage() Failed: %d", eglGetError());
+ goto create_image_fail;
+ }
+ ++count;
+ }
+ return count;
+
+create_image_fail:
+ m_PrimeDescriptor.num_layers = count;
+sync_fail:
+ freeEGLImages(dpy, images);
+ return -1;
+}
+
+void
+VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
+ for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
+ eglDestroyImage(dpy, images[i]);
+ }
+ for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
+ close(m_PrimeDescriptor.objects[i].fd);
+ }
+ m_PrimeDescriptor.num_layers = 0;
+ m_PrimeDescriptor.num_objects = 0;
+}
+
+#endif
diff --git a/app/streaming/video/ffmpeg-renderers/vaapi.h b/app/streaming/video/ffmpeg-renderers/vaapi.h
index f10f7be2..f14c69ed 100644
--- a/app/streaming/video/ffmpeg-renderers/vaapi.h
+++ b/app/streaming/video/ffmpeg-renderers/vaapi.h
@@ -26,6 +26,9 @@ extern "C" {
#include
#endif
#include
+#ifdef HAVE_EGL
+#include
+#endif
}
class VAAPIRenderer : public IFFmpegRenderer
@@ -39,6 +42,12 @@ public:
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
virtual int getDecoderColorspace() override;
+#ifdef HAVE_EGL
+ virtual bool canExportEGL() override;
+ virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext);
+ virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
+ virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
+#endif
private:
bool validateDriver(VADisplay display);
@@ -57,4 +66,9 @@ private:
int m_VideoHeight;
int m_DisplayWidth;
int m_DisplayHeight;
+
+#ifdef HAVE_EGL
+ VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
+ bool m_EGLExtDmaBuf;
+#endif
};
diff --git a/app/streaming/video/ffmpeg.cpp b/app/streaming/video/ffmpeg.cpp
index 8347842f..fd550fb8 100644
--- a/app/streaming/video/ffmpeg.cpp
+++ b/app/streaming/video/ffmpeg.cpp
@@ -32,6 +32,10 @@
#include "ffmpeg-renderers/drm.h"
#endif
+#ifdef HAVE_EGL
+#include "ffmpeg-renderers/eglvid.h"
+#endif
+
// This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp"
@@ -195,6 +199,15 @@ bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
m_FrontendRenderer = m_BackendRenderer;
}
else {
+#ifdef HAVE_EGL
+ if (m_BackendRenderer->canExportEGL()) {
+ m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
+ if (m_FrontendRenderer->initialize(params)) {
+ return true;
+ }
+ delete m_FrontendRenderer;
+ }
+#endif
// The backend renderer cannot directly render to the display, so
// we will create an SDL renderer to draw the frames.
m_FrontendRenderer = new SdlRenderer();