Skip to content

fix: SG-43585: Performance improvement of pyevaluate and pyexec - #1351

Open
cedrik-fuoco-adsk wants to merge 6 commits into
AcademySoftwareFoundation:mainfrom
cedrik-fuoco-adsk:perf/qt6-webchannel-latency
Open

fix: SG-43585: Performance improvement of pyevaluate and pyexec#1351
cedrik-fuoco-adsk wants to merge 6 commits into
AcademySoftwareFoundation:mainfrom
cedrik-fuoco-adsk:perf/qt6-webchannel-latency

Conversation

@cedrik-fuoco-adsk

@cedrik-fuoco-adsk cedrik-fuoco-adsk commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

SG-43585: Performance improvement of pyevaluate and pyexec

Linked issues

none

Summarize your change.

Two independent performance fixes for JS→Python web-panel bridge latency in Qt6:

  • Skip the redundant view-edit-mode deactivate→reactivate churn when a view change doesn't actually change the edit mode.

  • Stop a static (non-playback) askForRedraw() from opening the 2-second continuous-repaint "settle window".

Describe the reason for the change.

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. Two sources of avoidable GUI-thread work were the root cause:

  • 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 4× 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 even after the single needed frame had been drawn. In Qt5 each QGLWidget swapped its own back buffer cheaply, so this was nearly free; in Qt6 every redraw forces a full-window QOpenGLWidget recomposite + present, so the window saturated the GUI thread long after any real drawing was needed.

First commit cuts the time it takes to do a clearSession.
Second commit makes a static redraw cost a single frame instead of ~2 s of continuous composites.

Describe what you have tested and on which operating system.

Windows

Signed-off-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com>
Signed-off-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com>
Signed-off-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com>
@cedrik-fuoco-adsk cedrik-fuoco-adsk changed the title Perf/qt6 webchannel latency fix: SG-43585: Performance improvement of pyevaluate and pyexec Jul 16, 2026
Signed-off-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com>
@cedrik-fuoco-adsk
cedrik-fuoco-adsk marked this pull request as ready for review July 16, 2026 15:40
@cedrik-fuoco-adsk
cedrik-fuoco-adsk force-pushed the perf/qt6-webchannel-latency branch from a022cca to 67bcfcf Compare July 24, 2026 18:12
…atency

The Qt6/PySide6 migration regressed the JS->Python QWebChannel bridge in
docked QWebEngineView panels: pyevaluate round-trips grew unbounded (into the
seconds) while media plays, where Qt5 stayed flat (~8 ms). Root cause is the
per-presented-frame ~80-90 ms full-window QOpenGLWidget composite, which
serializes on the GUI thread and starves QWebChannel delivery. The two prior
fixes (edit-mode churn skip, settle-window removal) only address the
non-playing path, so the regression persisted whenever a handler started
playback.

Render the RV viewport as a native QOpenGLWindow (new GLWindow) embedded via
QWidget::createWindowContainer, instead of a QOpenGLWidget:

- The top-level QMainWindow is no longer forced onto the OpenGL RHI backend,
  so docked QWebEngineView panels render on the platform-default backend and
  QT_QUICK_BACKEND=software is no longer needed (removed).
- The viewport presents on its own surface, off the full-window widget
  composite, so playback no longer starves the bridge. Bridge latency stays
  flat and self-healing while playing.

GLView becomes a plain QWidget host that delegates the API RV expects to the
GLWindow; QTGLVideoDevice gains a window-backed path alongside the widget one.

Also fixed, exposed by the surface change:

- Presentation / second output: DesktopVideoDevice::open no longer dereferences
  the control device's (now-null) QOpenGLWidget; ScreenView shares a
  QOpenGLContext via new backing-agnostic QTGLVideoDevice::glShareContext()/
  glSurfaceFormat().
- Set Qt::AA_ShareOpenGLContexts (documented QtWebEngine requirement) so the
  second-output and upload-worker GL surfaces share with the viewport, and drop
  a null-context dereference in newSharedContextWorkerDevice that crashed when
  Multithread GPU Upload was enabled.
- 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.

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. macOS and HDPI passes still pending.

Signed-off-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com>
@cedrik-fuoco-adsk
cedrik-fuoco-adsk force-pushed the perf/qt6-webchannel-latency branch from 42d33f6 to 6879c31 Compare July 24, 2026 18:20
@cedrik-fuoco-adsk

Copy link
Copy Markdown
Contributor Author

While this commit (6879c31) seems promising in fixing the issue in my test, it's a shift in how GLView works, and it's experimental. I only tested it on Windows. @bernie-laberge @eloisebrosseau

From my observation and testing, the root cause seems to be the following:

GLView was a QOpenGLWidget, which renders into an offscreen FBO that the top-level QMainWindow must re-composite every frame (~90 ms). That composite runs on the same GUI event loop that delivers QWebChannel messages, so at ~120Hz the bridge reply queues behind an ever-growing backlog of presents.

The fix:
Make the viewport a native QOpenGLWindow (new GLWindow) embedded via QWidget::createWindowContainer. It presents on its own surface via swapBuffers, decoupled from the main window's paint. The GUI thread stays free to dispatch QWebChannel promptly.

pr-diagram

Why Qt5 didn't have this problem

Before the Qt6 port, RV's viewport was a QGLWidget, which owned its own native window and GL surface and presented (swapBuffers) directly to the screen. Essentially the same model we're returning to with QOpenGLWindow. There was no offscreen FBO and no full-window re-composite, so the per-frame present never ran on the shared GUI-thread composite path and never serialized against QWebChannel delivery. Playback and the JS bridge ran independently.

QGLWidget was removed in Qt6, so the port replaced it with QOpenGLWidget. Unlike its predecessor, QOpenGLWidget renders into an offscreen FBO that the top-level QMainWindow must composite into its backing store every frame on the GUI thread (~90 ms). That new per-frame composite is exactly what starves the bridge during playback.

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