From 958492411edb050909d5533a42422f7365e3b065 Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Wed, 8 Jul 2026 09:51:00 +0200 Subject: [PATCH 1/2] GUI: reduce macOS repaint overhead Signed-off-by: Roberto Nibali --- .../VirtualBox/src/runtime/UIFrameBuffer.cpp | 64 +++++++++++++------ .../VirtualBox/src/runtime/UIMachineLogic.cpp | 29 ++++++--- .../VirtualBox/src/runtime/UIMachineLogic.h | 1 + 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp b/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp index dbbd96bb1f03..1cc34a90e309 100644 --- a/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp +++ b/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp @@ -1151,6 +1151,9 @@ void UIFrameBufferPrivate::performRescale() switch (m_pMachineView->machineLogic()->visualStateType()) { case UIVisualStateType_Scale: +#ifdef VBOX_WS_MAC + case UIVisualStateType_Fullscreen: +#endif m_scaledSize = scaledSize().width() == m_iWidth && scaledSize().height() == m_iHeight ? QSize() : scaledSize(); break; default: @@ -1314,11 +1317,30 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) /* Take the device-pixel-ratio into account: */ paintRectHiDPI.moveTo(paintRectHiDPI.topLeft() * devicePixelRatio()); paintRectHiDPI.setSize(paintRectHiDPI.size() * devicePixelRatio()); + const QRect requestedPaintRectHiDPI = paintRectHiDPI; /* Make sure hidpi paint rectangle is within the image boundary: */ paintRectHiDPI &= pSourceImage->rect(); if (paintRectHiDPI.isEmpty()) + { +#ifdef VBOX_WS_MAC + switch (m_pMachineView->visualStateType()) + { + case UIVisualStateType_Normal: + case UIVisualStateType_Fullscreen: + case UIVisualStateType_Scale: + { + QPainter painter(m_pMachineView->viewport()); + painter.fillRect(paintRect, QColor(Qt::black)); + break; + } + default: + break; + } +#endif /* VBOX_WS_MAC */ return; + } + const bool fPaintRectFullyCovered = paintRectHiDPI == requestedPaintRectHiDPI; /* Create painter: */ QPainter painter(m_pMachineView->viewport()); @@ -1330,12 +1352,18 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) case UIVisualStateType_Fullscreen: case UIVisualStateType_Scale: { -#ifdef VBOX_WS_MAC +#if defined(VBOX_WS_MAC) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) /* On OSX for Qt5 we need to fill the backing store first: */ painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(paintRect, QColor(Qt::black)); painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); -#endif /* VBOX_WS_MAC */ +#elif defined(VBOX_WS_MAC) + /* Clear only newly exposed macOS backing-store regions. Qt 6 handles steady-state + * dirty repaints without this fill, but resize/fullscreen transitions can otherwise + * leave the default gray backing-store color outside the current guest image. */ + if (!fPaintRectFullyCovered) + painter.fillRect(paintRect, QColor(Qt::black)); +#endif /* VBOX_WS_MAC && Qt < 6 */ break; } @@ -1357,12 +1385,12 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) painter.setCompositionMode(QPainter::CompositionMode_SourceOver); unlock(); -#ifdef VBOX_WITH_TRANSLUCENT_SEAMLESS +#if defined(VBOX_WITH_TRANSLUCENT_SEAMLESS) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) /* In case of translucent seamless for Qt5 we need to fill the backing store first: */ painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(paintRect, QColor(Qt::black)); painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); -#endif /* VBOX_WITH_TRANSLUCENT_SEAMLESS */ +#endif /* VBOX_WITH_TRANSLUCENT_SEAMLESS && Qt < 6 */ break; } @@ -1441,31 +1469,25 @@ void UIFrameBufferPrivate::drawImageRect(QPainter &painter, const QImage &image, int iContentsShiftX, int iContentsShiftY, double dDevicePixelRatio) { - /* Calculate offset: */ - const size_t offset = (rect.x() + iContentsShiftX) * image.depth() / 8 + - (rect.y() + iContentsShiftY) * image.bytesPerLine(); - /* Restrain boundaries: */ const int iSubImageWidth = qMin(rect.width(), image.width() - rect.x() - iContentsShiftX); const int iSubImageHeight = qMin(rect.height(), image.height() - rect.y() - iContentsShiftY); + if ( iSubImageWidth <= 0 + || iSubImageHeight <= 0) + return; - /* Create sub-image (no copy involved): */ - QImage subImage = QImage(image.bits() + offset, - iSubImageWidth, iSubImageHeight, - image.bytesPerLine(), image.format()); - - /* Create sub-pixmap on the basis of sub-image above (1st copy involved): */ - QPixmap subPixmap = QPixmap::fromImage(subImage); - /* Take the device-pixel-ratio into account: */ - subPixmap.setDevicePixelRatio(dDevicePixelRatio); - - /* Which point we should draw corresponding sub-pixmap? */ + /* Which point we should draw the image at? */ QPoint paintPoint = rect.topLeft(); /* Take the device-pixel-ratio into account: */ paintPoint /= dDevicePixelRatio; - /* Draw sub-pixmap: */ - painter.drawPixmap(paintPoint, subPixmap); + /* Draw directly from the source image to avoid materializing a temporary pixmap for every update rectangle. */ + const QRect sourceRect(rect.x() + iContentsShiftX, rect.y() + iContentsShiftY, + iSubImageWidth, iSubImageHeight); + const QRectF targetRect(QPointF(paintPoint), + QSizeF((double)iSubImageWidth / dDevicePixelRatio, + (double)iSubImageHeight / dDevicePixelRatio)); + painter.drawImage(targetRect, image, sourceRect); } /* static */ diff --git a/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.cpp b/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.cpp index 401a46ecb1e0..348b2b1cd84a 100644 --- a/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.cpp +++ b/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.cpp @@ -114,6 +114,7 @@ /* Other VBox includes: */ #include +#include /* VirtualBox interface declarations: */ #include @@ -332,19 +333,30 @@ void UIMachineLogic::openNetworkSettingsDialogTheModalWay() } #ifdef VBOX_WS_MAC +/** Minimum interval between live Dock preview frame updates. */ +static const uint64_t s_u64DockIconUpdateIntervalMs = 250; + void UIMachineLogic::updateDockIcon() { if (!isMachineWindowsCreated()) return; - if ( m_fIsDockIconEnabled - && m_pDockIconPreview) - if(UIMachineView *pView = machineWindows().at(m_DockIconPreviewMonitor)->machineView()) - if (CGImageRef image = pView->vmContentImage()) - { - m_pDockIconPreview->updateDockPreview(image); - CGImageRelease(image); - } + if ( !m_fIsDockIconEnabled + || !m_pDockIconPreview) + return; + + const uint64_t u64NowMs = RTTimeMilliTS(); + if ( m_u64LastDockIconUpdateMs + && u64NowMs - m_u64LastDockIconUpdateMs < s_u64DockIconUpdateIntervalMs) + return; + + if (UIMachineView *pView = machineWindows().at(m_DockIconPreviewMonitor)->machineView()) + if (CGImageRef image = pView->vmContentImage()) + { + m_u64LastDockIconUpdateMs = u64NowMs; + m_pDockIconPreview->updateDockPreview(image); + CGImageRelease(image); + } } void UIMachineLogic::updateDockIconSize(int screenId, int width, int height) @@ -790,6 +802,7 @@ UIMachineLogic::UIMachineLogic(UIMachine *pMachine) , m_pDockPreviewSelectMonitorGroup(0) , m_pDockSettingsMenuSeparator(0) , m_DockIconPreviewMonitor(0) + , m_u64LastDockIconUpdateMs(0) , m_pDockSettingMenuAction(0) #endif /* VBOX_WS_MAC */ , m_pHostLedsState(NULL) diff --git a/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.h b/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.h index c5bd35983335..dd5568598003 100644 --- a/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.h +++ b/src/VBox/Frontends/VirtualBox/src/runtime/UIMachineLogic.h @@ -407,6 +407,7 @@ private slots: QActionGroup *m_pDockPreviewSelectMonitorGroup; QAction *m_pDockSettingsMenuSeparator; int m_DockIconPreviewMonitor; + uint64_t m_u64LastDockIconUpdateMs; QAction *m_pDockSettingMenuAction; /* Keeps a list of machine menu actions that we add to dock menu. */ QList m_dockMachineMenuActions; From e4f1e18b2a1db3ed45473366b2ed1151831d168e Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Thu, 9 Jul 2026 01:03:50 +0200 Subject: [PATCH 2/2] GUI: keep macOS framebuffer paints opaque Signed-off-by: Roberto Nibali --- .../VirtualBox/src/runtime/UIFrameBuffer.cpp | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp b/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp index 1cc34a90e309..29d19445c677 100644 --- a/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp +++ b/src/VBox/Frontends/VirtualBox/src/runtime/UIFrameBuffer.cpp @@ -1317,8 +1317,6 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) /* Take the device-pixel-ratio into account: */ paintRectHiDPI.moveTo(paintRectHiDPI.topLeft() * devicePixelRatio()); paintRectHiDPI.setSize(paintRectHiDPI.size() * devicePixelRatio()); - const QRect requestedPaintRectHiDPI = paintRectHiDPI; - /* Make sure hidpi paint rectangle is within the image boundary: */ paintRectHiDPI &= pSourceImage->rect(); if (paintRectHiDPI.isEmpty()) @@ -1331,7 +1329,9 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) case UIVisualStateType_Scale: { QPainter painter(m_pMachineView->viewport()); + painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(paintRect, QColor(Qt::black)); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); break; } default: @@ -1340,10 +1340,9 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) #endif /* VBOX_WS_MAC */ return; } - const bool fPaintRectFullyCovered = paintRectHiDPI == requestedPaintRectHiDPI; - /* Create painter: */ QPainter painter(m_pMachineView->viewport()); + bool fRestoreSourceOverComposition = false; /* Depending on visual-state type: */ switch (m_pMachineView->visualStateType()) @@ -1352,19 +1351,18 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) case UIVisualStateType_Fullscreen: case UIVisualStateType_Scale: { -#if defined(VBOX_WS_MAC) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - /* On OSX for Qt5 we need to fill the backing store first: */ +#ifdef VBOX_WS_MAC + /* On macOS, make the destination explicitly opaque before drawing the guest image. + * The guest RGB32 backing buffer may carry transparent/undefined alpha bits for + * black pixels; drawing it with Source can copy those bits into the window backing + * store and make the boot framebuffer appear gray or washed out. */ painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(paintRect, QColor(Qt::black)); painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); -#elif defined(VBOX_WS_MAC) - /* Clear only newly exposed macOS backing-store regions. Qt 6 handles steady-state - * dirty repaints without this fill, but resize/fullscreen transitions can otherwise - * leave the default gray backing-store color outside the current guest image. */ - if (!fPaintRectFullyCovered) - painter.fillRect(paintRect, QColor(Qt::black)); -#endif /* VBOX_WS_MAC && Qt < 6 */ - + fRestoreSourceOverComposition = true; +#else + /* Nothing to do. */ +#endif /* VBOX_WS_MAC */ break; } case UIVisualStateType_Seamless: @@ -1402,6 +1400,8 @@ void UIFrameBufferPrivate::paintDefault(QPaintEvent *pEvent) drawImageRect(painter, *pSourceImage, paintRectHiDPI, m_pMachineView->contentsX(), m_pMachineView->contentsY(), devicePixelRatio()); + if (fRestoreSourceOverComposition) + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); /* If we had to scale image for some reason: */ if ( scaledSize().isValid()