diff options
Diffstat (limited to 'pica-client/vaapivideowidget.cpp')
| -rw-r--r-- | pica-client/vaapivideowidget.cpp | 398 |
1 files changed, 398 insertions, 0 deletions
diff --git a/pica-client/vaapivideowidget.cpp b/pica-client/vaapivideowidget.cpp new file mode 100644 index 0000000..fa50d10 --- /dev/null +++ b/pica-client/vaapivideowidget.cpp @@ -0,0 +1,398 @@ +/* + (c) Copyright 2012 - 2026 Anton Sviridenko + https://picapica.im + + This program 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, version 3. + + This program 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 this program. If not, see <https://www.gnu.org/licenses/>. +*/ +#include "vaapivideowidget.h" + +#ifdef HAVE_VAAPI + +#include <QDebug> +#include <unistd.h> + +#include <EGL/egl.h> +#include <EGL/eglext.h> +#include <GL/gl.h> +#include <drm/drm_fourcc.h> + +extern "C" { +#include <libavutil/pixfmt.h> +} + +#include <va/va_drmcommon.h> + +namespace +{ + // Extension entry points, resolved once at first use: they are not + // linkable symbols, only reachable through eglGetProcAddress. + PFNEGLCREATEIMAGEKHRPROC g_eglCreateImageKHR = nullptr; + PFNEGLDESTROYIMAGEKHRPROC g_eglDestroyImageKHR = nullptr; + PFNGLEGLIMAGETARGETTEXTURE2DOESPROC g_glEGLImageTargetTexture2DOES = nullptr; + bool g_resolved = false; + + bool resolveEglExtensions() + { + if (g_resolved) + return g_eglCreateImageKHR && g_eglDestroyImageKHR && g_glEGLImageTargetTexture2DOES; + + g_resolved = true; + g_eglCreateImageKHR = + (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); + g_eglDestroyImageKHR = + (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); + g_glEGLImageTargetTexture2DOES = + (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); + + return g_eglCreateImageKHR && g_eglDestroyImageKHR && g_glEGLImageTargetTexture2DOES; + } + + const char *kVertexShader = + "attribute highp vec2 vertex;\n" + "attribute highp vec2 texcoord;\n" + "varying highp vec2 uv;\n" + "void main() {\n" + " uv = texcoord;\n" + " gl_Position = vec4(vertex, 0.0, 1.0);\n" + "}\n"; + + // NV12 arrives as a full resolution luma plane and a half resolution + // plane of interleaved Cb/Cr. The BT.601 limited range coefficients below + // match what the decoders in this pipeline produce for camera video. + const char *kFragmentShader = + "varying highp vec2 uv;\n" + "uniform sampler2D planeY;\n" + "uniform sampler2D planeUV;\n" + "void main() {\n" + " highp float y = texture2D(planeY, uv).r;\n" + " highp vec2 c = texture2D(planeUV, uv).rg;\n" + " y = 1.1643 * (y - 0.0625);\n" + " highp float u = c.r - 0.5;\n" + " highp float v = c.g - 0.5;\n" + " highp float r = y + 1.5958 * v;\n" + " highp float g = y - 0.39173 * u - 0.81290 * v;\n" + " highp float b = y + 2.017 * u;\n" + " gl_FragColor = vec4(r, g, b, 1.0);\n" + "}\n"; +} + +VaapiVideoWidget::VaapiVideoWidget(QWidget *parent) + : QOpenGLWidget(parent), m_program(nullptr), m_numFds(0), + m_frameWidth(0), m_frameHeight(0), m_working(true), m_reportedFailure(false), + m_describedSurface(false) +{ + m_textures[0] = m_textures[1] = 0; + m_images[0] = m_images[1] = EGL_NO_IMAGE_KHR; + for (int i = 0; i < 4; i++) + m_dmabufFds[i] = -1; +} + +VaapiVideoWidget::~VaapiVideoWidget() +{ + // The GL objects belong to this widget's context, so it has to be current + // while they are destroyed. + makeCurrent(); + releaseImports(); + if (m_textures[0]) + glDeleteTextures(2, m_textures); + delete m_program; + doneCurrent(); +} + +void VaapiVideoWidget::setFrame(AVFramePtr frame) +{ + if (!m_working) + return; + + m_frame = frame; + update(); +} + +void VaapiVideoWidget::clearFrame() +{ + m_frame.clear(); + update(); +} + +void VaapiVideoWidget::initializeGL() +{ + initializeOpenGLFunctions(); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glDisable(GL_DEPTH_TEST); + + if (!resolveEglExtensions()) + { + m_working = false; + if (!m_reportedFailure) + { + m_reportedFailure = true; + QString msg = "EGL does not offer the extensions needed to draw GPU frames"; + qWarning() << msg; + emit renderingFailed(msg); + } + return; + } + + m_program = new QOpenGLShaderProgram(this); + if (!m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, kVertexShader) || + !m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, kFragmentShader) || + !m_program->link()) + { + m_working = false; + if (!m_reportedFailure) + { + m_reportedFailure = true; + QString msg = QString("Could not build the video shader: %1").arg(m_program->log()); + qWarning() << msg; + emit renderingFailed(msg); + } + return; + } + + glGenTextures(2, m_textures); + for (int i = 0; i < 2; i++) + { + glBindTexture(GL_TEXTURE_2D, m_textures[i]); + 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); + } + glBindTexture(GL_TEXTURE_2D, 0); +} + +void VaapiVideoWidget::releaseImports() +{ + for (int i = 0; i < 2; i++) + { + if (m_images[i] != EGL_NO_IMAGE_KHR) + { + if (g_eglDestroyImageKHR) + g_eglDestroyImageKHR(eglGetCurrentDisplay(), (EGLImageKHR)m_images[i]); + m_images[i] = EGL_NO_IMAGE_KHR; + } + } + + for (int i = 0; i < m_numFds; i++) + { + if (m_dmabufFds[i] >= 0) + { + ::close(m_dmabufFds[i]); + m_dmabufFds[i] = -1; + } + } + m_numFds = 0; +} + +bool VaapiVideoWidget::importFrame() +{ + VADisplay va_display = VaapiContext::display(); + if (!va_display || !m_frame) + return false; + + // The surface id lives in data[3] for AV_PIX_FMT_VAAPI frames. + VASurfaceID surface = (VASurfaceID)(uintptr_t)m_frame->data[3]; + + // Everything still queued on the surface has to have finished before its + // memory can be handed to another API. + if (vaSyncSurface(va_display, surface) != VA_STATUS_SUCCESS) + return false; + + VADRMPRIMESurfaceDescriptor desc; + memset(&desc, 0, sizeof desc); + + VAStatus st = vaExportSurfaceHandle(va_display, surface, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, + &desc); + if (st != VA_STATUS_SUCCESS) + return false; + + // Held so they can be closed again after drawing; the export hands over + // ownership of every one of them. + m_numFds = desc.num_objects > 4 ? 4 : (int)desc.num_objects; + for (int i = 0; i < m_numFds; i++) + m_dmabufFds[i] = desc.objects[i].fd; + + m_frameWidth = desc.width; + m_frameHeight = desc.height; + + // Logged once: how the driver actually laid the surface out is the first + // thing worth knowing when the picture comes out wrong. + if (!m_describedSurface) + { + m_describedSurface = true; + + QString layers; + for (uint32_t i = 0; i < desc.num_layers; i++) + { + uint32_t object = desc.layers[i].object_index[0]; + layers += QString(" layer%1[fourcc=0x%2 offset=%3 pitch=%4 modifier=0x%5]") + .arg(i) + .arg(desc.layers[i].drm_format, 0, 16) + .arg(desc.layers[i].offset[0]) + .arg(desc.layers[i].pitch[0]) + .arg(desc.objects[object].drm_format_modifier, 0, 16); + } + + qDebug() << QString("VAAPI surface for display: %1x%2 fourcc=0x%3 objects=%4 layers=%5%6") + .arg(desc.width).arg(desc.height) + .arg(desc.fourcc, 0, 16) + .arg(desc.num_objects).arg(desc.num_layers) + .arg(layers); + } + + // Two layers for NV12: the luma plane, then the interleaved chroma plane + // at half size in both directions. + if (desc.num_layers < 2) + { + releaseImports(); + return false; + } + + EGLDisplay egl_display = eglGetCurrentDisplay(); + + for (int i = 0; i < 2; i++) + { + uint32_t object = desc.layers[i].object_index[0]; + int planeWidth = (i == 0) ? desc.width : desc.width / 2; + int planeHeight = (i == 0) ? desc.height : desc.height / 2; + uint64_t modifier = desc.objects[object].drm_format_modifier; + + EGLint attribs[16]; + int n = 0; + + attribs[n++] = EGL_WIDTH; attribs[n++] = planeWidth; + attribs[n++] = EGL_HEIGHT; attribs[n++] = planeHeight; + attribs[n++] = EGL_LINUX_DRM_FOURCC_EXT; attribs[n++] = (EGLint)desc.layers[i].drm_format; + attribs[n++] = EGL_DMA_BUF_PLANE0_FD_EXT; attribs[n++] = desc.objects[object].fd; + attribs[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; attribs[n++] = (EGLint)desc.layers[i].offset[0]; + attribs[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; attribs[n++] = (EGLint)desc.layers[i].pitch[0]; + + // Without this the import is assumed to be a plain linear buffer. GPUs + // hand out tiled surfaces instead, and reading one as linear is what + // produces a picture in bands with the colour gone - so the modifier + // the driver reported has to be passed along whenever it has one. + if (modifier != DRM_FORMAT_MOD_INVALID) + { + attribs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; + attribs[n++] = (EGLint)(modifier & 0xFFFFFFFF); + attribs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; + attribs[n++] = (EGLint)(modifier >> 32); + } + + attribs[n++] = EGL_NONE; + + m_images[i] = g_eglCreateImageKHR(egl_display, EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, nullptr, attribs); + if (m_images[i] == EGL_NO_IMAGE_KHR) + { + releaseImports(); + return false; + } + + glBindTexture(GL_TEXTURE_2D, m_textures[i]); + g_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)m_images[i]); + } + + glBindTexture(GL_TEXTURE_2D, 0); + + return true; +} + +void VaapiVideoWidget::paintGL() +{ + glClear(GL_COLOR_BUFFER_BIT); + + if (!m_working || !m_program || !m_frame) + return; + + if (!importFrame()) + { + m_working = false; + if (!m_reportedFailure) + { + m_reportedFailure = true; + QString msg = "Could not hand the decoded GPU frame to OpenGL for drawing"; + qWarning() << msg; + emit renderingFailed(msg); + } + return; + } + + // Letterbox rather than stretch, matching what the software path does + // with Qt::KeepAspectRatio. + float widgetAspect = float(width()) / float(height() ? height() : 1); + float frameAspect = float(m_frameWidth) / float(m_frameHeight ? m_frameHeight : 1); + float sx = 1.0f, sy = 1.0f; + + if (frameAspect > widgetAspect) + sy = widgetAspect / frameAspect; + else + sx = frameAspect / widgetAspect; + + const GLfloat vertices[] = + { + -sx, -sy, + sx, -sy, + -sx, sy, + sx, sy + }; + // Flipped vertically: video rows run top to bottom, OpenGL's origin is at + // the bottom left. + const GLfloat texcoords[] = + { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f + }; + + m_program->bind(); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_textures[0]); + m_program->setUniformValue("planeY", 0); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, m_textures[1]); + m_program->setUniformValue("planeUV", 1); + + int vertexLocation = m_program->attributeLocation("vertex"); + int texcoordLocation = m_program->attributeLocation("texcoord"); + + m_program->enableAttributeArray(vertexLocation); + m_program->enableAttributeArray(texcoordLocation); + m_program->setAttributeArray(vertexLocation, vertices, 2); + m_program->setAttributeArray(texcoordLocation, texcoords, 2); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + m_program->disableAttributeArray(vertexLocation); + m_program->disableAttributeArray(texcoordLocation); + m_program->release(); + + glActiveTexture(GL_TEXTURE0); + + // Done with this frame's handles - holding on to them would leak a set of + // file descriptors for every frame drawn. + releaseImports(); +} + +void VaapiVideoWidget::resizeGL(int w, int h) +{ + glViewport(0, 0, w, h); +} + +#endif // HAVE_VAAPI |
