diff --git a/CMakeLists.txt b/CMakeLists.txt index 750e143441..cbbd287afc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,9 @@ find_package(Qt6 6.4 REQUIRED Widgets Xml ) +if(NOT APPLE) + find_package(Qt6 6.4 REQUIRED GuiPrivate) +endif() if(UNIX AND NOT APPLE) find_package(Qt6 6.4 REQUIRED COMPONENTS DBus) # X11 for WindowPicker (Linux/X11) @@ -66,6 +69,9 @@ endif() add_subdirectory(CuteLogger) add_subdirectory(src) add_subdirectory(translations) +if(UNIX AND NOT APPLE) + add_subdirectory(MinimalMediaBackend) +endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/MinimalMediaBackend/CMakeLists.txt b/MinimalMediaBackend/CMakeLists.txt new file mode 100644 index 0000000000..f9498d5376 --- /dev/null +++ b/MinimalMediaBackend/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.12...3.31) + +project(MinimalMediaBackend) + +find_package(Qt6 REQUIRED COMPONENTS Core Multimedia) +find_package(Qt6MultimediaPrivate REQUIRED) + +qt_add_plugin(minimalmediaplugin + CLASS_NAME MinimalMediaPlugin + PLUGIN_TYPE multimedia + OUTPUT_TARGETS minimalmediaplugin_targets + minimalmediaplugin.cpp +) + +target_link_libraries(minimalmediaplugin PRIVATE + Qt6::Core + Qt6::Multimedia + Qt6::MultimediaPrivate +) diff --git a/MinimalMediaBackend/minimalmediaplugin.cpp b/MinimalMediaBackend/minimalmediaplugin.cpp new file mode 100644 index 0000000000..a0bdfc559d --- /dev/null +++ b/MinimalMediaBackend/minimalmediaplugin.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +// Minimal QPlatformVideoSink — the base class does all the work +class MinimalVideoSink : public QPlatformVideoSink +{ + Q_OBJECT +public: + explicit MinimalVideoSink(QVideoSink *sink) + : QPlatformVideoSink(sink) + {} +}; + +// Minimal integration — only creates video sinks +class MinimalMediaIntegration : public QPlatformMediaIntegration +{ +public: + MinimalMediaIntegration() + : QPlatformMediaIntegration(QLatin1String("minimal")) + {} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + q23::expected createVideoSink(QVideoSink *sink) override + { + return new MinimalVideoSink(sink); + } +#else + QMaybe createVideoSink(QVideoSink *sink) override + { + return new MinimalVideoSink(sink); + } +#endif +}; + +// Plugin entry point +class MinimalMediaPlugin : public QPlatformMediaPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QPlatformMediaPlugin_iid FILE "minimalmediaplugin.json") + +public: + MinimalMediaPlugin(QObject *parent = nullptr) + : QPlatformMediaPlugin(parent) + {} + + QPlatformMediaIntegration *create(const QString &key) override + { + if (key == QLatin1String("minimal")) + return new MinimalMediaIntegration; + return nullptr; + } +}; + +QT_END_NAMESPACE + +#include "minimalmediaplugin.moc" diff --git a/MinimalMediaBackend/minimalmediaplugin.json b/MinimalMediaBackend/minimalmediaplugin.json new file mode 100644 index 0000000000..71ec6ad265 --- /dev/null +++ b/MinimalMediaBackend/minimalmediaplugin.json @@ -0,0 +1,3 @@ +{ + "Keys": ["minimal"] +} diff --git a/scripts/build-shotcut-msys2.sh b/scripts/build-shotcut-msys2.sh index 74a15d9410..b1124c79dd 100755 --- a/scripts/build-shotcut-msys2.sh +++ b/scripts/build-shotcut-msys2.sh @@ -1240,7 +1240,7 @@ function deploy log Copying some libs from Qt if [ "$DEBUG_BUILD" != "1" -o "$SDK" = "1" ]; then - cmd cp -p "$QTDIR"/bin/Qt6{Charts,Concurrent,Core,Core5Compat,Gui,Multimedia,Network,OpenGL,OpenGLWidgets,Qml,QmlMeta,QmlModels,QmlWorkerScript,Quick,QuickControls2*,QuickDialogs2,QuickDialogs2QuickImpl,QuickDialogs2Utils,QuickLayouts,QuickTemplates2,QuickWidgets,Sql,Svg,SvgWidgets,UiTools,WebSockets,Widgets,Xml}.dll . + cmd cp -p "$QTDIR"/bin/Qt6{Charts,Concurrent,Core,Core5Compat,Gui,Multimedia,MultimediaQuick,Network,OpenGL,OpenGLWidgets,Qml,QmlMeta,QmlModels,QmlWorkerScript,Quick,QuickControls2*,QuickDialogs2,QuickDialogs2QuickImpl,QuickDialogs2Utils,QuickLayouts,QuickShapes,QuickTemplates2,QuickWidgets,Sql,Svg,SvgWidgets,UiTools,WebSockets,Widgets,Xml}.dll . fi if [ "$ENABLE_GLAXNIMATE" = "1" ]; then diff --git a/scripts/build-shotcut.sh b/scripts/build-shotcut.sh index 8876e8d92d..7a6ef971b7 100755 --- a/scripts/build-shotcut.sh +++ b/scripts/build-shotcut.sh @@ -1202,13 +1202,15 @@ function install_shotcut_linux { cmd install -p -c COPYING "$FINAL_INSTALL_DIR" cmd install -p -c "$QTDIR"/translations/qt_*.qm "$FINAL_INSTALL_DIR"/share/shotcut/translations cmd install -p -c "$QTDIR"/translations/qtbase_*.qm "$FINAL_INSTALL_DIR"/share/shotcut/translations - cmd install -p -c "$QTDIR"/lib/libQt6{Charts,Concurrent,Core,Core5Compat,DBus,Gui,LabsFolderListModel,Multimedia,Network,OpenGL,OpenGLWidgets,Qml,QmlMeta,QmlModels,QmlWorkerScript,Quick,QuickControls2*,QuickDialogs2,QuickDialogs2QuickImpl,QuickDialogs2Utils,QuickLayouts,QuickTemplates2,QuickWidgets,Sql,Svg,SvgWidgets,UiTools,WaylandClient,WaylandEglClientHwIntegration,WebSockets,Widgets,Xml,X11Extras,XcbQpa}.so.6 "$FINAL_INSTALL_DIR"/lib + cmd install -p -c "$QTDIR"/lib/libQt6{Charts,Concurrent,Core,Core5Compat,DBus,Gui,LabsFolderListModel,Multimedia,MultimediaQuick,Network,OpenGL,OpenGLWidgets,Qml,QmlMeta,QmlModels,QmlWorkerScript,Quick,QuickControls2*,QuickDialogs2,QuickDialogs2QuickImpl,QuickDialogs2Utils,QuickLayouts,QuickShapes,QuickTemplates2,QuickWidgets,Sql,Svg,SvgWidgets,UiTools,WaylandClient,WaylandEglClientHwIntegration,WebSockets,Widgets,Xml,X11Extras,XcbQpa}.so.6 "$FINAL_INSTALL_DIR"/lib cmd install -p -c "$QTDIR"/lib/lib{icudata,icui18n,icuuc}.so* "$FINAL_INSTALL_DIR"/lib cmd install -d "$FINAL_INSTALL_DIR"/lib/qt6/sqldrivers cmd cp -a "$QTDIR"/plugins/{egldeviceintegrations,generic,iconengines,imageformats,multimedia,platforminputcontexts,platforms,platformthemes,tls,wayland-decoration-client,wayland-graphics-integration-client,wayland-shell-integration,xcbglintegrations} "$FINAL_INSTALL_DIR"/lib/qt6 cmd cp -p "$QTDIR"/plugins/sqldrivers/libqsqlite.so "$FINAL_INSTALL_DIR"/lib/qt6/sqldrivers cmd install -d "$FINAL_INSTALL_DIR"/lib/qml cmd cp -a "$QTDIR"/qml/{Qt,QtCore,QtQml,QtQuick} "$FINAL_INSTALL_DIR"/lib/qml + cmd install -d "$FINAL_INSTALL_DIR"/lib/qt6/multimedia + cmd cp -p MinimalMediaBackend/libminimalmediaplugin.so "$FINAL_INSTALL_DIR"/lib/qt6/multimedia } function build_vmaf_darwin { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1662ae669f..bc4611179d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -146,6 +146,7 @@ add_executable(shotcut WIN32 MACOSX_BUNDLE transportcontrol.h util.cpp util.h videowidget.cpp videowidget.h + hdrpreviewwindow.cpp hdrpreviewwindow.h widgets/alsawidget.cpp widgets/alsawidget.h widgets/alsawidget.ui widgets/audiometerwidget.cpp widgets/audiometerwidget.h @@ -266,12 +267,29 @@ add_custom_target(OTHER_FILES ../scripts/staple.sh ) +# Compile HDR gain shader (used by HdrPreview.qml ShaderEffect) +find_program(QSB_EXECUTABLE qsb HINTS + "${Qt6_DIR}/../../../bin" "${Qt6Core_DIR}/../../../bin") +if(QSB_EXECUTABLE) + set(HDR_GAIN_FRAG ${CMAKE_CURRENT_SOURCE_DIR}/qml/views/hdr_gain.frag) + set(HDR_GAIN_QSB ${CMAKE_CURRENT_SOURCE_DIR}/qml/views/hdr_gain.frag.qsb) + add_custom_command( + OUTPUT ${HDR_GAIN_QSB} + COMMAND ${QSB_EXECUTABLE} --glsl "100 es,120,150" --hlsl 50 --msl 12 -o ${HDR_GAIN_QSB} ${HDR_GAIN_FRAG} + DEPENDS ${HDR_GAIN_FRAG} + COMMENT "Compiling HDR gain shader" + ) + add_custom_target(hdr_shaders ALL DEPENDS ${HDR_GAIN_QSB}) + add_dependencies(shotcut hdr_shaders) +endif() + target_link_libraries(shotcut PRIVATE CuteLogger PkgConfig::mlt++ PkgConfig::FFTW Qt6::Charts + Qt6::GuiPrivate Qt6::Multimedia Qt6::Network Qt6::OpenGL @@ -310,9 +328,7 @@ if(WIN32) # Windows integration features target_sources(shotcut PRIVATE windowstools.cpp windowstools.h) - target_sources(shotcut PRIVATE widgets/d3dvideowidget.h widgets/d3dvideowidget.cpp) - target_sources(shotcut PRIVATE widgets/openglvideowidget.h widgets/openglvideowidget.cpp) - target_link_libraries(shotcut PRIVATE d3d11 d3dcompiler ole32) + target_link_libraries(shotcut PRIVATE ole32) # Runtime exception handler for debug only if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") @@ -331,13 +347,10 @@ if(WIN32) install(DIRECTORY qml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/shotcut/) install(DIRECTORY ${CMAKE_SOURCE_DIR}/filter-sets DESTINATION ${CMAKE_INSTALL_PREFIX}/share/shotcut/) install(DIRECTORY ${CMAKE_SOURCE_DIR}/voices DESTINATION ${CMAKE_INSTALL_PREFIX}/share/shotcut/) -else() - target_sources(shotcut PRIVATE widgets/openglvideowidget.h widgets/openglvideowidget.cpp) endif() if(APPLE) - target_sources(shotcut PRIVATE macos.mm macos.h - widgets/metalvideowidget.h widgets/metalvideowidget.mm) + target_sources(shotcut PRIVATE macos.mm macos.h) set_target_properties(shotcut PROPERTIES OUTPUT_NAME "Shotcut" MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/packaging/macos/Info.plist.in) diff --git a/src/hdrpreviewwindow.cpp b/src/hdrpreviewwindow.cpp new file mode 100644 index 0000000000..8b18ef097c --- /dev/null +++ b/src/hdrpreviewwindow.cpp @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#include "hdrpreviewwindow.h" + +#include "actions.h" +#include "mainwindow.h" +#include "mltcontroller.h" +#include "player.h" +#include "qmltypes/qmlutilities.h" + +#ifdef Q_OS_WIN +#include +#endif + +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_MACOS +#include "macos.h" +#endif + +// HLG OETF: scene-referred linear to HLG electrical signal. +// See ITU-R BT.2100-2. +static float hlgOetf(float linear) +{ + const float a = 0.17883277f; + const float b = 0.28466892f; + const float c = 0.55991073f; + if (linear < 1.0f / 12.0f) + return sqrtf(3.0f * linear); + return a * logf(12.0f * linear - b) + c; +} + +static QString formatTimecode(int frames, double fps) +{ + if (frames < 0 || fps <= 0.0) + return QStringLiteral("00:00"); + const int totalSec = static_cast(frames / fps); + const int h = totalSec / 3600; + const int m = (totalSec % 3600) / 60; + const int s = totalSec % 60; + if (h > 0) + return QString("%1:%2:%3").arg(h).arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0')); + return QString("%1:%2").arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0')); +} + +HdrPreviewWindow::HdrPreviewWindow(QWindow *parent) + : QQuickView(QmlUtilities::sharedEngine(), parent) +{ + setTitle(tr("HDR Preview")); + setResizeMode(QQuickView::SizeRootObjectToView); + setColor(Qt::black); + + // Request HDR swapchain via the internal Qt scene graph property. + // Qt's video fragment shaders only select the linear HDR output path + // (nv12_bt2020_hlg_linear.frag) for HDRExtendedSrgbLinear, not for + // HDRExtendedDisplayP3Linear. So we must use "scrgb" on all platforms. + setProperty("_qt_sg_hdr_format", QByteArrayLiteral("scrgb")); + + rootContext()->setContextProperty("hdrWindow", this); + + QDir qmlDir = QmlUtilities::qmlDir(); + setSource(QUrl::fromLocalFile(qmlDir.filePath("views/HdrPreview.qml"))); + + resize(960, 540); + + connect(this, &QWindow::windowStateChanged, this, [this]() { emit fullScreenChanged(); }); + +#ifdef Q_OS_MACOS + // Override NSScreen.maximumExtendedDynamicRangeColorComponentValue so that + // Qt's video shader outputs > 1.0 values (HDR) on the first frame, which + // then causes macOS to allocate real EDR headroom. + macosOverrideEdrHeadroom(true); + + // Monitor EDR headroom every second for the first 30 seconds. + connect(&m_edrTimer, &QTimer::timeout, this, &HdrPreviewWindow::checkEdrHeadroom); + m_edrTimer.start(1000); +#endif +} + +HdrPreviewWindow::~HdrPreviewWindow() +{ +#ifdef Q_OS_MACOS + macosOverrideEdrHeadroom(false); +#endif +} + +void HdrPreviewWindow::setVideoSink(QVideoSink *sink) +{ + m_videoSink = sink; +} + +void HdrPreviewWindow::pushFrame(const QVideoFrame &frame) +{ + if (m_videoSink && isVisible()) { + if (!m_loggedSwapChain) { + m_loggedSwapChain = true; + auto *sc = swapChain(); + if (sc) { + qDebug() << "HDR Preview: swapChain format =" << sc->format() + << "hdrInfo =" << sc->hdrInfo(); + } else { + qDebug() << "HDR Preview: swapChain() returned nullptr!"; + } + qDebug() << "HDR Preview frame: pixelFormat =" << frame.surfaceFormat().pixelFormat() + << "colorTransfer =" << frame.surfaceFormat().colorTransfer() + << "maxLuminance =" << frame.surfaceFormat().maxLuminance(); +#ifdef Q_OS_MACOS + auto wid = winId(); + qDebug() << "HDR Preview EDR:" + << "current =" << macosCurrentEdrHeadroom(wid) + << "potential =" << macosPotentialEdrHeadroom(wid) + << "reference =" << macosReferenceEdrHeadroom(wid); +#endif + } + updateHdrGain(); + m_videoSink->setVideoFrame(frame); + + // Track playback position from frame timestamp + const qint64 pts = frame.startTime(); // microseconds + const double fps = MLT.profile().fps(); + if (pts >= 0 && fps > 0.0) { + const int frameNum = qRound(pts * fps / 1000000.0); + if (m_videoPosition != frameNum) { + m_videoPosition = frameNum; + emit videoPositionChanged(); + } + } + // Track duration from the active producer + if (auto *prod = MLT.producer()) { + const int len = qMax(0, prod->get_length() - 1); + if (m_videoDuration != len) { + m_videoDuration = len; + emit videoDurationChanged(); + } + } + } +} + +void HdrPreviewWindow::triggerPlayPause() +{ + Actions["playerPlayPauseAction"]->trigger(); +} + +void HdrPreviewWindow::triggerRewind() +{ + Actions["playerRewindAction"]->trigger(); +} + +void HdrPreviewWindow::triggerFastForward() +{ + Actions["playerFastForwardAction"]->trigger(); +} + +void HdrPreviewWindow::toggleFullScreen() +{ + if (windowStates() & Qt::WindowFullScreen) { + setWindowStates(Qt::WindowNoState); + if (m_normalGeometry.isValid()) + setGeometry(m_normalGeometry); + } else { + m_normalGeometry = geometry(); + showFullScreen(); + } +} + +QString HdrPreviewWindow::positionText() const +{ + return formatTimecode(m_videoPosition, MLT.profile().fps()); +} + +QString HdrPreviewWindow::durationText() const +{ + return formatTimecode(m_videoDuration, MLT.profile().fps()); +} + +void HdrPreviewWindow::seekToFrame(int frame) +{ + MAIN.player()->seek(frame); +} + +void HdrPreviewWindow::setPlaying(bool playing) +{ + if (m_isPlaying != playing) { + m_isPlaying = playing; + emit playingChanged(); + } +} + +bool HdrPreviewWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) +{ +#ifdef Q_OS_WIN + if (eventType == "windows_generic_MSG") { + MSG *msg = static_cast(message); + if (msg->message == WM_SIZING && !(windowStates() & Qt::WindowFullScreen)) { + const int darNum = MLT.profile().display_aspect_num(); + const int darDen = MLT.profile().display_aspect_den(); + if (darNum > 0 && darDen > 0) { + RECT *r = reinterpret_cast(msg->lParam); + // Measure the actual frame overhead from the live window state. + // AdjustWindowRectEx under-reports the DWM extended frame on + // Windows 10/11, leading to a wrong client-area AR calculation. + RECT curWin, curClient; + GetWindowRect(msg->hwnd, &curWin); + GetClientRect(msg->hwnd, &curClient); + const int fw = (curWin.right - curWin.left) - (curClient.right - curClient.left); + const int fh = (curWin.bottom - curWin.top) - (curClient.bottom - curClient.top); + const int clientW = (r->right - r->left) - fw; + const int clientH = (r->bottom - r->top) - fh; + const bool heightPrimary = (msg->wParam == WMSZ_TOP || msg->wParam == WMSZ_BOTTOM); + if (heightPrimary) { + // Height drives: adjust width from the right + r->right = r->left + qRound((double) clientH * darNum / darDen) + fw; + } else { + // Width drives: adjust height + const int newH = qRound((double) clientW * darDen / darNum) + fh; + const bool fromTop = (msg->wParam == WMSZ_TOPLEFT + || msg->wParam == WMSZ_TOPRIGHT); + if (fromTop) + r->top = r->bottom - newH; + else + r->bottom = r->top + newH; + } + *result = TRUE; + return true; + } + } + } +#endif + return QQuickView::nativeEvent(eventType, message, result); +} + +void HdrPreviewWindow::resizeEvent(QResizeEvent *event) +{ + QQuickView::resizeEvent(event); +#ifndef Q_OS_WIN + // On Windows, WM_SIZING handles AR constraining smoothly. + // On other platforms, snap to the correct AR after each resize. + if (windowStates() & Qt::WindowFullScreen) + return; + const QSize newSize = event->size(); + const QSize oldSize = event->oldSize(); + if (!oldSize.isValid()) + return; + const int darNum = MLT.profile().display_aspect_num(); + const int darDen = MLT.profile().display_aspect_den(); + if (darNum <= 0 || darDen <= 0) + return; + // Infer which axis the user is dragging by which changed proportionally more + const double wChange = qAbs((double) (newSize.width() - oldSize.width()) + / qMax(oldSize.width(), 1)); + const double hChange = qAbs((double) (newSize.height() - oldSize.height()) + / qMax(oldSize.height(), 1)); + int targetW, targetH; + if (wChange >= hChange) { + targetW = newSize.width(); + targetH = qRound((double) targetW * darDen / darNum); + } else { + targetH = newSize.height(); + targetW = qRound((double) targetH * darNum / darDen); + } + if (targetW != newSize.width() || targetH != newSize.height()) { + // Defer to avoid recursion + QTimer::singleShot(0, this, [this, targetW, targetH]() { + if (!(windowStates() & Qt::WindowFullScreen)) + resize(targetW, targetH); + }); + } +#endif +} + +void HdrPreviewWindow::keyPressEvent(QKeyEvent *event) +{ + // Forward to MainWindow for J/K/L transport handling + event->setAccepted(false); + MAIN.keyPressEvent(event); + if (event->isAccepted()) + return; + + // Match QAction shortcuts since this window is outside MainWindow's hierarchy + QKeySequence keySeq(event->keyCombination()); + for (const auto &key : Actions.keys()) { + QAction *action = Actions[key]; + if (action && action->isEnabled()) { + for (const auto &shortcut : action->shortcuts()) { + if (shortcut.matches(keySeq) == QKeySequence::ExactMatch) { + action->trigger(); + event->accept(); + return; + } + } + } + } +} + +void HdrPreviewWindow::keyReleaseEvent(QKeyEvent *event) +{ + event->setAccepted(false); + MAIN.keyReleaseEvent(event); + if (!event->isAccepted()) + QQuickView::keyReleaseEvent(event); +} + +void HdrPreviewWindow::setHlg(bool isHlg) +{ + if (m_isHlg != isHlg) { + m_isHlg = isHlg; + if (!m_isHlg && !qFuzzyCompare(m_hdrGain, 1.0f)) { + m_hdrGain = 1.0f; + emit hdrGainChanged(); + } + } +} + +void HdrPreviewWindow::updateHdrGain() +{ + if (!m_isHlg) + return; + + auto *sc = swapChain(); + if (!sc || sc->format() != QRhiSwapChain::HDRExtendedSrgbLinear) + return; + + auto info = sc->hdrInfo(); + float maxNits = 100.0f; + if (info.limitsType == QRhiSwapChainHdrInfo::ColorComponentValue) + maxNits = 100.0f * info.limits.colorComponentValue.maxColorComponentValue; + else if (info.limitsType == QRhiSwapChainHdrInfo::LuminanceInNits) + maxNits = info.limits.luminanceInNits.maxLuminance; + + float displayMaxLinear = maxNits / 100.0f; + if (displayMaxLinear <= 1.0f) + return; + + // Qt's HLG shader has a bug: maxLum is HLG-encoded (via hlgOetf) but used + // as a linear multiplier in the OOTF. The shader uniform is set as: + // maxLum = hlgOetf(maxNits / 100) + // but it should be the linear value (maxNits / 100). Compensate by + // multiplying the rendered output by the ratio of the correct linear + // value to the HLG-encoded one. + float newGain = displayMaxLinear / hlgOetf(displayMaxLinear); + if (!qFuzzyCompare(newGain, m_hdrGain)) { + m_hdrGain = newGain; + qDebug() << "HDR Preview: gain =" << m_hdrGain << "(maxNits =" << maxNits << ")"; + emit hdrGainChanged(); + } +} + +void HdrPreviewWindow::checkEdrHeadroom() +{ +#ifdef Q_OS_MACOS + float headroom = macosCurrentEdrHeadroom(winId()); + if (headroom != m_lastLoggedHeadroom) { + m_lastLoggedHeadroom = headroom; + auto *sc = swapChain(); + qDebug() << "HDR Preview: EDR headroom =" << headroom + << "(swapChain hdrInfo =" << (sc ? sc->hdrInfo() : QRhiSwapChainHdrInfo()) << ")"; + } + if (++m_edrCheckCount >= 30) + m_edrTimer.stop(); +#endif +} diff --git a/src/hdrpreviewwindow.h b/src/hdrpreviewwindow.h new file mode 100644 index 0000000000..4f0862d188 --- /dev/null +++ b/src/hdrpreviewwindow.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +#ifndef HDRPREVIEWWINDOW_H +#define HDRPREVIEWWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class HdrPreviewWindow : public QQuickView +{ + Q_OBJECT + Q_PROPERTY(float hdrGain READ hdrGain NOTIFY hdrGainChanged) + Q_PROPERTY(bool playing READ isPlaying NOTIFY playingChanged) + Q_PROPERTY(bool fullScreen READ isFullScreen NOTIFY fullScreenChanged) + Q_PROPERTY(int videoPosition READ videoPosition NOTIFY videoPositionChanged) + Q_PROPERTY(int videoDuration READ videoDuration NOTIFY videoDurationChanged) + Q_PROPERTY(QString positionText READ positionText NOTIFY videoPositionChanged) + Q_PROPERTY(QString durationText READ durationText NOTIFY videoDurationChanged) + +public: + explicit HdrPreviewWindow(QWindow *parent = nullptr); + ~HdrPreviewWindow(); + + Q_INVOKABLE void setVideoSink(QVideoSink *sink); + float hdrGain() const { return m_hdrGain; } + bool isPlaying() const { return m_isPlaying; } + bool isFullScreen() const { return windowStates() & Qt::WindowFullScreen; } + int videoPosition() const { return m_videoPosition; } + int videoDuration() const { return m_videoDuration; } + QString positionText() const; + QString durationText() const; + + Q_INVOKABLE void triggerPlayPause(); + Q_INVOKABLE void triggerRewind(); + Q_INVOKABLE void triggerFastForward(); + Q_INVOKABLE void toggleFullScreen(); + Q_INVOKABLE void seekToFrame(int frame); + +public slots: + void pushFrame(const QVideoFrame &frame); + void setHlg(bool isHlg); + void setPlaying(bool playing); + +signals: + void hdrGainChanged(); + void playingChanged(); + void fullScreenChanged(); + void videoPositionChanged(); + void videoDurationChanged(); + +protected: + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + void resizeEvent(QResizeEvent *event) override; + +private slots: + void checkEdrHeadroom(); + +private: + void updateHdrGain(); + + QPointer m_videoSink; + QTimer m_edrTimer; + bool m_loggedSwapChain{false}; + bool m_isHlg{false}; + bool m_isPlaying{false}; + float m_lastLoggedHeadroom{0.0f}; + int m_edrCheckCount{0}; + float m_hdrGain{1.0f}; + QRect m_normalGeometry; + int m_videoPosition{0}; + int m_videoDuration{0}; +}; + +#endif // HDRPREVIEWWINDOW_H diff --git a/src/macos.h b/src/macos.h index 8bd2052d39..83a4f5a343 100644 --- a/src/macos.h +++ b/src/macos.h @@ -17,8 +17,32 @@ #pragma once +#include + void removeMacosTabBar(); void macosSetDockProgress(int percent); void macosPauseDockProgress(int percent); void macosResetDockProgress(); void macosFinishDockProgress(bool isSuccess, bool stopped); + +/// Override NSScreen.maximumExtendedDynamicRangeColorComponentValue to return +/// the *potential* headroom. This breaks the chicken-and-egg where Qt's shader +/// won't output > 1.0 because headroom=1, and macOS won't allocate headroom +/// because no content > 1.0 is being rendered. Once the shader outputs HDR +/// values, macOS allocates real headroom and the override becomes a no-op. +/// Safe: only affects the HDR preview window's video shader (main window uses +/// SDR swapchain format, so Qt's video node ignores hdrInfo for it). +void macosOverrideEdrHeadroom(bool enable); + +/// Query the current EDR headroom for the screen hosting the given window. +/// Returns NSScreen.maximumExtendedDynamicRangeColorComponentValue. +/// @param windowId QWindow::winId() +float macosCurrentEdrHeadroom(uintptr_t windowId); + +/// Query the potential (maximum) EDR headroom. +/// Returns NSScreen.maximumPotentialExtendedDynamicRangeColorComponentValue. +float macosPotentialEdrHeadroom(uintptr_t windowId); + +/// Query the reference EDR headroom. +/// Returns NSScreen.maximumReferenceExtendedDynamicRangeColorComponentValue. +float macosReferenceEdrHeadroom(uintptr_t windowId); diff --git a/src/macos.mm b/src/macos.mm index 73e6298f3a..5bc2869b9f 100644 --- a/src/macos.mm +++ b/src/macos.mm @@ -20,8 +20,12 @@ #import #import #import +#import #import #import +#import + +#include void removeMacosTabBar() { @@ -96,3 +100,69 @@ void macosFinishDockProgress(bool isSuccess, bool stopped) [NSApp requestUserAttention:NSCriticalRequest]; } } + +// --------------------------------------------------------------------------- +// EDR headroom override via method swizzle on NSScreen +// --------------------------------------------------------------------------- +static std::atomic s_edrOverrideEnabled{false}; +static bool s_swizzled = false; + +// Category that holds the swizzled implementation. +@interface NSScreen (ShotcutEdrOverride) +- (CGFloat)shotcut_maximumExtendedDynamicRangeColorComponentValue; +@end + +@implementation NSScreen (ShotcutEdrOverride) +- (CGFloat)shotcut_maximumExtendedDynamicRangeColorComponentValue +{ + // After swizzling, calling the swizzled selector invokes the ORIGINAL impl. + CGFloat real = [self shotcut_maximumExtendedDynamicRangeColorComponentValue]; + if (s_edrOverrideEnabled.load(std::memory_order_relaxed) && real < 2.0) { + return self.maximumPotentialExtendedDynamicRangeColorComponentValue; + } + return real; +} +@end + +void macosOverrideEdrHeadroom(bool enable) +{ + if (!s_swizzled) { + s_swizzled = true; + Method original = class_getInstanceMethod( + [NSScreen class], + @selector(maximumExtendedDynamicRangeColorComponentValue)); + Method swizzled = class_getInstanceMethod( + [NSScreen class], + @selector(shotcut_maximumExtendedDynamicRangeColorComponentValue)); + method_exchangeImplementations(original, swizzled); + NSLog(@"macosOverrideEdrHeadroom: swizzled NSScreen.maximumExtendedDynamicRangeColorComponentValue"); + } + s_edrOverrideEnabled.store(enable, std::memory_order_relaxed); + NSLog(@"macosOverrideEdrHeadroom: %s", enable ? "enabled" : "disabled"); +} + +float macosCurrentEdrHeadroom(uintptr_t windowId) +{ + NSView *view = reinterpret_cast(windowId); + if (!view || !view.window || !view.window.screen) + return 1.0f; + return static_cast(view.window.screen.maximumExtendedDynamicRangeColorComponentValue); +} + +float macosPotentialEdrHeadroom(uintptr_t windowId) +{ + NSView *view = reinterpret_cast(windowId); + if (!view || !view.window || !view.window.screen) + return 1.0f; + return static_cast(view.window.screen.maximumPotentialExtendedDynamicRangeColorComponentValue); +} + +float macosReferenceEdrHeadroom(uintptr_t windowId) +{ + NSView *view = reinterpret_cast(windowId); + if (!view || !view.window || !view.window.screen) + return 1.0f; + if (@available(macOS 12.0, *)) + return static_cast(view.window.screen.maximumReferenceExtendedDynamicRangeColorComponentValue); + return 1.0f; +} diff --git a/src/main.cpp b/src/main.cpp index afaac5a630..ad63863876 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -371,7 +371,7 @@ int main(int argc, char **argv) if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) qputenv("QT_QPA_PLATFORM", "windows:altgr"); #else - ; + qputenv("QT_MEDIA_BACKEND", "minimal"); #endif #ifdef Q_OS_MAC diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b93889ad47..b559c08d9a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -47,6 +47,7 @@ #include "docks/recentdock.h" #include "docks/subtitlesdock.h" #include "docks/timelinedock.h" +#include "hdrpreviewwindow.h" #include "jobqueue.h" #include "jobs/screencapturejob.h" #include "models/audiolevelstask.h" @@ -116,6 +117,7 @@ #include #define SHOTCUT_THEME +#define USE_SCREENS_FOR_EXTERNAL_MONITORING static bool eventDebugCallback(void **data) { @@ -976,16 +978,6 @@ void MainWindow::connectVideoWidgetSignals() videoWidget, &Mlt::VideoWidget::initialize, Qt::DirectConnection); - connect(videoWidget->quickWindow(), - &QQuickWindow::beforeRendering, - videoWidget, - &Mlt::VideoWidget::beforeRendering, - Qt::DirectConnection); - connect(videoWidget->quickWindow(), - &QQuickWindow::beforeRenderPassRecording, - videoWidget, - &Mlt::VideoWidget::renderVideo, - Qt::DirectConnection); connect(videoWidget->quickWindow(), &QQuickWindow::sceneGraphInitialized, this, @@ -1271,8 +1263,9 @@ void MainWindow::setupSettingsMenu() int n = screens.size(); for (int i = 0; n > 1 && i < n; i++) { QAction *action - = new QAction(tr("Screen %1 (%2 x %3 @ %4 Hz)") + = new QAction(tr("Screen %1 %2 (%3x%4 @ %5Hz)") .arg(i) + .arg(screens[i]->name()) .arg(screens[i]->size().width() * screens[i]->devicePixelRatio()) .arg(screens[i]->size().height() * screens[i]->devicePixelRatio()) .arg(screens[i]->refreshRate()), @@ -1281,6 +1274,63 @@ void MainWindow::setupSettingsMenu() action->setData(i); m_externalGroup->addAction(action); } + + auto hdrAction = m_externalGroup->addAction(tr("HDR Preview Window")); + hdrAction->setCheckable(true); + Actions.add("hdrPreviewAction", hdrAction, tr("Player")); + connect(hdrAction, &QAction::toggled, this, [this, hdrAction](bool checked) { + if (checked) { + if (!m_hdrPreviewWindow) { + m_hdrPreviewWindow = new HdrPreviewWindow(); + auto videoWidget = static_cast(&MLT); + connect(videoWidget, + &Mlt::VideoWidget::videoFrameReady, + m_hdrPreviewWindow, + &HdrPreviewWindow::pushFrame); + connect(videoWidget, + &Mlt::VideoWidget::hlgActiveChanged, + m_hdrPreviewWindow, + &HdrPreviewWindow::setHlg); + auto *win = m_hdrPreviewWindow; + connect(m_player, &Player::played, win, [win](double) { win->setPlaying(true); }); + connect(m_player, &Player::paused, win, [win](int) { win->setPlaying(false); }); + connect(m_player, &Player::stopped, win, [win]() { win->setPlaying(false); }); + connect(m_hdrPreviewWindow, + &QWindow::visibleChanged, + this, + [this, hdrAction](bool visible) { + if (!visible) { + Settings.setPlayerHdrPreviewFullScreen( + m_hdrPreviewWindow->windowStates() & Qt::WindowFullScreen); + Settings.setPlayerHdrPreviewGeometry(m_hdrPreviewWindow->geometry()); + hdrAction->setChecked(false); + } + }); + auto savedGeometry = Settings.playerHdrPreviewGeometry(); + if (savedGeometry.isValid()) + m_hdrPreviewWindow->setGeometry(savedGeometry); + } + m_hdrPreviewWindow->show(); + if (Settings.playerHdrPreviewFullScreen()) + m_hdrPreviewWindow->showFullScreen(); + } else { + if (m_hdrPreviewWindow) { + Settings.setPlayerHdrPreviewFullScreen(m_hdrPreviewWindow->windowStates() + & Qt::WindowFullScreen); + Settings.setPlayerHdrPreviewGeometry(m_hdrPreviewWindow->geometry()); + delete m_hdrPreviewWindow; + m_hdrPreviewWindow = nullptr; + } + } + Settings.setPlayerHdrPreview(checked); + }); + connect(hdrAction, &QAction::triggered, this, [this, hdrAction]() { + if (hdrAction->isChecked() && m_hdrPreviewWindow) { + m_hdrPreviewWindow->show(); + m_hdrPreviewWindow->raise(); + m_hdrPreviewWindow->requestActivate(); + } + }); #endif Mlt::Profile profile; @@ -2583,6 +2633,12 @@ void MainWindow::readPlayerSettings() } } + if (Settings.playerHdrPreview()) { + auto hdr = Actions["hdrPreviewAction"]; + if (hdr) + hdr->setChecked(true); + } + QString profile = Settings.playerProfile(); // Automatic not permitted for SDI/HDMI if (isExternalPeripheral && profile.isEmpty()) @@ -2862,6 +2918,12 @@ void MainWindow::writeSettings() #endif Settings.setWindowGeometry(saveGeometry()); Settings.setWindowState(saveState()); + if (m_hdrPreviewWindow) { + Settings.setPlayerHdrPreviewFullScreen(m_hdrPreviewWindow->windowStates() + & Qt::WindowFullScreen); + if (!(m_hdrPreviewWindow->windowStates() & Qt::WindowFullScreen)) + Settings.setPlayerHdrPreviewGeometry(m_hdrPreviewWindow->geometry()); + } Settings.sync(); } diff --git a/src/mainwindow.h b/src/mainwindow.h index fac0c7bf15..062c33f210 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -55,6 +55,7 @@ class MarkersDock; class NotesDock; class SubtitlesDock; class ScreenCapture; +class HdrPreviewWindow; class MainWindow : public QMainWindow { @@ -85,6 +86,7 @@ class MainWindow : public QMainWindow QString fileName() const { return m_currentFile; } bool isSourceClipMyProject(QString resource = MLT.resource(), bool withDialog = true); bool keyframesDockIsVisible() const; + Player *player() const { return m_player; } void keyPressEvent(QKeyEvent *); void keyReleaseEvent(QKeyEvent *); @@ -224,6 +226,7 @@ class MainWindow : public QMainWindow std::unique_ptr m_producerWidget; FilesDock *m_filesDock; ScreenCapture *m_screenCapture; + HdrPreviewWindow *m_hdrPreviewWindow{nullptr}; public slots: bool isCompatibleWithProcessingMode(MltXmlChecker &checker, QString &fileName, bool &converted); diff --git a/src/mltcontroller.cpp b/src/mltcontroller.cpp index 66953e2647..8203047b3e 100644 --- a/src/mltcontroller.cpp +++ b/src/mltcontroller.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 Meltytech, LLC + * Copyright (c) 2011-2026 Meltytech, LLC * * 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 @@ -25,14 +25,7 @@ #include "settings.h" #include "shotcut_mlt_properties.h" #include "util.h" -#if defined(Q_OS_WIN) -#include "widgets/d3dvideowidget.h" -#include "widgets/openglvideowidget.h" -#elif defined(Q_OS_MAC) -#include "widgets/metalvideowidget.h" -#else -#include "widgets/openglvideowidget.h" -#endif +#include "videowidget.h" #include #include @@ -79,16 +72,7 @@ Controller &Controller::singleton(QObject *parent) if (!instance) { qRegisterMetaType("Mlt::Frame"); qRegisterMetaType("SharedFrame"); -#if defined(Q_OS_WIN) - if (QSGRendererInterface::Direct3D11 == QQuickWindow::graphicsApi()) - instance = new D3DVideoWidget(parent); - else - instance = new OpenGLVideoWidget(parent); -#elif defined(Q_OS_MAC) - instance = new MetalVideoWidget(parent); -#else - instance = new OpenGLVideoWidget(parent); -#endif + instance = new VideoWidget(parent); } return *instance; } diff --git a/src/qml/modules/Shotcut/Controls/VuiBase.qml b/src/qml/modules/Shotcut/Controls/VuiBase.qml index 192d720e19..299795634b 100644 --- a/src/qml/modules/Shotcut/Controls/VuiBase.qml +++ b/src/qml/modules/Shotcut/Controls/VuiBase.qml @@ -16,8 +16,25 @@ */ import QtQuick +import QtMultimedia DropArea { + clip: true + + property real _zoom: (video.zoom > 0) ? video.zoom : 1 + + Component.onCompleted: video.setVideoSink(videoOutput.videoSink) + + VideoOutput { + id: videoOutput + + width: video.rect.width * _zoom + height: video.rect.height * _zoom + x: video.rect.x + (video.rect.width - width) / 2 - video.offset.x + y: video.rect.y + (video.rect.height - height) / 2 - video.offset.y + fillMode: VideoOutput.Stretch + } + Canvas { id: grid diff --git a/src/qml/views/HdrPreview.qml b/src/qml/views/HdrPreview.qml new file mode 100644 index 0000000000..c2e46117e3 --- /dev/null +++ b/src/qml/views/HdrPreview.qml @@ -0,0 +1,402 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 . + */ + +import QtQuick +import QtQuick.Shapes +import QtMultimedia + +Rectangle { + color: "black" + + component ControlButton: Rectangle { + id: _btn + + signal clicked() + + property bool active: false + + width: 44 + height: 44 + radius: 8 + color: _btnMouse.containsPress ? Qt.rgba(1, 1, 1, 0.3) : _btnMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.15) : active ? Qt.rgba(1, 1, 1, 0.12) : "transparent" + + MouseArea { + id: _btnMouse + + anchors.fill: parent + hoverEnabled: true + onClicked: _btn.clicked() + } + } + + Component.onCompleted: hdrWindow.setVideoSink(videoOutput.videoSink) + + VideoOutput { + id: videoOutput + + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + layer.enabled: true + layer.format: ShaderEffectSource.RGBA16F + layer.effect: ShaderEffect { + property real gain: hdrWindow.hdrGain + + fragmentShader: "hdr_gain.frag.qsb" + } + } + + // Auto-hide overlay + Item { + id: overlay + + property bool _visible: true + + anchors.fill: parent + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: overlay._visible ? Qt.ArrowCursor : Qt.BlankCursor + onPositionChanged: { + overlay._visible = true; + hideTimer.restart(); + } + onEntered: { + overlay._visible = true; + hideTimer.restart(); + } + } + + Timer { + id: hideTimer + + interval: 3000 + running: true + onTriggered: overlay._visible = false + } + + // Floating rounded control bar + Rectangle { + id: controlBar + + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: parent.height * 0.20 + } + width: Math.max(buttonRow.implicitWidth + 24, 380) + height: 88 + radius: 12 + visible: overlay._visible + color: Qt.rgba(0, 0, 0, 0.72) + + // Absorb clicks so they don't pass through to the video + MouseArea { + anchors.fill: parent + } + + // Transport buttons + Row { + id: buttonRow + + anchors { + top: parent.top + topMargin: 8 + horizontalCenter: parent.horizontalCenter + } + spacing: 4 + + // Rewind << + ControlButton { + onClicked: hdrWindow.triggerRewind() + + Shape { + anchors.centerIn: parent + width: 22 + height: 22 + + // Left chevron + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: 11; startY: 2 + PathLine { x: 3; y: 11 } + PathLine { x: 11; y: 20 } + PathLine { x: 14; y: 20 } + PathLine { x: 6; y: 11 } + PathLine { x: 14; y: 2 } + } + + // Right chevron + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: 18; startY: 2 + PathLine { x: 10; y: 11 } + PathLine { x: 18; y: 20 } + PathLine { x: 21; y: 20 } + PathLine { x: 13; y: 11 } + PathLine { x: 21; y: 2 } + } + } + } + + // Play / Pause + ControlButton { + onClicked: hdrWindow.triggerPlayPause() + + // Play triangle (visible when paused) + Shape { + anchors.centerIn: parent + visible: !hdrWindow.playing + width: 22 + height: 22 + + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: 5; startY: 2 + PathLine { x: 19; y: 11 } + PathLine { x: 5; y: 20 } + } + } + + // Pause bars (visible when playing) + Shape { + anchors.centerIn: parent + visible: hdrWindow.playing + width: 22 + height: 22 + + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: 4; startY: 3 + PathLine { x: 8; y: 3 } + PathLine { x: 8; y: 19 } + PathLine { x: 4; y: 19 } + } + + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: 12; startY: 3 + PathLine { x: 16; y: 3 } + PathLine { x: 16; y: 19 } + PathLine { x: 12; y: 19 } + } + } + } + + // Fast Forward >> + ControlButton { + onClicked: hdrWindow.triggerFastForward() + + Shape { + anchors.centerIn: parent + width: 22 + height: 22 + + // Left chevron + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: 1; startY: 2 + PathLine { x: 9; y: 11 } + PathLine { x: 1; y: 20 } + PathLine { x: 4; y: 20 } + PathLine { x: 12; y: 11 } + PathLine { x: 4; y: 2 } + } + + // Right chevron + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: 8; startY: 2 + PathLine { x: 16; y: 11 } + PathLine { x: 8; y: 20 } + PathLine { x: 11; y: 20 } + PathLine { x: 19; y: 11 } + PathLine { x: 11; y: 2 } + } + } + } + + // Fullscreen toggle + ControlButton { + onClicked: hdrWindow.toggleFullScreen() + + Shape { + id: fullscreenIcon + + anchors.centerIn: parent + width: 22 + height: 22 + + // Top-left corner — expand outward + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + startX: hdrWindow.fullScreen ? 8 : 1; startY: hdrWindow.fullScreen ? 2 : 8 + PathLine { x: hdrWindow.fullScreen ? 2 : 1; y: hdrWindow.fullScreen ? 2 : 1 } + PathLine { x: hdrWindow.fullScreen ? 2 : 8; y: hdrWindow.fullScreen ? 8 : 1 } + } + + // Top-right corner + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + startX: hdrWindow.fullScreen ? 14 : 21; startY: hdrWindow.fullScreen ? 2 : 8 + PathLine { x: hdrWindow.fullScreen ? 20 : 21; y: hdrWindow.fullScreen ? 2 : 1 } + PathLine { x: hdrWindow.fullScreen ? 20 : 14; y: hdrWindow.fullScreen ? 8 : 1 } + } + + // Bottom-left corner + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + startX: hdrWindow.fullScreen ? 8 : 1; startY: hdrWindow.fullScreen ? 20 : 14 + PathLine { x: hdrWindow.fullScreen ? 2 : 1; y: hdrWindow.fullScreen ? 20 : 21 } + PathLine { x: hdrWindow.fullScreen ? 2 : 8; y: hdrWindow.fullScreen ? 14 : 21 } + } + + // Bottom-right corner + ShapePath { + strokeColor: "white" + strokeWidth: 2 + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + startX: hdrWindow.fullScreen ? 14 : 21; startY: hdrWindow.fullScreen ? 20 : 14 + PathLine { x: hdrWindow.fullScreen ? 20 : 21; y: hdrWindow.fullScreen ? 20 : 21 } + PathLine { x: hdrWindow.fullScreen ? 20 : 14; y: hdrWindow.fullScreen ? 14 : 21 } + } + } + } + } + + // Scrub bar row + Item { + id: scrubRow + + anchors { + top: buttonRow.bottom + topMargin: 6 + left: parent.left + right: parent.right + leftMargin: 12 + rightMargin: 12 + } + height: 20 + + Text { + id: posLabel + + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + text: hdrWindow.positionText + color: Qt.rgba(1, 1, 1, 0.85) + font.pixelSize: 11 + } + + Text { + id: durLabel + + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + text: hdrWindow.durationText + color: Qt.rgba(1, 1, 1, 0.85) + font.pixelSize: 11 + } + + Item { + id: scrubArea + + anchors { + left: posLabel.right + right: durLabel.left + leftMargin: 8 + rightMargin: 8 + verticalCenter: parent.verticalCenter + } + height: parent.height + + // Track background + Rectangle { + width: parent.width + height: 3 + anchors.verticalCenter: parent.verticalCenter + radius: 1.5 + color: Qt.rgba(1, 1, 1, 0.25) + + // Filled portion + Rectangle { + width: hdrWindow.videoDuration > 0 ? parent.width * hdrWindow.videoPosition / hdrWindow.videoDuration : 0 + height: parent.height + radius: 1.5 + color: "white" + } + } + + // Scrub handle + Rectangle { + x: hdrWindow.videoDuration > 0 ? (scrubArea.width - width) * hdrWindow.videoPosition / hdrWindow.videoDuration : 0 + anchors.verticalCenter: parent.verticalCenter + width: 10 + height: 10 + radius: 5 + color: "white" + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + + function seek(mx) { + if (hdrWindow.videoDuration > 0) { + const frame = Math.round(mx / width * hdrWindow.videoDuration); + hdrWindow.seekToFrame(Math.max(0, Math.min(frame, hdrWindow.videoDuration - 1))); + } + } + + onPressed: mouse => seek(mouseX) + onPositionChanged: mouse => { + if (pressed) + seek(mouseX); + } + } + } + } + } + } +} + diff --git a/src/qml/views/hdr_gain.frag b/src/qml/views/hdr_gain.frag new file mode 100644 index 0000000000..a12992a4e5 --- /dev/null +++ b/src/qml/views/hdr_gain.frag @@ -0,0 +1,17 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float gain; +}; + +layout(binding = 1) uniform sampler2D source; + +void main() { + vec4 c = texture(source, qt_TexCoord0); + fragColor = vec4(c.rgb * gain, c.a) * qt_Opacity; +} diff --git a/src/qml/views/hdr_gain.frag.qsb b/src/qml/views/hdr_gain.frag.qsb new file mode 100644 index 0000000000..f186842c3c Binary files /dev/null and b/src/qml/views/hdr_gain.frag.qsb differ diff --git a/src/settings.cpp b/src/settings.cpp index 7321de5443..1fc049dbb5 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -845,6 +845,36 @@ void ShotcutSettings::setPlayerPauseAfterSeek(bool b) settings.setValue("player/pauseAfterSeek", b); } +bool ShotcutSettings::playerHdrPreview() const +{ + return settings.value("player/hdrPreview", false).toBool(); +} + +void ShotcutSettings::setPlayerHdrPreview(bool b) +{ + settings.setValue("player/hdrPreview", b); +} + +QRect ShotcutSettings::playerHdrPreviewGeometry() const +{ + return settings.value("player/hdrPreviewGeometry").toRect(); +} + +void ShotcutSettings::setPlayerHdrPreviewGeometry(const QRect &r) +{ + settings.setValue("player/hdrPreviewGeometry", r); +} + +bool ShotcutSettings::playerHdrPreviewFullScreen() const +{ + return settings.value("player/hdrPreviewFullScreen", false).toBool(); +} + +void ShotcutSettings::setPlayerHdrPreviewFullScreen(bool b) +{ + settings.setValue("player/hdrPreviewFullScreen", b); +} + QString ShotcutSettings::playlistThumbnails() const { return settings.value("playlist/thumbnails", "small").toString(); diff --git a/src/settings.h b/src/settings.h index 19d8eedcfb..c5952b7dca 100644 --- a/src/settings.h +++ b/src/settings.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,12 @@ class ShotcutSettings : public QObject void setPlayerAudioDriver(const QString &s); bool playerPauseAfterSeek() const; void setPlayerPauseAfterSeek(bool); + bool playerHdrPreview() const; + void setPlayerHdrPreview(bool); + QRect playerHdrPreviewGeometry() const; + void setPlayerHdrPreviewGeometry(const QRect &); + bool playerHdrPreviewFullScreen() const; + void setPlayerHdrPreviewFullScreen(bool); // playlist QString playlistThumbnails() const; diff --git a/src/util.cpp b/src/util.cpp index 562e4dc1b6..c619d065bf 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -59,6 +59,14 @@ #include #endif +#ifdef _MSC_VER +#include +#endif + +#if defined(__x86_64__) || defined(_M_AMD64) +#include +#endif + #ifdef Q_OS_MAC static constexpr unsigned int kLowMemoryThresholdPercent = 10U; #else @@ -1260,3 +1268,30 @@ bool Util::openUrl(const QUrl &url) #endif return success; } + +bool Util::cpuHasAVX2() +{ + static const bool result = []() -> bool { +#if (defined(__GNUC__) || defined(__clang__)) && (defined(__x86_64__) || defined(_M_AMD64)) + return __builtin_cpu_supports("avx2"); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_AMD64)) + int info[4]; + __cpuid(info, 1); + // Check OSXSAVE and AVX bits in ECX + if (!((info[2] >> 27) & 1) || !((info[2] >> 28) & 1)) + return false; + // Check OS saves/restores YMM registers (XCR0[2:1] == 0b11) + if ((_xgetbv(0) & 0x6) != 0x6) + return false; + // Check AVX2 via structured extended feature leaf 7, EBX bit 5 + __cpuid(info, 0); + if (info[0] < 7) + return false; + __cpuidex(info, 7, 0); + return (info[1] >> 5) & 1; +#else + return false; +#endif + }(); + return result; +} diff --git a/src/util.h b/src/util.h index 5e3d5c2b6b..1aabf509b4 100644 --- a/src/util.h +++ b/src/util.h @@ -109,6 +109,7 @@ class Util static bool isChromiumAvailable(); static bool startDetached(const QString &program, const QStringList &arguments); static bool openUrl(const QUrl &url); + static bool cpuHasAVX2(); }; #endif // UTIL_H diff --git a/src/videowidget.cpp b/src/videowidget.cpp index 1643dcb2ce..323c8a7670 100644 --- a/src/videowidget.cpp +++ b/src/videowidget.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 Meltytech, LLC + * Copyright (c) 2011-2026 Meltytech, LLC * * 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 @@ -23,6 +23,7 @@ #include "qmltypes/qmlfilter.h" #include "qmltypes/qmlutilities.h" #include "settings.h" +#include "util.h" #include #include @@ -32,6 +33,14 @@ #include #include +#ifdef __ARM_NEON +#include +#endif + +#if defined(__x86_64__) || defined(_M_AMD64) +#include +#endif + using namespace Mlt; VideoWidget::VideoWidget(QObject *parent) @@ -40,7 +49,8 @@ VideoWidget::VideoWidget(QObject *parent) , m_grid(0) , m_initSem(0) , m_isInitialized(false) - , m_frameRenderer(nullptr) + , m_frameSemaphore(3) + , m_imageRequested(false) , m_zoom(0.0f) , m_offset(QPoint(0, 0)) , m_snapToGrid(true) @@ -80,36 +90,17 @@ VideoWidget::~VideoWidget() { LOG_DEBUG() << "begin"; stop(); - if (m_frameRenderer && m_frameRenderer->isRunning()) { - m_frameRenderer->quit(); - m_frameRenderer->wait(); - m_frameRenderer->deleteLater(); - } LOG_DEBUG() << "end"; } void VideoWidget::initialize() { LOG_DEBUG() << "begin"; - m_frameRenderer = new FrameRenderer(); - connect(m_frameRenderer, - &FrameRenderer::frameDisplayed, - this, - &VideoWidget::onFrameDisplayed, - Qt::QueuedConnection); - connect(m_frameRenderer, - &FrameRenderer::frameDisplayed, - this, - &VideoWidget::frameDisplayed, - Qt::QueuedConnection); - connect(m_frameRenderer, SIGNAL(imageReady()), SIGNAL(imageReady())); m_initSem.release(); m_isInitialized = true; LOG_DEBUG() << "end"; } -void VideoWidget::renderVideo() {} - void VideoWidget::setBlankScene() { quickWindow()->setColor(palette().window().color()); @@ -213,8 +204,8 @@ void VideoWidget::mouseMoveEvent(QMouseEvent *event) mimeData->setData(Mlt::XmlMimeType, MLT.XML().toUtf8()); drag->setMimeData(mimeData); mimeData->setText(QString::number(MLT.producer()->get_playtime())); - if (m_frameRenderer && m_frameRenderer->getDisplayFrame().is_valid()) { - Mlt::Frame displayFrame(m_frameRenderer->getDisplayFrame().clone(false, true)); + if (m_sharedFrame.is_valid()) { + Mlt::Frame displayFrame(m_sharedFrame.clone(false, true)); QImage displayImage = MLT.image(&displayFrame, 45 * MLT.profile().dar(), 45).scaledToHeight(45); drag->setPixmap(QPixmap::fromImage(displayImage)); @@ -261,8 +252,8 @@ void VideoWidget::keyPressEvent(QKeyEvent *event) bool VideoWidget::event(QEvent *event) { bool result = QQuickWidget::event(event); - if (event->type() == QEvent::PaletteChange && m_sharedFrame.is_valid()) - onFrameDisplayed(m_sharedFrame); + if (event->type() == QEvent::PaletteChange) + setClearColor(palette().window().color()); return result; } @@ -398,13 +389,19 @@ int VideoWidget::reconfigure(bool isMulti) const int processingMode = property("processing_mode").toInt(); const bool isDeckLinkHLG = serviceName.startsWith("decklink") && property("decklinkGamma").toInt() == 1; + const bool hdrPreview = Settings.playerHdrPreview(); switch (processingMode) { case ShotcutSettings::Native10Cpu: + m_consumer->set("mlt_image_format", hdrPreview ? "yuv420p10" : "rgba64"); + break; case ShotcutSettings::Linear10Cpu: m_consumer->set("mlt_image_format", "rgba64"); break; case ShotcutSettings::Linear10GpuCpu: - m_consumer->set("mlt_image_format", isDeckLinkHLG ? "yuv444p10" : "rgba64"); + m_consumer->set("mlt_image_format", + isDeckLinkHLG ? "yuv444p10" + : hdrPreview ? "yuv420p10" + : "rgba64"); break; default: // Native8Cpu m_consumer->set("mlt_image_format", @@ -429,7 +426,7 @@ int VideoWidget::reconfigure(bool isMulti) m_consumer->set("color_trc", "bt470bg"); break; case 2020: - if (isDeckLinkHLG) { + if (isDeckLinkHLG || hdrPreview) { m_consumer->set("color_trc", "arib-std-b67"); } else { m_consumer->clear("color_trc"); @@ -439,9 +436,10 @@ int VideoWidget::reconfigure(bool isMulti) m_consumer->set("color_trc", "bt709"); break; } + emit hlgActiveChanged(!qstrcmp(m_consumer->get("color_trc"), "arib-std-b67")); if (processingMode == ShotcutSettings::Linear10Cpu - || (processingMode == ShotcutSettings::Linear10GpuCpu - && property("decklinkGamma").toInt() != 1)) { + || (processingMode == ShotcutSettings::Linear10GpuCpu && !isDeckLinkHLG + && !hdrPreview)) { m_consumer->set("mlt_color_trc", "linear"); } else { m_consumer->clear("mlt_color_trc"); @@ -520,12 +518,11 @@ QPoint VideoWidget::offset() const QImage VideoWidget::image() const { - SharedFrame frame = m_frameRenderer->getDisplayFrame(); - if (frame.is_valid()) { - const uint8_t *image = frame.get_image(mlt_image_rgba); + if (m_sharedFrame.is_valid()) { + const uint8_t *image = m_sharedFrame.get_image(mlt_image_rgba); if (image) { - int width = frame.get_image_width(); - int height = frame.get_image_height(); + int width = m_sharedFrame.get_image_width(); + int height = m_sharedFrame.get_image_height(); QImage temp(image, width, height, QImage::Format_RGBA8888); return temp.copy(); } @@ -536,7 +533,7 @@ QImage VideoWidget::image() const bool VideoWidget::imageIsProxy() const { bool isProxy = false; - SharedFrame frame = m_frameRenderer->getDisplayFrame(); + SharedFrame frame = m_sharedFrame; if (frame.is_valid()) { Mlt::Producer *frameProducer = frame.get_original_producer(); if (frameProducer && frameProducer->is_valid() && frameProducer->get_int(kIsProxyProperty)) { @@ -547,9 +544,9 @@ bool VideoWidget::imageIsProxy() const return isProxy; } -void VideoWidget::requestImage() const +void VideoWidget::requestImage() { - m_frameRenderer->requestImage(); + m_imageRequested = true; } void VideoWidget::toggleVuiDisplay() @@ -558,19 +555,280 @@ void VideoWidget::toggleVuiDisplay() refreshConsumer(); } -void VideoWidget::onFrameDisplayed(const SharedFrame &frame) +void VideoWidget::setVideoSink(QVideoSink *sink) +{ + m_videoSink = sink; + if (m_videoSink && m_sharedFrame.is_valid()) + pushFrameToSink(m_sharedFrame); +} + +#if defined(__x86_64__) || defined(_M_AMD64) +#if defined(__GNUC__) || defined(__clang__) +__attribute__((target("avx2"))) +#endif +static void +shiftYPlane_AVX2(const uint16_t *src, uint16_t *dst, int n) +{ + int i = 0; + for (; i + 16 <= n; i += 16) { + __m256i y = _mm256_loadu_si256(reinterpret_cast(src + i)); + _mm256_storeu_si256(reinterpret_cast<__m256i *>(dst + i), _mm256_slli_epi16(y, 6)); + } + for (; i < n; ++i) + dst[i] = src[i] << 6; +} + +#if defined(__GNUC__) || defined(__clang__) +__attribute__((target("avx2"))) +#endif +static void +interleaveUVPlanes_AVX2(const uint16_t *srcU, const uint16_t *srcV, uint16_t *dst, int n) +{ + // AVX2 unpack operates within 128-bit lanes; permute to restore linear order. + // unpacklo(u,v): lane0 = u0v0u1v1u2v2u3v3, lane1 = u8v8...u11v11 + // unpackhi(u,v): lane0 = u4v4...u7v7, lane1 = u12v12...u15v15 + // permute2x128 0x20 → [lo.lane0 | hi.lane0] = u0v0..u7v7 + // permute2x128 0x31 → [lo.lane1 | hi.lane1] = u8v8..u15v15 + int j = 0; + for (; j + 16 <= n; j += 16) { + __m256i u + = _mm256_slli_epi16(_mm256_loadu_si256(reinterpret_cast(srcU + j)), 6); + __m256i v + = _mm256_slli_epi16(_mm256_loadu_si256(reinterpret_cast(srcV + j)), 6); + __m256i lo = _mm256_unpacklo_epi16(u, v); + __m256i hi = _mm256_unpackhi_epi16(u, v); + _mm256_storeu_si256(reinterpret_cast<__m256i *>(dst + j * 2), + _mm256_permute2x128_si256(lo, hi, 0x20)); + _mm256_storeu_si256(reinterpret_cast<__m256i *>(dst + j * 2 + 16), + _mm256_permute2x128_si256(lo, hi, 0x31)); + } + for (; j < n; ++j) { + dst[2 * j] = srcU[j] << 6; + dst[2 * j + 1] = srcV[j] << 6; + } +} + +static void shiftYPlane_SSE2(const uint16_t *src, uint16_t *dst, int n) +{ + int i = 0; + for (; i + 8 <= n; i += 8) { + __m128i y = _mm_loadu_si128(reinterpret_cast(src + i)); + _mm_storeu_si128(reinterpret_cast<__m128i *>(dst + i), _mm_slli_epi16(y, 6)); + } + for (; i < n; ++i) + dst[i] = src[i] << 6; +} + +static void interleaveUVPlanes_SSE2(const uint16_t *srcU, const uint16_t *srcV, uint16_t *dst, int n) +{ + int j = 0; + for (; j + 8 <= n; j += 8) { + __m128i u = _mm_slli_epi16(_mm_loadu_si128(reinterpret_cast(srcU + j)), 6); + __m128i v = _mm_slli_epi16(_mm_loadu_si128(reinterpret_cast(srcV + j)), 6); + _mm_storeu_si128(reinterpret_cast<__m128i *>(dst + j * 2), _mm_unpacklo_epi16(u, v)); + _mm_storeu_si128(reinterpret_cast<__m128i *>(dst + j * 2 + 8), _mm_unpackhi_epi16(u, v)); + } + for (; j < n; ++j) { + dst[2 * j] = srcU[j] << 6; + dst[2 * j + 1] = srcV[j] << 6; + } +} +#endif // defined(__x86_64__) || defined(_M_AMD64) + +void VideoWidget::pushFrameToSink(const SharedFrame &frame) +{ + if (!m_videoSink) + return; + int width = frame.get_image_width(); + int height = frame.get_image_height(); + if (width < 1 || height < 1) + return; + + bool is10bit = !qstrcmp(m_consumer->get("mlt_image_format"), "yuv420p10"); + mlt_image_format mltFormat = is10bit ? mlt_image_yuv420p10 : mlt_image_yuv420p; + const uint8_t *image = frame.get_image(mltFormat); + if (!image) + return; + + auto pixFmt = is10bit ? QVideoFrameFormat::Format_P016 : QVideoFrameFormat::Format_YUV420P; + QVideoFrameFormat fmt(QSize(width, height), pixFmt); + fmt.setColorRange(QVideoFrameFormat::ColorRange_Video); + switch (profile().colorspace()) { + case 601: + case 170: + fmt.setColorSpace(QVideoFrameFormat::ColorSpace_BT601); + fmt.setColorTransfer(QVideoFrameFormat::ColorTransfer_BT601); + break; + case 2020: + fmt.setColorSpace(QVideoFrameFormat::ColorSpace_BT2020); + if (!qstrcmp(m_consumer->get("color_trc"), "arib-std-b67")) { + fmt.setColorTransfer(QVideoFrameFormat::ColorTransfer_STD_B67); + fmt.setMaxLuminance(1000.0f); + } else { + fmt.setColorTransfer(QVideoFrameFormat::ColorTransfer_BT709); + } + break; + default: + fmt.setColorSpace(QVideoFrameFormat::ColorSpace_BT709); + fmt.setColorTransfer(QVideoFrameFormat::ColorTransfer_BT709); + break; + } + + // For BT.2020/HLG, convert MLT's planar yuv420p10 (3 planes: Y, U, V with + // 16-bit samples, 10 significant bits in LSBs) to semi-planar P016 (2 planes: + // Y, interleaved UV with full 16-bit range) so that Qt selects the + // nv12_bt2020_hlg fragment shader for proper HLG tone mapping. + QByteArray p016Buffer; + if (is10bit) { + int uvW = width / 2; + int uvH = height / 2; + int ySamples = width * height; + int yPlaneSize = ySamples * 2; // 16-bit per sample + int uvPlaneSize = uvW * uvH * 2; // per U or V plane in source + int interleavedUvSize = uvW * uvH * 4; // interleaved UV in dest + p016Buffer.resize(yPlaneSize + interleavedUvSize); + + // Copy Y plane, shifting 10-bit values (LSBs) to full 16-bit range + const uint16_t *srcY = reinterpret_cast(image); + uint16_t *dstY = reinterpret_cast(p016Buffer.data()); +#ifdef __ARM_NEON + int i = 0; + for (; i + 8 <= ySamples; i += 8) { + uint16x8_t y = vld1q_u16(srcY + i); + vst1q_u16(dstY + i, vshlq_n_u16(y, 6)); + } + for (; i < ySamples; ++i) + dstY[i] = srcY[i] << 6; +#elif defined(__x86_64__) || defined(_M_AMD64) + if (Util::cpuHasAVX2()) + shiftYPlane_AVX2(srcY, dstY, ySamples); + else + shiftYPlane_SSE2(srcY, dstY, ySamples); +#else + for (int i = 0; i < ySamples; ++i) { + dstY[i] = srcY[i] << 6; + } +#endif + + // Interleave U and V planes, also shifting to full 16-bit range + const uint16_t *srcU = reinterpret_cast(image + yPlaneSize); + const uint16_t *srcV = reinterpret_cast(image + yPlaneSize + uvPlaneSize); + uint16_t *dstUV = reinterpret_cast(p016Buffer.data() + yPlaneSize); + int uvSamples = uvW * uvH; +#ifdef __ARM_NEON + int j = 0; + for (; j + 8 <= uvSamples; j += 8) { + uint16x8_t u = vshlq_n_u16(vld1q_u16(srcU + j), 6); + uint16x8_t v = vshlq_n_u16(vld1q_u16(srcV + j), 6); + uint16x8x2_t uv = {u, v}; + vst2q_u16(dstUV + j * 2, uv); + } + for (; j < uvSamples; ++j) { + dstUV[2 * j] = srcU[j] << 6; + dstUV[2 * j + 1] = srcV[j] << 6; + } +#elif defined(__x86_64__) || defined(_M_AMD64) + if (Util::cpuHasAVX2()) + interleaveUVPlanes_AVX2(srcU, srcV, dstUV, uvSamples); + else + interleaveUVPlanes_SSE2(srcU, srcV, dstUV, uvSamples); +#else + for (int i = 0; i < uvSamples; ++i) { + dstUV[2 * i] = srcU[i] << 6; + dstUV[2 * i + 1] = srcV[i] << 6; + } +#endif + } + + // Zero-copy buffer for 8-bit, or P016 converted buffer for 10-bit. + class SharedFrameVideoBuffer : public QAbstractVideoBuffer + { + public: + SharedFrameVideoBuffer(const SharedFrame &sf, const QVideoFrameFormat &f) + : m_sharedFrame(sf) + , m_format(f) + {} + SharedFrameVideoBuffer(QByteArray p016, const QVideoFrameFormat &f) + : m_p016(std::move(p016)) + , m_format(f) + {} + QVideoFrameFormat format() const override { return m_format; } + MapData map(QVideoFrame::MapMode) override + { + int w = m_sharedFrame.get_image_width(); + int h = m_sharedFrame.get_image_height(); + MapData md; + if (!m_p016.isEmpty()) { + // P016: semi-planar 16-bit, 2 planes + w = m_format.frameWidth(); + h = m_format.frameHeight(); + auto *p = reinterpret_cast(m_p016.data()); + md.planeCount = 2; + // Y plane (16-bit per sample) + md.bytesPerLine[0] = w * 2; + md.data[0] = p; + md.dataSize[0] = w * h * 2; + // Interleaved UV plane (2 × 16-bit per sample pair) + md.bytesPerLine[1] = (w / 2) * 4; + md.data[1] = p + md.dataSize[0]; + md.dataSize[1] = (w / 2) * (h / 2) * 4; + } else { + // YUV420P: planar 8-bit, 3 planes + auto *p = const_cast(m_sharedFrame.get_image(mlt_image_yuv420p)); + md.planeCount = 3; + md.bytesPerLine[0] = w; + md.data[0] = p; + md.dataSize[0] = w * h; + md.bytesPerLine[1] = w / 2; + md.data[1] = p + md.dataSize[0]; + md.dataSize[1] = (w / 2) * (h / 2); + md.bytesPerLine[2] = w / 2; + md.data[2] = md.data[1] + md.dataSize[1]; + md.dataSize[2] = md.dataSize[1]; + } + return md; + } + + private: + SharedFrame m_sharedFrame; + QByteArray m_p016; + QVideoFrameFormat m_format; + }; + + std::unique_ptr buffer; + if (is10bit) { + buffer = std::make_unique(std::move(p016Buffer), fmt); + } else { + buffer = std::make_unique(frame, fmt); + } + QVideoFrame videoFrame(std::move(buffer)); + // Set PTS so downstream consumers (e.g. HdrPreviewWindow) can track position. + const double fps = profile().fps(); + if (fps > 0.0) + videoFrame.setStartTime(qRound64(frame.get_position() / fps * 1000000.0)); + m_videoSink->setVideoFrame(videoFrame); + emit videoFrameReady(videoFrame); +} + +void VideoWidget::showFrame(Mlt::Frame frame) { m_mutex.lock(); - m_sharedFrame = frame; + m_sharedFrame = SharedFrame(frame); m_mutex.unlock(); - bool isVui = frame.get_int(kShotcutVuiMetaProperty) && !m_hideVui; + bool isVui = m_sharedFrame.get_int(kShotcutVuiMetaProperty) && !m_hideVui; if (!isVui && source() != QmlUtilities::blankVui()) { m_savedQmlSource = source(); setSource(QmlUtilities::blankVui()); } else if (isVui && !m_savedQmlSource.isEmpty() && source() != m_savedQmlSource) { setSource(m_savedQmlSource); } - quickWindow()->update(); + pushFrameToSink(m_sharedFrame); + emit frameDisplayed(m_sharedFrame); + if (m_imageRequested) { + m_imageRequested = false; + emit imageReady(); + } + m_frameSemaphore.release(); } void VideoWidget::setGrid(int grid) @@ -629,9 +887,8 @@ void VideoWidget::on_frame_show(mlt_consumer, VideoWidget *widget, mlt_event_dat auto frame = Mlt::EventData(data).to_frame(); if (frame.is_valid() && frame.get_int("rendered")) { int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; - if (widget->m_frameRenderer - && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { - QMetaObject::invokeMethod(widget->m_frameRenderer, + if (widget->m_frameSemaphore.tryAcquire(1, timeout)) { + QMetaObject::invokeMethod(widget, "showFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); @@ -673,38 +930,3 @@ void RenderThread::run() m_function(m_data); m_context->doneCurrent(); } - -FrameRenderer::FrameRenderer() - : QThread(nullptr) - , m_semaphore(3) - , m_imageRequested(false) -{ - setObjectName("FrameRenderer"); - moveToThread(this); - start(); -} - -FrameRenderer::~FrameRenderer() {} - -void FrameRenderer::showFrame(Mlt::Frame frame) -{ - m_displayFrame = SharedFrame(frame); - emit frameDisplayed(m_displayFrame); - - if (m_imageRequested) { - m_imageRequested = false; - emit imageReady(); - } - - m_semaphore.release(); -} - -void FrameRenderer::requestImage() -{ - m_imageRequested = true; -} - -SharedFrame FrameRenderer::getDisplayFrame() -{ - return m_displayFrame; -} diff --git a/src/videowidget.h b/src/videowidget.h index 12e0b77aa3..0c968a6cfc 100644 --- a/src/videowidget.h +++ b/src/videowidget.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 Meltytech, LLC + * Copyright (c) 2011-2026 Meltytech, LLC * * 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 @@ -22,12 +22,17 @@ #include "settings.h" #include "sharedframe.h" +#include #include +#include #include #include #include #include #include +#include +#include +#include class QmlFilter; class QmlMetadata; @@ -38,7 +43,6 @@ namespace Mlt { class Filter; class RenderThread; -class FrameRenderer; typedef void *(*thread_function_t)(void *); @@ -91,10 +95,11 @@ class VideoWidget : public QQuickWidget, public Controller QPoint offset() const; QImage image() const; bool imageIsProxy() const; - void requestImage() const; + void requestImage(); bool snapToGrid() const { return m_snapToGrid; } int maxTextureSize() const { return m_maxTextureSize; } void toggleVuiDisplay(); + Q_INVOKABLE void setVideoSink(QVideoSink *sink); public slots: void setGrid(int grid); @@ -105,9 +110,7 @@ public slots: void setCurrentFilter(QmlFilter *filter, QmlMetadata *meta); void setSnapToGrid(bool snap); virtual void initialize(); - virtual void beforeRendering(){}; - virtual void renderVideo(); - virtual void onFrameDisplayed(const SharedFrame &frame); + Q_INVOKABLE void showFrame(Mlt::Frame frame); signals: void frameDisplayed(const SharedFrame &frame); @@ -125,6 +128,8 @@ public slots: void snapToGridChanged(); void toggleZoom(bool); void stepZoom(float, float); + void videoFrameReady(const QVideoFrame &frame); + void hlgActiveChanged(bool isHlg); private: QRectF m_rect; @@ -137,7 +142,8 @@ public slots: std::unique_ptr m_threadStopEvent; std::unique_ptr m_threadCreateEvent; std::unique_ptr m_threadJoinEvent; - FrameRenderer *m_frameRenderer; + QSemaphore m_frameSemaphore; + bool m_imageRequested; float m_zoom; QPoint m_offset; QUrl m_savedQmlSource; @@ -147,8 +153,10 @@ public slots: bool m_scrubAudio; QPoint m_mousePosition; std::unique_ptr m_renderThread; + QPointer m_videoSink; static void on_frame_show(mlt_consumer, VideoWidget *widget, mlt_event_data); + void pushFrameToSink(const SharedFrame &frame); private slots: void resizeVideo(int width, int height); @@ -161,7 +169,6 @@ private slots: void wheelEvent(QWheelEvent *event) override; void keyPressEvent(QKeyEvent *event) override; bool event(QEvent *event) override; - void createShader(); int m_maxTextureSize; SharedFrame m_sharedFrame; @@ -185,29 +192,6 @@ class RenderThread : public QThread std::unique_ptr m_surface; }; -class FrameRenderer : public QThread -{ - Q_OBJECT -public: - FrameRenderer(); - ~FrameRenderer(); - QSemaphore *semaphore() { return &m_semaphore; } - SharedFrame getDisplayFrame(); - Q_INVOKABLE void showFrame(Mlt::Frame frame); - void requestImage(); - QImage image() const { return m_image; } - -signals: - void frameDisplayed(const SharedFrame &frame); - void imageReady(); - -private: - QSemaphore m_semaphore; - SharedFrame m_displayFrame; - bool m_imageRequested; - QImage m_image; -}; - } // namespace Mlt #endif diff --git a/src/widgets/d3dvideowidget.cpp b/src/widgets/d3dvideowidget.cpp deleted file mode 100644 index b017d02c6c..0000000000 --- a/src/widgets/d3dvideowidget.cpp +++ /dev/null @@ -1,403 +0,0 @@ -/* - * Copyright (c) 2023-2025 Meltytech, LLC - * - * 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, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ - -#include "d3dvideowidget.h" - -#include "Logger.h" - -#include - -D3DVideoWidget::D3DVideoWidget(QObject *parent) - : Mlt::VideoWidget{parent} -{ - m_maxTextureSize = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION; - ::memset(&m_constants, 0, sizeof(m_constants)); -} - -D3DVideoWidget::~D3DVideoWidget() -{ - for (int i = 0; i < 3; i++) { - if (m_texture[i]) - m_texture[i]->Release(); - } - if (m_vs) - m_vs->Release(); - if (m_ps) - m_ps->Release(); - if (m_vbuf) - m_vbuf->Release(); - if (m_cbuf) - m_cbuf->Release(); - if (m_inputLayout) - m_inputLayout->Release(); - if (m_rastState) - m_rastState->Release(); - if (m_dsState) - m_dsState->Release(); -} - -void D3DVideoWidget::initialize() -{ - m_initialized = true; - QSGRendererInterface *rif = quickWindow()->rendererInterface(); - - // We are not prepared for anything other than running with the RHI and its D3D11 backend. - Q_ASSERT(rif->graphicsApi() == QSGRendererInterface::Direct3D11); - - m_device = reinterpret_cast( - rif->getResource(quickWindow(), QSGRendererInterface::DeviceResource)); - Q_ASSERT(m_device); - m_context = reinterpret_cast( - rif->getResource(quickWindow(), QSGRendererInterface::DeviceContextResource)); - Q_ASSERT(m_context); - - if (m_vert.isEmpty()) - prepareShader(VertexStage); - if (m_frag.isEmpty()) - prepareShader(FragmentStage); - - const QByteArray vs = compileShader(VertexStage, m_vert, m_vertEntryPoint); - const QByteArray fs = compileShader(FragmentStage, m_frag, m_fragEntryPoint); - - HRESULT hr = m_device->CreateVertexShader(vs.constData(), vs.size(), nullptr, &m_vs); - if (FAILED(hr)) - qFatal("Failed to create vertex shader: 0x%x", uint(hr)); - - hr = m_device->CreatePixelShader(fs.constData(), fs.size(), nullptr, &m_ps); - if (FAILED(hr)) - qFatal("Failed to create pixel shader: 0x%x", uint(hr)); - - D3D11_BUFFER_DESC bufDesc; - memset(&bufDesc, 0, sizeof(bufDesc)); - bufDesc.ByteWidth = sizeof(float) * 16; - bufDesc.Usage = D3D11_USAGE_DEFAULT; - bufDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; - hr = m_device->CreateBuffer(&bufDesc, nullptr, &m_vbuf); - if (FAILED(hr)) - qFatal("Failed to create buffer: 0x%x", uint(hr)); - - bufDesc.ByteWidth = sizeof(m_constants) + 0xf & 0xfffffff0; // must be a multiple of 16 - bufDesc.Usage = D3D11_USAGE_DYNAMIC; - bufDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - bufDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - hr = m_device->CreateBuffer(&bufDesc, nullptr, &m_cbuf); - if (FAILED(hr)) - qFatal("Failed to create buffer: 0x%x", uint(hr)); - - const D3D11_INPUT_ELEMENT_DESC inputDesc[] = { - {"VERTEX", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, - {"TEXCOORD", - 0, - DXGI_FORMAT_R32G32B32_FLOAT, - 0, - sizeof(DirectX::XMFLOAT2), - D3D11_INPUT_PER_VERTEX_DATA, - 0}, - }; - hr = m_device->CreateInputLayout(inputDesc, - ARRAYSIZE(inputDesc), - vs.constData(), - vs.size(), - &m_inputLayout); - if (FAILED(hr)) - qFatal("Failed to create input layout: 0x%x", uint(hr)); - - D3D11_RASTERIZER_DESC rastDesc; - memset(&rastDesc, 0, sizeof(rastDesc)); - rastDesc.FillMode = D3D11_FILL_SOLID; - rastDesc.CullMode = D3D11_CULL_NONE; - hr = m_device->CreateRasterizerState(&rastDesc, &m_rastState); - if (FAILED(hr)) - qFatal("Failed to create rasterizer state: 0x%x", uint(hr)); - - D3D11_DEPTH_STENCIL_DESC dsDesc; - memset(&dsDesc, 0, sizeof(dsDesc)); - hr = m_device->CreateDepthStencilState(&dsDesc, &m_dsState); - if (FAILED(hr)) - qFatal("Failed to create depth/stencil state: 0x%x", uint(hr)); - - Mlt::VideoWidget::initialize(); -} - -void D3DVideoWidget::beforeRendering() -{ - quickWindow()->beginExternalCommands(); - m_context->ClearState(); - - // Provide vertices of triangle strip - float width = rect().width() * devicePixelRatioF() / 2.0f; - float height = rect().height() * devicePixelRatioF() / 2.0f; - float vertexData[] = { - // x,y plus u,v texture coordinates - width, - -height, - 1.f, - 1.f, // bottom left - -width, - -height, - 0.f, - 1.f, // bottom right - width, - height, - 1.f, - 0.f, // top left - -width, - height, - 0.f, - 0.f // top right - }; - - // Setup an orthographic projection - QMatrix4x4 modelView; - width = this->width() * devicePixelRatioF(); - height = this->height() * devicePixelRatioF(); - modelView.scale(2.0f / width, 2.0f / height); - - // Set model-view - if (rect().width() > 0.0 && zoom() > 0.0) { - if (offset().x() || offset().y()) - modelView.translate(-offset().x() * devicePixelRatioF(), - offset().y() * devicePixelRatioF()); - modelView.scale(zoom(), zoom()); - } - for (int i = 0; i < 4; i++) { - vertexData[4 * i] *= modelView(0, 0); - vertexData[4 * i] += modelView(0, 3); - vertexData[4 * i + 1] *= modelView(1, 1); - vertexData[4 * i + 1] += modelView(1, 3); - } - m_context->UpdateSubresource(m_vbuf, 0, nullptr, vertexData, 0, 0); - - // (Re)create the textures - m_mutex.lock(); - if (!m_sharedFrame.is_valid()) { - m_mutex.unlock(); - quickWindow()->endExternalCommands(); - Mlt::VideoWidget::beforeRendering(); - return; - } - int iwidth = m_sharedFrame.get_image_width(); - int iheight = m_sharedFrame.get_image_height(); - const uint8_t *image = m_sharedFrame.get_image(mlt_image_yuv420p); - for (int i = 0; i < 3; i++) { - if (m_texture[i]) - m_texture[i]->Release(); - } - m_texture[0] = initTexture(image, iwidth, iheight); - m_texture[1] = initTexture(image + iwidth * iheight, iwidth / 2, iheight / 2); - m_texture[2] = initTexture(image + iwidth * iheight + iwidth / 2 * iheight / 2, - iwidth / 2, - iheight / 2); - m_mutex.unlock(); - - // Update the constants - D3D11_MAPPED_SUBRESOURCE mp; - // will copy the entire constant buffer every time -> pass WRITE_DISCARD -> prevent pipeline stalls - HRESULT hr = m_context->Map(m_cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, &mp); - if (SUCCEEDED(hr)) { - m_constants.colorspace = MLT.profile().colorspace(); - ::memcpy(mp.pData, &m_constants, sizeof(m_constants)); - m_context->Unmap(m_cbuf, 0); - } else { - quickWindow()->endExternalCommands(); - qFatal("Failed to map constant buffer: 0x%x", uint(hr)); - return; - } - - quickWindow()->endExternalCommands(); - Mlt::VideoWidget::beforeRendering(); -} - -void D3DVideoWidget::renderVideo() -{ - if (!m_texture[0]) { - Mlt::VideoWidget::renderVideo(); - return; - } - quickWindow()->beginExternalCommands(); - - D3D11_VIEWPORT v; - v.TopLeftX = 0.f; - v.TopLeftY = 0.f; - v.Width = this->width() * devicePixelRatioF(); - v.Height = this->height() * devicePixelRatioF(); - v.MinDepth = 0.f; - v.MaxDepth = 1.f; - - m_context->RSSetViewports(1, &v); - m_context->VSSetShader(m_vs, nullptr, 0); - m_context->PSSetShader(m_ps, nullptr, 0); - m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); - m_context->IASetInputLayout(m_inputLayout); - m_context->OMSetDepthStencilState(m_dsState, 0); - m_context->RSSetState(m_rastState); - const UINT stride = sizeof(float) * 4; - const UINT offset = 0; - m_context->IASetVertexBuffers(0, 1, &m_vbuf, &stride, &offset); - m_context->PSSetConstantBuffers(0, 1, &m_cbuf); - m_context->PSSetShaderResources(0, 3, m_texture); - m_context->Draw(4, 0); - - quickWindow()->endExternalCommands(); - Mlt::VideoWidget::renderVideo(); -} - -void D3DVideoWidget::prepareShader(Stage stage) -{ - if (stage == VertexStage) { - m_vert = "struct VSInput {" - " float2 vertex : VERTEX;" - " float2 coords : TEXCOORD;" - "};" - "struct VSOutput {" - " float2 coords : TEXCOORD0;" - " float4 position : SV_Position;" - "};" - "VSOutput main(VSInput input) {" - " VSOutput output;" - " output.position = float4(input.vertex, 0.0f, 1.0f);" - " output.coords = input.coords;" - " return output;" - "}"; - Q_ASSERT(!m_vert.isEmpty()); - m_vertEntryPoint = QByteArrayLiteral("main"); - } else { - m_frag = "Texture2D yTex, uTex, vTex;" - "SamplerState yuvSampler;" - "cbuffer buf {" - " int colorspace;" - "};" - "struct PSInput {" - " float2 coords : TEXCOORD0;" - "};" - "struct PSOutput {" - " float4 color : SV_Target0;" - "};" - "PSOutput main(PSInput input) {" - " float3 yuv;" - " yuv.x = yTex.Sample(yuvSampler, input.coords).r - 16.0f/255.0f;" - " yuv.y = uTex.Sample(yuvSampler, input.coords).r - 128.0f/255.0f;" - " yuv.z = vTex.Sample(yuvSampler, input.coords).r - 128.0f/255.0f;" - " float3x3 coefficients;" - " if (colorspace == 601) {" - " coefficients = float3x3(" - " 1.1643f, 0.0f, 1.5958f," - " 1.1643f, -0.39173f, -0.8129f," - " 1.1643f, 2.017f, 0.0f);" - " } else if (colorspace == 2020) {" // ITU-R BT.2020 - " coefficients = float3x3(" - " 1.1643f, 0.0f, 1.7167f," - " 1.1643f, -0.1873f, -0.6504f," - " 1.1643f, 2.1418f, 0.0f);" - " } else {" // ITU-R 709 - " coefficients = float3x3(" - " 1.1643f, 0.0f, 1.793f," - " 1.1643f, -0.213f, -0.533f," - " 1.1643f, 2.112f, 0.0f);" - " }" - " PSOutput output;" - " output.color = float4(mul(coefficients, yuv), 1.0f);" - " return output;" - "}"; - m_fragEntryPoint = QByteArrayLiteral("main"); - } -} - -QByteArray D3DVideoWidget::compileShader(Stage stage, - const QByteArray &source, - const QByteArray &entryPoint) -{ - const char *target; - switch (stage) { - case VertexStage: - target = "vs_5_0"; - break; - case FragmentStage: - target = "ps_5_0"; - break; - default: - qFatal("Unknown shader stage %d", stage); - return QByteArray(); - } - - ID3DBlob *bytecode = nullptr; - ID3DBlob *errors = nullptr; - HRESULT hr = D3DCompile(source.constData(), - source.size(), - nullptr, - nullptr, - nullptr, - entryPoint.constData(), - target, - 0, - 0, - &bytecode, - &errors); - if (FAILED(hr) || !bytecode) { - LOG_WARNING("HLSL shader compilation failed: 0x%x", uint(hr)); - if (errors) { - const QByteArray msg(static_cast(errors->GetBufferPointer()), - errors->GetBufferSize()); - errors->Release(); - LOG_WARNING("%s", msg.constData()); - } - return QByteArray(); - } - - QByteArray result; - result.resize(bytecode->GetBufferSize()); - memcpy(result.data(), bytecode->GetBufferPointer(), result.size()); - bytecode->Release(); - - return result; -} - -ID3D11ShaderResourceView *D3DVideoWidget::initTexture(const void *p, int width, int height) -{ - ID3D11ShaderResourceView *result; - D3D11_TEXTURE2D_DESC desc; - desc.Width = width; - desc.Height = height; - desc.MipLevels = 1; - desc.ArraySize = 1; - desc.Format = DXGI_FORMAT_R8_UNORM; - desc.SampleDesc.Count = 1; - desc.SampleDesc.Quality = 0; - desc.Usage = D3D11_USAGE_DEFAULT; - desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; - desc.CPUAccessFlags = 0; - desc.MiscFlags = 0; - - D3D11_SUBRESOURCE_DATA subresourceData; - subresourceData.pSysMem = p; - subresourceData.SysMemPitch = width; - subresourceData.SysMemSlicePitch = 0; - - ID3D11Texture2D *texture; - m_device->CreateTexture2D(&desc, &subresourceData, &texture); - - D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; - srvDesc.Format = desc.Format; - srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; - srvDesc.Texture2D.MipLevels = 1; - srvDesc.Texture2D.MostDetailedMip = 0; - - m_device->CreateShaderResourceView(texture, &srvDesc, &result); - texture->Release(); - - return result; -} diff --git a/src/widgets/d3dvideowidget.h b/src/widgets/d3dvideowidget.h deleted file mode 100644 index f9528e55b6..0000000000 --- a/src/widgets/d3dvideowidget.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2023 Meltytech, LLC - * - * 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, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ - -#ifndef D3DVIDEOWIDGET_H -#define D3DVIDEOWIDGET_H - -#include "videowidget.h" - -#include -#include - -class D3DVideoWidget : public Mlt::VideoWidget -{ - Q_OBJECT -public: - explicit D3DVideoWidget(QObject *parent = nullptr); - virtual ~D3DVideoWidget(); - -public slots: - virtual void initialize(); - virtual void beforeRendering(); - virtual void renderVideo(); - -private: - enum Stage { VertexStage, FragmentStage }; - void prepareShader(Stage stage); - QByteArray compileShader(Stage stage, const QByteArray &source, const QByteArray &entryPoint); - ID3D11ShaderResourceView *initTexture(const void *p, int width, int height); - - ID3D11Device *m_device = nullptr; - ID3D11DeviceContext *m_context = nullptr; - QByteArray m_vert; - QByteArray m_vertEntryPoint; - QByteArray m_frag; - QByteArray m_fragEntryPoint; - - bool m_initialized = false; - ID3D11Buffer *m_vbuf = nullptr; - ID3D11Buffer *m_cbuf = nullptr; - ID3D11VertexShader *m_vs = nullptr; - ID3D11PixelShader *m_ps = nullptr; - ID3D11InputLayout *m_inputLayout = nullptr; - ID3D11RasterizerState *m_rastState = nullptr; - ID3D11DepthStencilState *m_dsState = nullptr; - ID3D11ShaderResourceView *m_texture[3] = {nullptr, nullptr, nullptr}; - - struct ConstantBuffer - { - int32_t colorspace; - }; - - ConstantBuffer m_constants; -}; - -#endif // D3DVIDEOWIDGET_H diff --git a/src/widgets/metalvideowidget.h b/src/widgets/metalvideowidget.h deleted file mode 100644 index 192162489e..0000000000 --- a/src/widgets/metalvideowidget.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 Meltytech, LLC - * - * 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, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ - -#ifndef METALVIDEOWIDGET_H -#define METALVIDEOWIDGET_H - -#include "videowidget.h" - -class MetalVideoRenderer; - -class MetalVideoWidget : public Mlt::VideoWidget -{ - Q_OBJECT -public: - explicit MetalVideoWidget(QObject *parent); - virtual ~MetalVideoWidget(); - -public slots: - virtual void initialize(); - virtual void renderVideo(); - -private: - std::unique_ptr m_renderer; -}; - -#endif // METALVIDEOWIDGET_H diff --git a/src/widgets/metalvideowidget.mm b/src/widgets/metalvideowidget.mm deleted file mode 100644 index 404e45eaa7..0000000000 --- a/src/widgets/metalvideowidget.mm +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (c) 2023-2025 Meltytech, LLC - * - * 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, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ - -#include "metalvideowidget.h" - -#include "Logger.h" - -#include - - -class MetalVideoRenderer : public QObject -{ - Q_OBJECT -public: - MetalVideoRenderer() - { - for (int i = 0; i < 3; ++i) { - m_ubuf[i] = nil; - m_texture[i] = nil; - } - m_vbuf = nil; - m_vs.first = nil; - m_vs.second = nil; - m_fs.first = nil; - m_fs.second = nil; - } - - ~MetalVideoRenderer() - { - LOG_DEBUG() << "cleanup"; - - for (int i = 0; i < 3; i++) { - [m_texture[i] release]; - [m_ubuf[i] release]; - } - [m_vbuf release]; - [m_vs.first release]; - [m_vs.second release]; - [m_fs.first release]; - [m_fs.second release]; - } - - void initialize(QQuickWindow *window) - { - LOG_DEBUG() << "init"; - m_window = window; - - QSGRendererInterface *rif = m_window->rendererInterface(); - - // We are not prepared for anything other than running with the RHI and its Metal backend. - Q_ASSERT(rif->graphicsApi() == QSGRendererInterface::Metal); - - m_device = (id) rif->getResource(m_window, QSGRendererInterface::DeviceResource); - Q_ASSERT(m_device); - - if (m_vert.isEmpty()) - prepareShader(VertexStage); - if (m_frag.isEmpty()) - prepareShader(FragmentStage); - - m_vbuf = [m_device newBufferWithLength: 16*sizeof(float) options: MTLResourceStorageModeShared]; - - for (int i = 0; i < m_window->graphicsStateInfo().framesInFlight && i < 3; ++i) - m_ubuf[i] = [m_device newBufferWithLength: sizeof(int) options: MTLResourceStorageModeShared]; - - MTLVertexDescriptor *inputLayout = [MTLVertexDescriptor vertexDescriptor]; - inputLayout.attributes[0].format = MTLVertexFormatFloat4; - inputLayout.attributes[0].offset = 0; - inputLayout.attributes[0].bufferIndex = 1; // ubuf is 0, vbuf is 1 - inputLayout.layouts[1].stride = 4 * sizeof(float); - - MTLRenderPipelineDescriptor *rpDesc = [[MTLRenderPipelineDescriptor alloc] init]; - rpDesc.vertexDescriptor = inputLayout; - - m_vs = compileShader(m_vert, m_vertEntryPoint); - rpDesc.vertexFunction = m_vs.first; - m_fs = compileShader(m_frag, m_fragEntryPoint); - rpDesc.fragmentFunction = m_fs.first; - - rpDesc.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; -// if (m_device.depth24Stencil8PixelFormatSupported) { -// rpDesc.depthAttachmentPixelFormat = MTLPixelFormatDepth24Unorm_Stencil8; -// rpDesc.stencilAttachmentPixelFormat = MTLPixelFormatDepth24Unorm_Stencil8; -// } else { -// rpDesc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float_Stencil8; -// rpDesc.stencilAttachmentPixelFormat = MTLPixelFormatDepth32Float_Stencil8; -// } - - NSError *err = nil; - m_pipeline = [m_device newRenderPipelineStateWithDescriptor: rpDesc error: &err]; - if (!m_pipeline) { - const QString msg = QString::fromNSString(err.localizedDescription); - qFatal("Failed to create render pipeline state: %s", qPrintable(msg)); - } - [rpDesc release]; - } - - void render(const QSize& viewportSize, const QRectF& videoRect, const double devicePixelRatio, - const double zoom, const QPoint& offset, const SharedFrame& sharedFrame) - { - const QQuickWindow::GraphicsStateInfo &stateInfo(m_window->graphicsStateInfo()); - - QSGRendererInterface *rif = m_window->rendererInterface(); - id encoder = (id) rif->getResource( - m_window, QSGRendererInterface::CommandEncoderResource); - Q_ASSERT(encoder); - - // Provide vertices of triangle strip - float width = videoRect.width() * devicePixelRatio / 2.0f; - float height = videoRect.height() * devicePixelRatio / 2.0f; - float vertexData[] = { // x,y plus u,v texture coordinates - -width, height, 0.f, 0.f, - width, height, 1.f, 0.f, - -width, -height, 0.f, 1.f, - width, -height, 1.f, 1.f - }; - - // Setup an orthographic projection - QMatrix4x4 modelView; - width = viewportSize.width() * devicePixelRatio; - height = viewportSize.height() * devicePixelRatio; - modelView.scale(2.0f / width, 2.0f / height); - - // Set model-view - if (videoRect.width() > 0.0 && zoom > 0.0) { - if (offset.x() || offset.y()) - modelView.translate(-offset.x() * devicePixelRatio, - offset.y() * devicePixelRatio); - modelView.scale(zoom, zoom); - } - for (int i = 0; i < 4; i++) { - vertexData[4 * i] *= modelView(0, 0); - vertexData[4 * i] += modelView(0, 3); - vertexData[4 * i + 1] *= modelView(1, 1); - vertexData[4 * i + 1] += modelView(1, 3); - } - - m_window->beginExternalCommands(); - - void *p = [m_vbuf contents]; - memcpy(p, vertexData, sizeof(vertexData)); - - p = [m_ubuf[stateInfo.currentFrameSlot] contents]; - int colorspace = MLT.profile().colorspace(); - memcpy(p, &colorspace, sizeof(colorspace)); - - MTLViewport vp; - vp.originX = 0; - vp.originY = 0; - vp.width = width; - vp.height = height; - vp.znear = 0; - vp.zfar = 1; - [encoder setViewport: vp]; - - // (Re)create the textures - int iwidth = sharedFrame.get_image_width(); - int iheight = sharedFrame.get_image_height(); - const uint8_t *image = sharedFrame.get_image(mlt_image_yuv420p); - for (int i = 0; i < 3; i++) { - [m_texture[i] release]; - } - m_texture[0] = initTexture(image, iwidth, iheight); - m_texture[1] = initTexture(image + iwidth * iheight, iwidth / 2, iheight / 2); - m_texture[2] = initTexture(image + iwidth * iheight + iwidth / 2 * iheight / 2, iwidth / 2, - iheight / 2); - // Set the texture object. The AAPLTextureIndexBaseColor enum value corresponds - /// to the 'colorMap' argument in the 'samplingShader' function because its - // texture attribute qualifier also uses AAPLTextureIndexBaseColor for its index. - for (NSUInteger i = 0; i < 3; i++) { - [encoder setFragmentTexture:m_texture[i] atIndex:i]; - } - - - [encoder setFragmentBuffer: m_ubuf[stateInfo.currentFrameSlot] offset: 0 atIndex: 0]; - [encoder setVertexBuffer: m_vbuf offset: 0 atIndex: 1]; - [encoder setRenderPipelineState: m_pipeline]; - [encoder drawPrimitives: MTLPrimitiveTypeTriangleStrip vertexStart: 0 vertexCount: 4 instanceCount: 1 baseInstance: 0]; - - m_window->endExternalCommands(); - } - - id initTexture(const void *p, NSUInteger width, NSUInteger height) - { - MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init]; - - // 8-bit unsigned normalized value (i.e. 0 maps to 0.0 and 255 maps to 1.0) - textureDescriptor.pixelFormat = MTLPixelFormatR8Unorm; - textureDescriptor.width = width; - textureDescriptor.height = height; - id texture = [m_device newTextureWithDescriptor:textureDescriptor]; - [textureDescriptor release]; - - MTLRegion region = { - { 0, 0, 0 }, // MTLOrigin - {width, height, 1} // MTLSize - }; - - // Copy the bytes from the data object into the texture - [texture replaceRegion:region mipmapLevel:0 withBytes:p bytesPerRow:width]; - return texture; - } - -private: - enum Stage { - VertexStage, - FragmentStage - }; - using FuncAndLib = QPair, id >; - QQuickWindow *m_window = nullptr; - QByteArray m_vert; - QByteArray m_vertEntryPoint; - QByteArray m_frag; - QByteArray m_fragEntryPoint; - id m_device; - id m_vbuf; - id m_ubuf[3]; - FuncAndLib m_vs; - FuncAndLib m_fs; - id m_pipeline; - id m_texture[3]; - - void prepareShader(Stage stage) - { - if (stage == VertexStage) { - m_vert ="#include \n" - "#include \n" - "using namespace metal;" - "struct main0_out {" - " float2 coords [[user(locn0)]];" - " float4 vertices [[position]];" - "};" - "struct main0_in {" - " float4 vertices [[attribute(0)]];" - "};" - "vertex main0_out main0(main0_in in [[stage_in]]) {" - " main0_out out = {};" - " out.vertices = vector_float4(in.vertices.xy, 0.0f, 1.0f);" - " out.coords = in.vertices.zw;" - " return out;" - "}"; - Q_ASSERT(!m_vert.isEmpty()); - m_vertEntryPoint = QByteArrayLiteral("main0"); - } else { - m_frag ="#include \n" - "#include \n" - "using namespace metal;" - "struct buf {" - " int colorspace;" - "};" - "struct main0_out {" - " float4 fragColor [[color(0)]];" - "};" - "struct main0_in {" - " float2 coords [[user(locn0)]];" - "};" - "fragment main0_out main0(main0_in in [[stage_in]], constant buf& ubuf [[buffer(0)]]," - " texture2d yTex [[texture(0)]]," - " texture2d uTex [[texture(1)]]," - " texture2d vTex [[texture(2)]]" - " ) {" - " main0_out out = {};" - " constexpr sampler yuvSampler (mag_filter::linear, min_filter::linear);" - " float3 yuv;" - " yuv.x = yTex.sample(yuvSampler, in.coords).r - 16.0f/255.0f;" - " yuv.y = uTex.sample(yuvSampler, in.coords).r - 128.0f/255.0f;" - " yuv.z = vTex.sample(yuvSampler, in.coords).r - 128.0f/255.0f;" - " float3x3 coefficients;" - " if (ubuf.colorspace == 601) {" - " coefficients = float3x3(" - " {1.1643f, 1.1643f, 1.1643f}," - " {0.0f, -0.39173f, 2.017f}," - " {1.5958f, -0.8129f, 0.0f});" - " } else if (ubuf.colorspace == 2020) {" // ITU-R BT.2020 - " coefficients = float3x3(" - " {1.1643f, 1.1643f, 1.1643f}," - " {0.0f, -0.1873f, 2.1418f}," - " {1.7167f, -0.6504f, 0.0f});" - " } else {" // ITU-R 709 - " coefficients = float3x3(" - " {1.1643f, 1.1643f, 1.1643f}," - " {0.0f, -0.213f, 2.112f}," - " {1.793f, -0.533f, 0.0f});" - " }" - " out.fragColor = float4(coefficients * yuv, 1.0f);" - " return out;" - "}"; - m_fragEntryPoint = QByteArrayLiteral("main0"); - } - } - - FuncAndLib compileShader(const QByteArray &source, const QByteArray &entryPoint) - { - FuncAndLib fl; - - NSString *srcstr = [NSString stringWithUTF8String: source.constData()]; - MTLCompileOptions *opts = [[MTLCompileOptions alloc] init]; - opts.languageVersion = MTLLanguageVersion1_2; - NSError *err = nil; - fl.second = [m_device newLibraryWithSource: srcstr options: opts error: &err]; - [opts release]; - // srcstr is autoreleased - - if (err) { - const QString msg = QString::fromNSString(err.localizedDescription); - qFatal("%s", qPrintable(msg)); - return fl; - } - - NSString *name = [NSString stringWithUTF8String: entryPoint.constData()]; - fl.first = [fl.second newFunctionWithName: name]; -// [name release]; - - return fl; - } -}; - -MetalVideoWidget::MetalVideoWidget(QObject *parent) - : Mlt::VideoWidget{parent} - , m_renderer{new MetalVideoRenderer} -{ - m_maxTextureSize = 16384; -} - -MetalVideoWidget::~MetalVideoWidget() -{ -} - -void MetalVideoWidget::initialize() -{ - m_renderer->initialize(quickWindow()); - Mlt::VideoWidget::initialize(); -} - -void MetalVideoWidget::renderVideo() -{ - m_mutex.lock(); - if (m_sharedFrame.is_valid()) { - m_renderer->render(size(), rect(), devicePixelRatio(), zoom(), offset(), m_sharedFrame); - } - m_mutex.unlock(); - Mlt::VideoWidget::renderVideo(); -} - -#include "metalvideowidget.moc" diff --git a/src/widgets/openglvideowidget.cpp b/src/widgets/openglvideowidget.cpp deleted file mode 100644 index fde689fbe5..0000000000 --- a/src/widgets/openglvideowidget.cpp +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright (c) 2011-2026 Meltytech, LLC - * - * Some GL shader based on BSD licensed code from Peter Bengtsson: - * http://www.fourcc.org/source/YUV420P-OpenGL-GLSLang.c - * - * 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, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ - -#include "openglvideowidget.h" -#include "mainwindow.h" - -#include "Logger.h" - -#include -#include -#include -#include - -#ifdef QT_NO_DEBUG -#define check_error(fn) \ - {} -#else -#define check_error(fn) \ - { \ - int err = fn->glGetError(); \ - if (err != GL_NO_ERROR) { \ - LOG_ERROR() << "GL error" << Qt::hex << err << Qt::dec << "at" << __FILE__ << ":" \ - << __LINE__; \ - } \ - } -#endif - -OpenGLVideoWidget::OpenGLVideoWidget(QObject *parent) - : VideoWidget{parent} - , m_quickContext(nullptr) - , m_isThreadedOpenGL(false) -{ - m_renderTexture[0] = m_renderTexture[1] = m_renderTexture[2] = 0; - m_displayTexture[0] = m_displayTexture[1] = m_displayTexture[2] = 0; -} - -OpenGLVideoWidget::~OpenGLVideoWidget() -{ - LOG_DEBUG() << "begin"; - if (m_renderTexture[0] && m_displayTexture[0] && m_context) { - m_context->makeCurrent(&m_offscreenSurface); - m_context->functions()->glDeleteTextures(3, m_renderTexture); - if (m_displayTexture[0] && m_displayTexture[1] && m_displayTexture[2]) - m_context->functions()->glDeleteTextures(3, m_displayTexture); - m_context->doneCurrent(); - } -} - -void OpenGLVideoWidget::initialize() -{ - LOG_DEBUG() << "begin"; - auto context = static_cast( - quickWindow()->rendererInterface()->getResource(quickWindow(), - QSGRendererInterface::OpenGLContextResource)); - m_quickContext = context; - - if (!m_offscreenSurface.isValid()) { - m_offscreenSurface.setFormat(context->format()); - m_offscreenSurface.create(); - } - Q_ASSERT(m_offscreenSurface.isValid()); - - initializeOpenGLFunctions(); - LOG_INFO() << "OpenGL vendor" << QString::fromUtf8((const char *) glGetString(GL_VENDOR)); - LOG_INFO() << "OpenGL renderer" << QString::fromUtf8((const char *) glGetString(GL_RENDERER)); - LOG_INFO() << "OpenGL threaded?" << context->supportsThreadedOpenGL(); - LOG_INFO() << "OpenGL ES?" << context->isOpenGLES(); - glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_maxTextureSize); - LOG_INFO() << "OpenGL maximum texture size =" << m_maxTextureSize; - GLint dims[2]; - glGetIntegerv(GL_MAX_VIEWPORT_DIMS, &dims[0]); - LOG_INFO() << "OpenGL maximum viewport size =" << dims[0] << "x" << dims[1]; - -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) - // Turn off the hardware decoder by default on Linux with NVIDIA - const auto nvidia - = QString::fromUtf8((const char *) glGetString(GL_RENDERER)).toLower().contains("nv"); - if (nvidia && !Settings.playerPreviewHardwareDecoderIsSet()) { - MAIN.turnOffHardwareDecoder(); - } -#endif - - createShader(); - - LOG_DEBUG() << "end"; - Mlt::VideoWidget::initialize(); -} - -void OpenGLVideoWidget::createShader() -{ - m_shader.reset(new QOpenGLShaderProgram); - m_shader->addShaderFromSourceCode(QOpenGLShader::Vertex, - "uniform highp mat4 projection;" - "uniform highp mat4 modelView;" - "attribute highp vec4 vertex;" - "attribute highp vec2 texCoord;" - "varying highp vec2 coordinates;" - "void main(void) {" - " gl_Position = projection * modelView * vertex;" - " coordinates = texCoord;" - "}"); - m_shader - ->addShaderFromSourceCode(QOpenGLShader::Fragment, - "uniform sampler2D Ytex, Utex, Vtex;" - "uniform lowp int colorspace;" - "varying highp vec2 coordinates;" - "void main(void) {" - " mediump vec3 texel;" - " texel.r = texture2D(Ytex, coordinates).r - 16.0/255.0;" // Y - " texel.g = texture2D(Utex, coordinates).r - 128.0/255.0;" // U - " texel.b = texture2D(Vtex, coordinates).r - 128.0/255.0;" // V - " mediump mat3 coefficients;" - " if (colorspace == 601) {" - " coefficients = mat3(" - " 1.1643, 1.1643, 1.1643," // column 1 - " 0.0, -0.39173, 2.017," // column 2 - " 1.5958, -0.8129, 0.0);" // column 3 - " } else if (colorspace == 2020) {" // ITU-R BT.2020 - " coefficients = mat3(" - " 1.1643, 1.1643, 1.1643," // column 1 - " 0.0, -0.1873, 2.1418," // column 2 - " 1.7167, -0.6504, 0.0);" // column 3 - " } else {" // ITU-R 709 - " coefficients = mat3(" - " 1.1643, 1.1643, 1.1643," // column 1 - " 0.0, -0.213, 2.112," // column 2 - " 1.793, -0.533, 0.0);" // column 3 - " }" - " gl_FragColor = vec4(coefficients * texel, 1.0);" - "}"); - m_shader->link(); - m_textureLocation[0] = m_shader->uniformLocation("Ytex"); - m_textureLocation[1] = m_shader->uniformLocation("Utex"); - m_textureLocation[2] = m_shader->uniformLocation("Vtex"); - m_colorspaceLocation = m_shader->uniformLocation("colorspace"); - m_projectionLocation = m_shader->uniformLocation("projection"); - m_modelViewLocation = m_shader->uniformLocation("modelView"); - m_vertexLocation = m_shader->attributeLocation("vertex"); - m_texCoordLocation = m_shader->attributeLocation("texCoord"); -} - -static void uploadTextures(QOpenGLContext *context, const SharedFrame &frame, GLuint texture[]) -{ - int width = frame.get_image_width(); - int height = frame.get_image_height(); - const uint8_t *image = frame.get_image(mlt_image_yuv420p); - QOpenGLFunctions *f = context->functions(); - - // The planes of pixel data may not be a multiple of the default 4 bytes. - f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - - // Upload each plane of YUV to a texture. - if (texture[0]) - f->glDeleteTextures(3, texture); - check_error(f); - f->glGenTextures(3, texture); - check_error(f); - - f->glBindTexture(GL_TEXTURE_2D, texture[0]); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - check_error(f); - f->glTexImage2D(GL_TEXTURE_2D, - 0, - GL_LUMINANCE, - width, - height, - 0, - GL_LUMINANCE, - GL_UNSIGNED_BYTE, - image); - check_error(f); - - f->glBindTexture(GL_TEXTURE_2D, texture[1]); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - check_error(f); - f->glTexImage2D(GL_TEXTURE_2D, - 0, - GL_LUMINANCE, - width / 2, - height / 2, - 0, - GL_LUMINANCE, - GL_UNSIGNED_BYTE, - image + width * height); - check_error(f); - - f->glBindTexture(GL_TEXTURE_2D, texture[2]); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - check_error(f); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - check_error(f); - f->glTexImage2D(GL_TEXTURE_2D, - 0, - GL_LUMINANCE, - width / 2, - height / 2, - 0, - GL_LUMINANCE, - GL_UNSIGNED_BYTE, - image + width * height + width / 2 * height / 2); - check_error(f); -} - -void OpenGLVideoWidget::renderVideo() -{ - auto context = static_cast( - quickWindow()->rendererInterface()->getResource(quickWindow(), - QSGRendererInterface::OpenGLContextResource)); - if (!m_quickContext) { - LOG_ERROR() << "No quickContext"; - return; - } - if (!context->isValid()) { - LOG_ERROR() << "No QSGRendererInterface::OpenGLContextResource"; - return; - } - -#ifndef QT_NO_DEBUG - QOpenGLFunctions *f = context->functions(); -#endif - float width = this->width() * devicePixelRatioF(); - float height = this->height() * devicePixelRatioF(); - - glDisable(GL_BLEND); - glDisable(GL_DEPTH_TEST); - glDepthMask(GL_FALSE); - glViewport(0, 0, width, height); - check_error(f); - - if (!m_isThreadedOpenGL) { - m_mutex.lock(); - if (!m_sharedFrame.is_valid()) { - m_mutex.unlock(); - return; - } - uploadTextures(context, m_sharedFrame, m_displayTexture); - m_mutex.unlock(); - } - - if (!m_displayTexture[0]) { - return; - } - - quickWindow()->beginExternalCommands(); - - // Bind textures. - for (int i = 0; i < 3; ++i) { - if (m_displayTexture[i]) { - glActiveTexture(GL_TEXTURE0 + i); - glBindTexture(GL_TEXTURE_2D, m_displayTexture[i]); - check_error(f); - } - } - - // Init shader program. - m_shader->bind(); - m_shader->setUniformValue(m_textureLocation[0], 0); - m_shader->setUniformValue(m_textureLocation[1], 1); - m_shader->setUniformValue(m_textureLocation[2], 2); - m_shader->setUniformValue(m_colorspaceLocation, MLT.profile().colorspace()); - check_error(f); - - // Setup an orthographic projection. - QMatrix4x4 projection; - projection.scale(2.0f / width, 2.0f / height); - m_shader->setUniformValue(m_projectionLocation, projection); - check_error(f); - - // Set model view. - QMatrix4x4 modelView; - if (rect().width() > 0.0 && zoom() > 0.0) { - if (offset().x() || offset().y()) - modelView.translate(-offset().x() * devicePixelRatioF(), - offset().y() * devicePixelRatioF()); - modelView.scale(zoom(), zoom()); - } - m_shader->setUniformValue(m_modelViewLocation, modelView); - check_error(f); - - // Provide vertices of triangle strip. - QVector vertices; - width = rect().width() * devicePixelRatioF(); - height = rect().height() * devicePixelRatioF(); - vertices << QVector2D(-width / 2.0f, -height / 2.0f); - vertices << QVector2D(-width / 2.0f, height / 2.0f); - vertices << QVector2D(width / 2.0f, -height / 2.0f); - vertices << QVector2D(width / 2.0f, height / 2.0f); - m_shader->enableAttributeArray(m_vertexLocation); - check_error(f); - m_shader->setAttributeArray(m_vertexLocation, vertices.constData()); - check_error(f); - - // Provide texture coordinates. - QVector texCoord; - texCoord << QVector2D(0.0f, 1.0f); - texCoord << QVector2D(0.0f, 0.0f); - texCoord << QVector2D(1.0f, 1.0f); - texCoord << QVector2D(1.0f, 0.0f); - m_shader->enableAttributeArray(m_texCoordLocation); - check_error(f); - m_shader->setAttributeArray(m_texCoordLocation, texCoord.constData()); - check_error(f); - - // Render - glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size()); - check_error(f); - - // Cleanup - m_shader->disableAttributeArray(m_vertexLocation); - m_shader->disableAttributeArray(m_texCoordLocation); - m_shader->release(); - for (int i = 0; i < 3; ++i) { - if (m_displayTexture[i]) { - glActiveTexture(GL_TEXTURE0 + i); - glBindTexture(GL_TEXTURE_2D, 0); - check_error(f); - } - } - glActiveTexture(GL_TEXTURE0); - check_error(f); - - quickWindow()->endExternalCommands(); - Mlt::VideoWidget::renderVideo(); -} - -void OpenGLVideoWidget::onFrameDisplayed(const SharedFrame &frame) -{ - if (m_isThreadedOpenGL && !m_context) { - m_context.reset(new QOpenGLContext); - if (m_context) { - m_context->setFormat(m_quickContext->format()); - m_context->setShareContext(m_quickContext); - m_context->create(); - } - } - if (m_context && m_context->isValid()) { - // Using threaded OpenGL to upload textures. - QOpenGLFunctions *f = m_context->functions(); - m_context->makeCurrent(&m_offscreenSurface); - uploadTextures(m_context.get(), frame, m_renderTexture); - f->glBindTexture(GL_TEXTURE_2D, 0); - check_error(f); - f->glFinish(); - m_context->doneCurrent(); - - m_mutex.lock(); - for (int i = 0; i < 3; ++i) - std::swap(m_renderTexture[i], m_displayTexture[i]); - m_mutex.unlock(); - } - Mlt::VideoWidget::onFrameDisplayed(frame); -} diff --git a/src/widgets/openglvideowidget.h b/src/widgets/openglvideowidget.h deleted file mode 100644 index d214bceefe..0000000000 --- a/src/widgets/openglvideowidget.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2023 Meltytech, LLC - * - * 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, either version 3 of the License, or - * (at your option) any later version. - * - * 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 . - */ - -#ifndef OPENGLVIDEOWIDGET_H -#define OPENGLVIDEOWIDGET_H - -#include "videowidget.h" - -#include -#include -#include -#include -#include - -class OpenGLVideoWidget : public Mlt::VideoWidget, protected QOpenGLFunctions -{ - Q_OBJECT - -public: - explicit OpenGLVideoWidget(QObject *parent = nullptr); - virtual ~OpenGLVideoWidget(); - -public slots: - virtual void initialize(); - virtual void renderVideo(); - virtual void onFrameDisplayed(const SharedFrame &frame); - -private: - void createShader(); - - QOffscreenSurface m_offscreenSurface; - std::unique_ptr m_shader; - GLint m_projectionLocation; - GLint m_modelViewLocation; - GLint m_vertexLocation; - GLint m_texCoordLocation; - GLint m_colorspaceLocation; - GLint m_textureLocation[3]; - QOpenGLContext *m_quickContext; - std::unique_ptr m_context; - GLuint m_renderTexture[3]; - GLuint m_displayTexture[3]; - bool m_isThreadedOpenGL; -}; - -#endif // OPENGLVIDEOWIDGET_H