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