perf: SG-43585: Performance improvement of pyevaluate and pyexec - #1375
Open
bernie-laberge wants to merge 1 commit into
Open
perf: SG-43585: Performance improvement of pyevaluate and pyexec#1375bernie-laberge wants to merge 1 commit into
bernie-laberge wants to merge 1 commit into
Conversation
After the Qt5->Qt6 migration, JS<->Python web-panel bridge round-trips (pyevaluate / pyexec) became slow and grew unbounded whenever the handler did RV work such as clearSession() / addSourceVerbose(). The bridge is synchronous on the Qt main (GUI) thread, so any redundant work RV does on that thread directly delays delivery of queued bridge calls. Three sources of avoidable GUI-thread work are addressed: - Redundant edit-mode churn. clearSession() sets the default view twice with force=true; each before/after view-change event toggled the same view edit mode off and back on (up to 4x per clear), rebuilding menus and event tables for no net change. - Settle-window repaint storm. A static askForRedraw() started a 2-second window that kept isUpdating() true, so the ~120 Hz heartbeat kept repainting for ~2 s after the single needed frame had been drawn. In Qt5 each QGLWidget swapped its own back buffer cheaply; in Qt6 every redraw forces a full-window QOpenGLWidget recomposite + present. - Per-presented-frame full-window composite. The viewport now renders as a native QOpenGLWindow (new GLWindow) embedded via QWidget::createWindowContainer instead of a QOpenGLWidget, so the top-level QMainWindow is no longer forced onto the OpenGL RHI backend and the viewport presents on its own surface. Docked QWebEngineView panels render on the platform-default backend and QT_QUICK_BACKEND= software is no longer needed. GLView becomes a plain QWidget host delegating to GLWindow; QTGLVideoDevice gains a window-backed path. Also fixed, exposed by the surface change: - DesktopVideoDevice::open no longer dereferences the control device's (now-null) QOpenGLWidget; ScreenView shares a QOpenGLContext via new backing-agnostic QTGLVideoDevice::glShareContext()/glSurfaceFormat(). - Qt::AA_ShareOpenGLContexts is set (documented QtWebEngine requirement) so second-output and upload-worker GL surfaces share with the viewport, and a null-context dereference in newSharedContextWorkerDevice that crashed with Multithread GPU Upload enabled is removed. - The UI blocking overlay is reworked from a raster child widget into a frameless translucent top-level window so it can dim and block input over the native viewport. - crashpad's MSVC toolset is pinned to the parent build (fixes LNK2019). - Session initialization is guarded on a current GL context: constructing an RvSession queries GL_SHADING_LANGUAGE_VERSION, which aborts with no current context. A QOpenGLWindow has no context until first exposed, so the constructor's retry now runs only when GLWindow::initializeGL() already bailed out early (Windows), leaving macOS to initialize from initializeGL() itself. Without this RV aborted at launch on macOS. Validated on Windows (release): flat bridge latency under playback spam, all web panels render docked, and viewport/events/drag-drop/annotations/ presentation/blocking-overlay all work. Validated on macOS: launch and session init/teardown. macOS feature and HDPI passes still pending. Co-authored-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com> Signed-off-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Bernard Laberge <bernard.laberge@autodesk.com> fix: SG-43585: initialize the session outside the GL callback RvDocument::initializeSession() ran from GLWindow::initializeGL(), so creating the RvSession -- and with it loading every package, which runs Mu and Python -- depended on when Qt happened to deliver the viewport window's first expose. That is not something the platforms agree on, and it broke two of them differently. Linux (X11): QWidget::show() is asynchronous, so no expose had arrived by the time RvApplication::newSessionFromFiles() reached doc->session() a few lines later. That returned null and RV segfaulted at startup in Session::queryAndStoreGLInfo() with this == nullptr, before any web panel or script was involved. Confirmed under gdb, and by the crash simply moving to the next use of the null session (rebuildSessionFromFiles) when that call was skipped. macOS: a QOpenGLWindow has no GL context until it is first exposed, so the session was constructed with no current context and RV aborted in IPCore::Shader::initGLSLVersion() when glGetString(GL_SHADING_LANGUAGE_VERSION) returned null. Windows appeared to escape both because initializeGL() fires synchronously while the window is being constructed there. Have the application drive it instead: newSessionFromFiles() calls initializeSession() immediately after show(), and initializeSession() makes the viewport context current itself before constructing the RvSession. There is a single RvDocument creation site, so this is the whole ordering. initializeGL() no longer calls back into the document, which also retires the m_sessionInitPending retry that worked around the same fragility from the other end. Validated on Linux: launches cleanly, and a docked QWebEngineView works. On macOS: launches cleanly and media loads. Not yet exercised on Windows, where this changes startup ordering as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Bernard Laberge <bernard.laberge@autodesk.com> docs: SG-43585: record why macOS keeps the software Qt Quick backend The QT_QUICK_BACKEND comments no longer described reality. Both files still explained the setting as a Qt 5.12.1 workaround for QWebEngineView conflicting with the QGLWidget viewport, which has not been the reason since the viewport became a native QOpenGLWindow. src/bin/apps/rv/main.cpp (Windows, Linux): drop the block outright. The call was already commented out, so all that remained was a commented-out line plus a rationale for a decision the code no longer makes. Linux has since confirmed the setting is not needed there -- it launches and runs docked web panels without it. src/bin/nsapps/RV/main.cpp (macOS): the setting stays, and the comment now says why, because it is load bearing rather than vestigial. The viewport window is embedded with QWidget::createWindowContainer, making it a QObject child of the top level window's QWidgetWindow. With the hardware Qt Quick backend, adding a QWebEngineView makes Qt destroy and recreate that native subtree; the viewport window is deleted with it rather than reparented, and QWindowContainer then dereferences the window it just lost, so RV segfaults inside Qt. Measured on macOS with the four line reproducer now quoted in the comment: software backend survives 2/2, hardware backend crashes 2/2. The cost is that macOS web panels composite in software, so macOS gets the cheap viewport repaint half of this work but not the hardware web panel half. Noted in the comment so the asymmetry with Windows and Linux reads as deliberate. Comment-only; no compiled behaviour changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Bernard Laberge <bernard.laberge@autodesk.com>
bernie-laberge
force-pushed
the
perf/qt6-webchannel-latency
branch
from
August 10, 2026 18:12
5a52bdf to
ded106d
Compare
bernie-laberge
marked this pull request as ready for review
August 10, 2026 18:40
bernie-laberge
requested review from
cedrik-fuoco-adsk and
eloisebrosseau
as code owners
August 10, 2026 18:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issues
No GitHub issue — tracked internally as SG-43585
Summarize your change.
After the Qt5 → Qt6 migration, JS ↔ Python web-panel bridge round-trips (
pyevaluate/pyexec)became slow whenever the JavaScript handler did real RV work such as
clearSession()/addSourceVerbose(). The bridge is delivered synchronously on the Qt main (GUI) thread, so anyredundant work RV does on that thread directly delays queued bridge calls.
Nothing about the bridge itself changed between Qt5 and Qt6 — QWebChannel, QtWebEngine, the
2-second
askForRedraw()settle window and the 120 Hz heartbeat are all identical in both trees.The one difference that matters is that
QGLWidgetwas removed in Qt6, so the viewport became aQOpenGLWidget. AQOpenGLWidgetrenders to an offscreen FBO and forces the whole top-level windowonto the OpenGL/RHI path, so every viewport repaint recomposites every widget in the window —
toolbars, docks, and the web panel — and presents the whole window. Same redundant redraws as Qt5,
roughly 10× the cost each, on exactly the thread the bridge needs.
This PR gives the viewport its own surface back: it now renders as a native
QOpenGLWindow(newGLWindow) embedded viaQWidget::createWindowContainer.GLViewbecomes a plainQWidgetfacadethat delegates to it, so nothing outside
RvCommonhad to change.Measured with the
mre_pluginreproducer at a 150 ms interactive cadence (playback + real sessionwork in the handler):
Describe the reason for the change.
A user reported that web-panel bridge calls became slow after the Qt6 migration. A three-way
isolation on Linux at interactive cadence pinned the cause to the surface, not the bridge and not
the repaint count:
main)QOpenGLWidgetviewportQOpenGLWidgetB − A ≈ 0, C − B ≈ −13.6 ms. The entire win is the surface change. A separate one-line
heartbeat fix (
isUpdating()→isPlaying() || m_wantsRedraw) was evaluated and deliberatelynot included here: it measured no benefit at interactive cadence, and it risks stopping
animation for anything that redraws during the settle window without calling
askForRedraw()eachframe (cache/buffer progress indicators first). It can land separately on its own merits.
Describe what you have tested and on which operating system.
Windows (Release) — the viewport refactor itself: flat bridge latency under playback spam, all
web panels render docked, and viewport / events / drag-and-drop / annotations / presentation /
blocking-overlay all work.
Linux (X11) — launches cleanly, docked
QWebEngineViewworks, media loads; bridge latencymeasured 15 → 1.4 ms at 150 ms cadence.
macOS — launches cleanly, session init/teardown and media load work, docked
QWebEngineViewworks; bridge latency measured 7 → 0.9 ms.
Not yet covered — please weigh this in review:
ordering. It is also the platform whose synchronous
initializeGL()masked both original startupbugs, so it is the most likely place for a surprise.
end-to-end test on real multi-display hardware.
Add a list of changes, and note any that might need special attention during the review.
The refactor
GLWindow : QOpenGLWindow(GLWindow.h/.cpp, 372 new lines) — takes overinitializeGL,resizeGL,paintGL,event,eventProcessingTimeout,absolutePosition,readPixels,stopProcessingEvents,firstPaintCompleted,devicePixelRatioF.GLViewbecomes a plainQWidgetfacade (−748 lines) — keepssizeHint,minimumSizeHint,rvGLFormat,setContentSize; everything else forwards tom_glWindow. Itspublic surface is unchanged, so no caller outside
RvCommonwas touched.QTGLVideoDevicegains a window-backed path — two constructors,m_viewXORm_window, plusbacking-agnostic
glShareContext()/glSurfaceFormat()so callers no longer care which backingthe control view uses.
Knock-on changes forced by the new surface — these are the parts most worth a careful read:
Qt::AA_ShareOpenGLContextsis now set before theQApplication(a documented QtWebEnginerequirement). It also lets second-output and upload-worker GL surfaces share with the viewport
without an ordering-sensitive
setShareContext(), and retires a null-context dereference thatcrashed with Multithread GPU Upload enabled.
DesktopVideoDevice::open()no longer dereferences the controldevice's (now-null)
QOpenGLWidget;ScreenViewshares aQOpenGLContextthrough the newaccessors.
top-level window. A native window renders above sibling raster widgets regardless of
raise()order, so the old overlay could no longer dim the viewport.
Startup-ordering fix —
RvDocument::initializeSession()used to run fromGLWindow::initializeGL(), so constructing theRvSession(and loading every package, which runs Muand Python) depended on when Qt delivered the viewport's first expose. The platforms disagree on
that: on macOS a
QOpenGLWindowhas no GL context until first exposed, so the session was built withno current context and aborted in
IPCore::Shader::initGLSLVersion(); on Linux/X11show()isasync, so
doc->session()returned null a few lines later and RV segfaulted inSession::queryAndStoreGLInfo()withthis == nullptr(confirmed under gdb); Windows appeared toescape both because
initializeGL()fires synchronously during construction there. NowRvApplication::newSessionFromFiles()callsinitializeSession()right aftershow(), andinitializeSession()makes the viewport context current itself. There is a singleRvDocumentcreation site, so that is the whole ordering, and
initializeGL()no longer calls back into thedocument.
QT_QUICK_BACKEND=software— deliberately asymmetric, please do not "clean this up":src/bin/apps/rv/main.cpp(Windows, Linux) — the block is dropped. It was already commented out,and Linux has since confirmed the setting is not needed.
src/bin/nsapps/RV/main.cpp(macOS) — the setting stays and is load bearing. The viewportwindow is a
QObjectchild of the top-level'sQWidgetWindow; with the hardware Qt Quick backend,adding a
QWebEngineViewmakes Qt destroy and recreate that native subtree, deleting the embeddedwindow rather than reparenting it, after which Qt's own
QWindowContainerdereferences the windowit lost. Measured with the four-line reproducer now quoted in the comment: software survives 2/2,
hardware crashes 2/2. The cost is that macOS web panels composite in software — macOS gets the
cheap-viewport half of this win, not the hardware-web-panel half.
Also in the diff: crashpad's MSVC toolset is pinned to the parent build (fixes an LNK2019).
Known risk areas for review
sibling widget needs checking — the blocking overlay already had to become a top-level window.
Annotations, HUD-style widgets and full-screen transitions are the places to look.
the window, so
devicePixelRatiopaths deserve a mixed-DPI pass.If possible, provide screenshots.
A slide deck walking through the refactor class by class is attached:
SG-43585_viewport_refactor.pptx.SG-43585_viewport_refactor.pptx