#include "vaapiplayer.hpp" #ifdef ZHAVE_FFMPEG #define GL_GLEXT_PROTOTYPES #include #include #include #ifdef ZHAVE_VAAPI #include extern "C" { #include #include #include } #ifndef DRM_FORMAT_MOD_INVALID #define DRM_FORMAT_MOD_INVALID 0xffffffffffffffffULL #endif #endif #include #include #include #include Q_LOGGING_CATEGORY(vaapiLog, "ZShell.Internal.wallpaper") namespace ZShell::wallpaper { namespace { enum AVPixelFormat chooseFormat( AVCodecContext* ctx, const AVPixelFormat* pix_fmts) { (void)ctx; for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p) if (*p == AV_PIX_FMT_VAAPI) return AV_PIX_FMT_VAAPI; return pix_fmts[0]; } #ifdef ZHAVE_VAAPI using GLEGLImageTargetProc = void (*)(GLenum, EGLImageKHR); GLEGLImageTargetProc glEGLImageTarget() { static GLEGLImageTargetProc fn = reinterpret_cast( eglGetProcAddress("glEGLImageTargetTexture2DOES")); return fn; } // eglCreateImageKHR/eglDestroyImageKHR are EGL extension entry points. Fetch // them through eglGetProcAddress rather than linking them directly, so the // shared library does not carry a hard undefined reference to symbols libEGL // may not export on every platform. using PfnCreateImageKHR = EGLImageKHR (*)( EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*); using PfnDestroyImageKHR = EGLBoolean (*)(EGLDisplay, EGLImageKHR); PfnCreateImageKHR eglCreateImageProc() { static PfnCreateImageKHR fn = reinterpret_cast( eglGetProcAddress("eglCreateImageKHR")); return fn; } PfnDestroyImageKHR eglDestroyImageProc() { static PfnDestroyImageKHR fn = reinterpret_cast( eglGetProcAddress("eglDestroyImageKHR")); return fn; } void destroyEglImage(EGLDisplay dpy, EGLImageKHR image) { if (image == EGL_NO_IMAGE_KHR) return; PfnDestroyImageKHR fn = eglDestroyImageProc(); if (!fn) { static bool warned = false; if (!warned) { warned = true; qCWarning( vaapiLog, "VideoPlayer: eglDestroyImageKHR unavailable, EGL images leak"); } return; } fn(dpy, image); } EGLImageKHR createDmaImage( EGLDisplay dpy, int fd, uint32_t offset, uint32_t pitch, uint32_t fourcc, uint64_t modifier) { PfnCreateImageKHR fn = eglCreateImageProc(); if (!fn) return EGL_NO_IMAGE_KHR; EGLint attrs[16]; int n = 0; attrs[n++] = EGL_LINUX_DRM_FOURCC_EXT; attrs[n++] = EGLint(fourcc); attrs[n++] = EGL_DMA_BUF_PLANE0_FD_EXT; attrs[n++] = fd; attrs[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; attrs[n++] = EGLint(offset); attrs[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; attrs[n++] = EGLint(pitch); if (modifier != DRM_FORMAT_MOD_INVALID) { attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; attrs[n++] = EGLint(modifier & 0xffffffffULL); attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; attrs[n++] = EGLint(modifier >> 32); } attrs[n] = EGL_NONE; EGLImageKHR image = fn(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs); if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR; return image; } #endif } // namespace VaapiPlayer::VaapiPlayer(EGLDisplay display) : m_eglDisplay(display) {} VaapiPlayer::~VaapiPlayer() { stop(); std::lock_guard lock(m_mtx); if (m_pending) { av_frame_unref(m_pending); av_frame_free(&m_pending); } if (m_displayed) { av_frame_unref(m_displayed); av_frame_free(&m_displayed); } if (m_hwDeviceCtx) { av_buffer_unref(&m_hwDeviceCtx); m_hwDeviceCtx = nullptr; } #ifdef ZHAVE_VAAPI if (m_imageY != EGL_NO_IMAGE_KHR) { destroyEglImage(m_eglDisplay, m_imageY); m_imageY = EGL_NO_IMAGE_KHR; } if (m_imageUV != EGL_NO_IMAGE_KHR) { destroyEglImage(m_eglDisplay, m_imageUV); m_imageUV = EGL_NO_IMAGE_KHR; } #endif } bool VaapiPlayer::available() { AVBufferRef* ref = nullptr; if (av_hwdevice_ctx_create( &ref, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) { if (ref) av_buffer_unref(&ref); return false; } av_buffer_unref(&ref); return true; } bool VaapiPlayer::ensureDevice() { if (m_hwDeviceCtx) return true; if (av_hwdevice_ctx_create( &m_hwDeviceCtx, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) < 0) { if (m_hwDeviceCtx) av_buffer_unref(&m_hwDeviceCtx); m_hwDeviceCtx = nullptr; qCWarning(vaapiLog, "VideoPlayer: could not create vaapi hwdevice"); return false; } return true; } bool VaapiPlayer::openDecoder(const QString& path) { if (avformat_open_input( &m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) { qCWarning( vaapiLog, "VideoPlayer: could not open %s", path.toUtf8().constData()); return false; } if (avformat_find_stream_info(m_fmt, nullptr) < 0) { qCWarning(vaapiLog, "VideoPlayer: could not read stream info"); return false; } m_streamIndex = -1; for (unsigned i = 0; i < m_fmt->nb_streams; ++i) if (m_fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { m_streamIndex = int(i); break; } if (m_streamIndex < 0) { qCWarning(vaapiLog, "VideoPlayer: no video stream"); return false; } AVCodecParameters* par = m_fmt->streams[m_streamIndex]->codecpar; const AVCodec* codec = avcodec_find_decoder(par->codec_id); const AVCodec* hwcodec = nullptr; void* it = nullptr; while (const AVCodec* c = av_codec_iterate(&it)) { if (c->id != par->codec_id || !av_codec_is_decoder(c)) continue; for (int i = 0; const AVCodecHWConfig* cfg = avcodec_get_hw_config(c, i); ++i) if (cfg->device_type == AV_HWDEVICE_TYPE_VAAPI && (cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) { hwcodec = c; break; } if (hwcodec) break; } if (hwcodec) codec = hwcodec; if (!codec) { qCWarning( vaapiLog, "VideoPlayer: no decoder for codec id %d", int(par->codec_id)); return false; } m_codec = avcodec_alloc_context3(codec); if (!m_codec) return false; if (avcodec_parameters_to_context(m_codec, par) < 0) { qCWarning(vaapiLog, "VideoPlayer: could not copy codec params"); return false; } if (!ensureDevice()) { return false; } m_codec->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx); m_codec->get_format = chooseFormat; if (avcodec_open2(m_codec, codec, nullptr) < 0) { qCWarning(vaapiLog, "VideoPlayer: could not open codec"); return false; } return true; } void VaapiPlayer::closeDecoder() { if (m_codec) { avcodec_free_context(&m_codec); m_codec = nullptr; } if (m_fmt) { avformat_close_input(&m_fmt); m_fmt = nullptr; } m_streamIndex = -1; } void VaapiPlayer::decodeLoop(const QString& path, uint32_t generation) { if (!m_threadRunning.load(std::memory_order_acquire) || m_generation.load(std::memory_order_acquire) != generation) return; if (!openDecoder(path)) { m_threadRunning.store(false); return; } AVRational fr = m_fmt->streams[m_streamIndex]->avg_frame_rate; if (fr.num <= 0 || fr.den <= 0) fr = m_codec->framerate; if (fr.num <= 0 || fr.den <= 0) fr = AVRational{30, 1}; const int64_t intervalUs = av_rescale_q(1, AVRational{fr.den, fr.num}, AVRational{1, 1000000}); AVFrame* frame = av_frame_alloc(); if (!frame) { m_threadRunning.store(false); return; } AVPacket pkt{}; while (m_threadRunning.load(std::memory_order_acquire) && m_generation.load(std::memory_order_acquire) == generation) { int ret = av_read_frame(m_fmt, &pkt); if (ret == AVERROR_EOF) { if (avformat_seek_file( m_fmt, m_streamIndex, INT64_MIN, 0, INT64_MAX, 0) < 0) { qCWarning(vaapiLog, "VideoPlayer: loop seek failed; stopping"); break; } avcodec_flush_buffers(m_codec); continue; } if (ret < 0) { qCWarning(vaapiLog, "VideoPlayer: read error; stopping"); break; } if (pkt.stream_index != m_streamIndex) { av_packet_unref(&pkt); continue; } if (avcodec_send_packet(m_codec, &pkt) < 0) { av_packet_unref(&pkt); continue; } av_packet_unref(&pkt); while (m_threadRunning.load(std::memory_order_acquire) && m_generation.load(std::memory_order_acquire) == generation && avcodec_receive_frame(m_codec, frame) == 0) { bool wasEmpty = false; { std::lock_guard lock(m_mtx); if (!m_pending) m_pending = av_frame_alloc(); av_frame_unref(m_pending); if (av_frame_ref(m_pending, frame) >= 0) { wasEmpty = !m_haveFrame; m_haveFrame = true; } } if (wasEmpty && m_readyCb) m_readyCb(); if (m_frameCb) m_frameCb(); av_frame_unref(frame); } std::this_thread::sleep_for(std::chrono::microseconds(intervalUs)); } av_frame_free(&frame); closeDecoder(); m_threadRunning.store(false); } bool VaapiPlayer::start(const QString& path) { if (!ensureDevice()) return false; const uint32_t generation = m_generation.fetch_add(1, std::memory_order_acq_rel) + 1; m_threadRunning.store(true); std::thread previous = std::move(m_thread); m_thread = std::thread( [this, path, generation, previous = std::move(previous)]() mutable { if (previous.joinable()) previous.join(); closeDecoder(); decodeLoop(path, generation); }); return true; } void VaapiPlayer::stop() { m_generation.fetch_add(1, std::memory_order_acq_rel); m_threadRunning.store(false); if (m_thread.joinable()) m_thread.join(); closeDecoder(); std::lock_guard lock(m_mtx); if (m_pending) { av_frame_unref(m_pending); m_pending = nullptr; } if (m_displayed) { av_frame_unref(m_displayed); m_displayed = nullptr; } m_haveFrame = false; m_size = QSize(); } void VaapiPlayer::deleteTexture() { #ifdef ZHAVE_VAAPI if (m_imageY != EGL_NO_IMAGE_KHR) { destroyEglImage(m_eglDisplay, m_imageY); m_imageY = EGL_NO_IMAGE_KHR; } if (m_imageUV != EGL_NO_IMAGE_KHR) { destroyEglImage(m_eglDisplay, m_imageUV); m_imageUV = EGL_NO_IMAGE_KHR; } #endif if (m_texY) { glDeleteTextures(1, &m_texY); m_texY = 0; } if (m_texUV) { glDeleteTextures(1, &m_texUV); m_texUV = 0; } m_texSize = QSize(); } bool VaapiPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) { const QSize sz = QSize(int(w), int(h)); if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) return true; if (m_texY) glDeleteTextures(1, &m_texY); if (m_texUV) glDeleteTextures(1, &m_texUV); m_texY = m_texUV = 0; const bool wide = compBytes == 2; auto makeTex = [&](GLuint& tex, GLint internal, GLenum fmt, unsigned tw, unsigned th) { glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D( GL_TEXTURE_2D, 0, internal, GLint(tw), GLint(th), 0, fmt, wide ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE, nullptr); 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); }; makeTex(m_texY, wide ? GL_R16 : GL_R8, GL_RED, w, h); makeTex(m_texUV, wide ? GL_RG16 : GL_RG8, GL_RG, w / 2, h / 2); m_texSize = sz; m_texComp = compBytes; return true; } bool VaapiPlayer::importCurrentFrame() { AVFrame* frame = nullptr; { std::lock_guard lock(m_mtx); if (!m_pending || m_pending->format == AV_PIX_FMT_NONE) return false; frame = m_pending; m_pending = nullptr; } if (frame->format != AV_PIX_FMT_VAAPI || !frame->hw_frames_ctx) { static int logged = 0; if (logged++ < 3) qCWarning( vaapiLog, "VideoPlayer: frame not on GPU (fmt=%s hw_frames=%d)", av_get_pix_fmt_name(AVPixelFormat(frame->format)), frame->hw_frames_ctx != nullptr); av_frame_unref(frame); return false; } const AVHWFramesContext* hf = reinterpret_cast(frame->hw_frames_ctx->data); const AVPixelFormat sw = hf->sw_format; const bool tenBit = sw == AV_PIX_FMT_P010LE || sw == AV_PIX_FMT_P010BE || sw == AV_PIX_FMT_P012LE || sw == AV_PIX_FMT_P016LE; const int compBytes = tenBit ? 2 : 1; const size_t compSize = size_t(compBytes); const unsigned w = unsigned(frame->width); const unsigned h = unsigned(frame->height); if (w == 0 || h == 0) { av_frame_unref(frame); return false; } switch (frame->colorspace) { case AVCOL_SPC_BT470BG: case AVCOL_SPC_SMPTE170M: m_matrix = 0; break; case AVCOL_SPC_BT709: m_matrix = 1; break; default: m_matrix = h >= 720 ? 1 : 0; break; } m_range = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0; m_size = QSize(int(w), int(h)); #ifdef ZHAVE_VAAPI if (importEglImages(frame)) { std::lock_guard lock(m_mtx); if (m_displayed) av_frame_unref(m_displayed); m_displayed = frame; return true; } #endif AVFrame* tmp = av_frame_alloc(); if (!tmp) { av_frame_unref(frame); return false; } if (av_hwframe_transfer_data(tmp, frame, 0) < 0) { av_frame_free(&tmp); av_frame_unref(frame); return false; } size_t pitchY = size_t(tmp->linesize[0]); size_t pitchUV = tmp->linesize[1] > 0 ? size_t(tmp->linesize[1]) : pitchY; if (pitchY == 0) pitchY = size_t(w) * compSize; if (!ensureTextures(w, h, compBytes)) { av_frame_free(&tmp); av_frame_unref(frame); return false; } const GLenum type = compBytes == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE; glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glBindTexture(GL_TEXTURE_2D, m_texY); glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchY / compSize)); glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, GLint(w), GLint(h), GL_RED, type, tmp->data[0]); glBindTexture(GL_TEXTURE_2D, m_texUV); glPixelStorei(GL_UNPACK_ROW_LENGTH, GLint(pitchUV / (compSize * 2))); glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, GLint(w / 2), GLint(h / 2), GL_RG, type, tmp->data[1]); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); av_frame_free(&tmp); { std::lock_guard lock(m_mtx); if (m_displayed) av_frame_unref(m_displayed); m_displayed = frame; } return true; } #ifdef ZHAVE_VAAPI bool VaapiPlayer::importEglImages(AVFrame* frame) { if (m_eglDisplay == EGL_NO_DISPLAY) return false; GLEGLImageTargetProc target = glEGLImageTarget(); if (!target) return false; const AVHWFramesContext* hf = reinterpret_cast(frame->hw_frames_ctx->data); const AVHWDeviceContext* dev = hf->device_ctx; const AVVAAPIDeviceContext* va = reinterpret_cast(dev->hwctx); if (!va->display) return false; const VASurfaceID surface = VASurfaceID(uintptr_t(frame->data[0])); VADRMPRIMESurfaceDescriptor 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) { static int logged = 0; if (logged++ < 3) qCWarning( vaapiLog, "VideoPlayer: vaExportSurfaceHandle failed (%d); CPU fallback", int(st)); return false; } if (desc.num_layers != 2 || desc.num_objects < 2) { for (uint32_t i = 0; i < desc.num_objects; ++i) if (desc.objects[i].fd >= 0) close(desc.objects[i].fd); return false; } const auto bindLayer = [&](uint32_t layer, GLuint tex) -> EGLImageKHR { const auto& L = desc.layers[layer]; if (L.num_planes < 1) return EGL_NO_IMAGE_KHR; const auto& obj = desc.objects[L.object_index[0]]; if (obj.fd < 0) return EGL_NO_IMAGE_KHR; EGLImageKHR image = createDmaImage( m_eglDisplay, obj.fd, L.offset[0], L.pitch[0], L.drm_format, obj.drm_format_modifier); if (image == EGL_NO_IMAGE_KHR) return EGL_NO_IMAGE_KHR; glBindTexture(GL_TEXTURE_2D, tex); target(GL_TEXTURE_2D, image); return image; }; const QSize sz = QSize(int(frame->width), int(frame->height)); if (m_texSize != sz || m_texY == 0) { if (m_imageY != EGL_NO_IMAGE_KHR) { destroyEglImage(m_eglDisplay, m_imageY); m_imageY = EGL_NO_IMAGE_KHR; } if (m_imageUV != EGL_NO_IMAGE_KHR) { destroyEglImage(m_eglDisplay, m_imageUV); m_imageUV = EGL_NO_IMAGE_KHR; } if (m_texY == 0) glGenTextures(1, &m_texY); if (m_texUV == 0) glGenTextures(1, &m_texUV); glBindTexture(GL_TEXTURE_2D, m_texY); 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, m_texUV); 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); m_texSize = sz; } EGLImageKHR imageY = bindLayer(0, m_texY); EGLImageKHR imageUV = bindLayer(1, m_texUV); if (imageY == EGL_NO_IMAGE_KHR || imageUV == EGL_NO_IMAGE_KHR) { destroyEglImage(m_eglDisplay, imageY); destroyEglImage(m_eglDisplay, imageUV); for (uint32_t i = 0; i < desc.num_objects; ++i) if (desc.objects[i].fd >= 0) close(desc.objects[i].fd); static int logged = 0; if (logged++ < 3) qCWarning( vaapiLog, "VideoPlayer: EGLImage import failed; CPU fallback"); return false; } if (m_imageY != EGL_NO_IMAGE_KHR) destroyEglImage(m_eglDisplay, m_imageY); if (m_imageUV != EGL_NO_IMAGE_KHR) destroyEglImage(m_eglDisplay, m_imageUV); m_imageY = imageY; m_imageUV = imageUV; for (uint32_t i = 0; i < desc.num_objects; ++i) if (desc.objects[i].fd >= 0) close(desc.objects[i].fd); return true; } #endif } // namespace ZShell::wallpaper #endif