Skip to content

perf: SG-43585: Performance improvement of pyevaluate and pyexec - #1375

Open
bernie-laberge wants to merge 1 commit into
AcademySoftwareFoundation:mainfrom
bernie-laberge:perf/qt6-webchannel-latency
Open

perf: SG-43585: Performance improvement of pyevaluate and pyexec#1375
bernie-laberge wants to merge 1 commit into
AcademySoftwareFoundation:mainfrom
bernie-laberge:perf/qt6-webchannel-latency

Conversation

@bernie-laberge

@bernie-laberge bernie-laberge commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Credit: the viewport refactor at the heart of this PR is Cédrik Fuoco's work — moving the
RV viewport off QOpenGLWidget and onto a native QOpenGLWindow, and every knock-on change that
followed from it (context sharing, presentation/second-output, blocking overlay). The additional
commits here are the startup-ordering fix and documentation on top of that foundation.

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 any
redundant 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 QGLWidget was removed in Qt6, so the viewport became a
QOpenGLWidget. A QOpenGLWidget renders to an offscreen FBO and forces the whole top-level window
onto 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 (new
GLWindow) embedded via QWidget::createWindowContainer. GLView becomes a plain QWidget facade
that delegates to it, so nothing outside RvCommon had to change.

Measured with the mre_plugin reproducer at a 150 ms interactive cadence (playback + real session
work in the handler):

Platform Before After
Linux (X11) ~15 ms ~1.4 ms ~10×
macOS ~7 ms ~0.9 ms ~8×
Windows not measured

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:

Tree What it is mean latency
A (main) QOpenGLWidget viewport ~15 ms
B one-line heartbeat fix, still QOpenGLWidget ~15 ms
C (this PR) native GL window + ordering fix ~1.4 ms

B − 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 deliberately
not 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() each
frame (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 QWebEngineView works, media loads; bridge latency
measured 15 → 1.4 ms at 150 ms cadence.

macOS — launches cleanly, session init/teardown and media load work, docked QWebEngineView
works; bridge latency measured 7 → 0.9 ms.

Not yet covered — please weigh this in review:

  • Windows has not been re-exercised since the startup-ordering commit, which changes launch
    ordering. It is also the platform whose synchronous initializeGL() masked both original startup
    bugs, so it is the most likely place for a surprise.
  • macOS feature sweep and an HDPI / mixed-DPI multi-monitor pass are still pending.
  • Presentation mode / second output was reworked to share a context explicitly and wants an
    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

  • New GLWindow : QOpenGLWindow (GLWindow.h/.cpp, 372 new lines) — takes over initializeGL,
    resizeGL, paintGL, event, eventProcessingTimeout, absolutePosition, readPixels,
    stopProcessingEvents, firstPaintCompleted, devicePixelRatioF.
  • GLView becomes a plain QWidget facade (−748 lines) — keeps sizeHint,
    minimumSizeHint, rvGLFormat, setContentSize; everything else forwards to m_glWindow. Its
    public surface is unchanged, so no caller outside RvCommon was touched.
  • QTGLVideoDevice gains a window-backed path — two constructors, m_view XOR m_window, plus
    backing-agnostic glShareContext() / glSurfaceFormat() so callers no longer care which backing
    the control view uses.

Knock-on changes forced by the new surface — these are the parts most worth a careful read:

  1. Qt::AA_ShareOpenGLContexts is now set before the QApplication (a documented QtWebEngine
    requirement). 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 that
    crashed with Multithread GPU Upload enabled.
  2. Presentation / second outputDesktopVideoDevice::open() no longer dereferences the control
    device's (now-null) QOpenGLWidget; ScreenView shares a QOpenGLContext through the new
    accessors.
  3. UI blocking overlay reworked from a raster child widget into a frameless translucent
    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 fixRvDocument::initializeSession() used to run from
GLWindow::initializeGL(), so constructing the RvSession (and loading every package, which runs Mu
and Python) depended on when Qt delivered the viewport's first expose. The platforms disagree on
that: on macOS a QOpenGLWindow has no GL context until first exposed, so the session was built with
no current context and aborted in IPCore::Shader::initGLSLVersion(); on Linux/X11 show() is
async, so doc->session() returned null a few lines later and RV segfaulted in
Session::queryAndStoreGLInfo() with this == nullptr (confirmed under gdb); Windows appeared to
escape both because initializeGL() fires synchronously during construction there. Now
RvApplication::newSessionFromFiles() calls initializeSession() right after show(), and
initializeSession() makes the viewport context current itself. There is a single RvDocument
creation site, so that is the whole ordering, and initializeGL() no longer calls back into the
document.

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 viewport
    window is a QObject child of the top-level's QWidgetWindow; with the hardware Qt Quick backend,
    adding a QWebEngineView makes Qt destroy and recreate that native subtree, deleting the embedded
    window rather than reparenting it, after which Qt's own QWindowContainer dereferences the window
    it 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

  • Native-window vs widget stacking. Anything that assumed it could draw over the viewport as a
    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.
  • HDPI. Coordinate translation now goes through the container widget while rendering goes through
    the window, so devicePixelRatio paths deserve a mixed-DPI pass.
  • Windows startup ordering, per the testing section above.

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

@bernie-laberge bernie-laberge changed the title Perf/qt6 webchannel latency perf: Perf/qt6 webchannel latency Aug 10, 2026
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
bernie-laberge force-pushed the perf/qt6-webchannel-latency branch from 5a52bdf to ded106d Compare August 10, 2026 18:12
@bernie-laberge bernie-laberge changed the title perf: Perf/qt6 webchannel latency perf: SG-43585: Performance improvement of pyevaluate and pyexec Aug 10, 2026
@bernie-laberge
bernie-laberge marked this pull request as ready for review August 10, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant