Skip to content

Replace IE WebBrowser output renderer with WebView2 (Chromium) - #3592

Open
jmcouffin wants to merge 12 commits into
developfrom
feature/webview2-output
Open

Replace IE WebBrowser output renderer with WebView2 (Chromium)#3592
jmcouffin wants to merge 12 commits into
developfrom
feature/webview2-output

Conversation

@jmcouffin

Copy link
Copy Markdown
Contributor

Summary

The script output window (ScriptConsole) now renders with Microsoft Edge
WebView2 (Chromium, Evergreen runtime)
instead of the legacy IE-based
System.Windows.Forms.WebBrowser control. All output-window behavior —
streaming stdout/stderr, html/markdown printing, tables, charts, progress,
freeze/unfreeze, save/copy/print, revit:// deep links, stdin — is preserved
behind the same public API, with a small set of intentional breaking changes
documented below.

Validated end-to-end on Revit 2023, 2024, 2025, 2026 and 2027 with the new
WebView2 Check diagnostic tool (17/17 checks on every version), plus manual
passes for streaming, audit-all dashboards, Chart.js rendering, deep links,
custom stylesheets, and both IronPython and CPython engines.


The critical finding: CEF ↔ WebView2 window-class collision

Revit hosts its own Chromium via CefSharp (libcef.dll) for internal UI
(Home page, Model Browser). WebView2 is a second Chromium embedded in the same
process. When the two are built on different Chromium major versions, both
register the same Win32 window classes (Chrome_WidgetWin_0, …) in the host
process; CreateWindowEx then fails for one of them and libcef fails a window
creation CHECK — surfacing as:

Error: Break instruction exception - code 80000003
Crashed Module Name: libcef.dll

…followed by a hang or hard crash of Revit. This took Revit 2023/2024/2025
down (older bundled CEF), while 2026/2027 (newer CEF, Chromium versions close
to the runtime's 151) coexisted fine — which made the failure look
version-flaky before the mechanism was identified.

Documented upstream:

Fix (Microsoft's official opt-in workaround): pass
--edge-webview-unique-window-class via CoreWebView2EnvironmentOptions.AdditionalBrowserArguments.
WebView2 then suffixes its app-process window class names
(Chrome_WidgetWin_0_EmbeddedBrowserWebView), removing the collision
regardless of the host CEF's Chromium version. Implemented as the default in
ScriptWebView.BuildBrowserArguments(); extra diagnostic flags can be appended
at runtime via the PYREVIT_WEBVIEW2_ARGS environment variable.

Debugging notes for posterity: the crash fired only when the output window's
HWND was created (i.e., on first render), never at environment creation; the
WebView2 user-data profile folder for the crashing version was sometimes
never created because the host died during the handshake. Both facts sent
the investigation down wrong paths (user-data-folder contention, GPU
contention, machine instability) before the window-class mechanism was
confirmed against the upstream issues.


Other engineering changes

Per-Revit-version browser profiles

%LocalAppData%\pyRevit\WebView2\<year> (parsed from the runtime assembly
name). Prevents multiple simultaneously-running Revit versions from attaching
to one shared browser process — a secondary source of startup instability.

Synchronous API over an asynchronous engine

The output API is synchronous and is called from engine threads, while
WebView2 is strictly async. ScriptWebView marshals every operation onto the
window dispatcher and blocks via a nested message pump (DispatcherFrame),
with ordered fire-and-forget inserts for the hot print path and pump-waiting
evaluations for DOM reads. Output entries are buffered per window and flushed
as single insertAdjacentHTML calls with JS-side stick-to-bottom scrolling.

Window visibility before controller creation

EnsureCoreWebView2Async defers until the WPF window has been shown (the
renderer's child HWND does not exist before Show()). EnsureDocumentReady
surfaces the window exactly like the first printed entry would, and
initialization failures are recorded so later calls fail fast instead of
re-waiting.

Chromium behavior differences handled

  • Script injection: <script> inserted via innerHTML never executes in
    Chromium. inject_script now builds real createElement('script') elements
    (charts/Chart.js depend on this).
  • Initial page: navigated via a temp .html file instead of
    NavigateToString, because Chromium blocks file:// subresources (custom
    stylesheets, print_image file URIs) from opaque origins.
  • Markdown links: the C# mini-markdown converter never supported
    [text](url); links are now converted (before emphasis patterns, so URLs
    are not mangled) and open in the default browser via the navigation policy.
  • Navigation policy moved from WebBrowser.Navigating to
    CoreWebView2.NavigationStarting (http(s) → shell, revit://
    ScriptConsoleUtils.ProcessUrl, file:// → in-window).
  • The Ctrl+C/Ctrl+A low-level keyboard hook is kept (Revit's accelerator table
    still eats the keystrokes) and routes to document.execCommand.
  • Print uses CoreWebView2.ShowPrintUI(); copy-all reads body.innerText.

Runtime availability handling

If the Evergreen runtime is missing, the window shows an explanatory page and
offers to open the Evergreen bootstrapper download. Initialization failures
are cached so subsequent calls fail fast.


Breaking changes (intentional, clean break)

  • output.renderer (Python) raises NotImplementedError with migration
    guidance; ScriptOutput.renderer and ScriptConsole.ActiveDocument
    (raw IE DOM leakage) are removed.
  • ScriptConsole.RendererVersion now reports the WebView2/Chromium runtime
    version; the rendererversion page meta carries it (the
    X-UA-Compatible IE meta is gone).
  • print_md markdown links now render as anchors (new capability, but
    scripts that relied on literal [text](url) output will see anchors).
  • Emoji shortcodes: the pyRevitLabs.Emojis dictionary is the conversion
    source and does not cover every name (white_check_mark/x are absent;
    heavy_check_mark/cross_mark work). Chromium renders literal Unicode
    emoji
    perfectly — prefer those in new scripts.
  • output.evaluate_js(script), output.renderer_engine,
    output.renderer_version are new public API (used by the preflight check tool, useful
    for extension authors).

Packaging & dependencies

  • Microsoft.Web.WebView2 1.0.4129.50 vendored into dev/libs/netfx and
    dev/libs/netcore (managed Core/WinForms/Wpf + x64 native
    WebView2Loader.dll), referenced via HintPath in the Runtime
    Directory.Build.targets; shipped by the existing dev\libs wildcard copy.
  • All four Inno Setup installers now call the previously dormant
    Dependency_AddWebView2 so installed deployments always have the Evergreen
    runtime. WiX picks the DLLs up automatically (HeatDirectory harvest of
    bin\).
  • WebView2-only by design: no IE fallback path remains. Machines without the
    runtime get the download dialog, not silent IE rendering.

New diagnostics tooling

pyRevitDev tab → Debug panel → WebView2 Check: 17 automated checks
against the live document — engine identity, runtime version, Chromium user
agent, head meta, inject_to_head/body, injected script execution,
add_style, markdown link conversion, revit:// anchors, progress-bar DOM,
legacy renderer removal, user-data folder, and emoji rendering through all
three paths (literal Unicode, Segoe UI Emoji font, DLL shortcodes). Results
print as a pass/fail table.


Testing

Check Result
Full pipeline build (build → dotnet run -c Release -- ci) ✅ all 7 Revit variants × IPY2712PR + IPY342
WebView2 Check tool ✅ 17/17 on Revit 2023, 2024, 2025, 2026, 2027
Streaming output / audit-all dashboard / charts
revit:// deep links (select / button / print)
Save / Open-in-browser / Copy / Print buttons
Ctrl+C / Ctrl+A keyboard hook
Custom stylesheets (set_stylesheet)
Multiple Revit versions running side by side ✅ (after per-version profiles + window-class fix)
Revit 2021/2022 (same netfx path as 2023/2024) built, not individually exercised

Rollout notes

  • The Evergreen runtime auto-updates; the --edge-webview-unique-window-class
    switch is honored by current runtimes and ignored (harmlessly) by older ones.
  • First launch per Revit version creates a new WebView2 profile under
    %LocalAppData%\pyRevit\WebView2\<year> (~tens of MB); stale profiles from
    removed versions can be deleted manually.
  • Machines on Windows Insider builds with old-CEF Revit versions should
    retest with this branch — the window-class fix specifically addresses the
    libcef break-exception crashes seen during validation.

- Introduced ScriptWebView class to manage WebView2 lifecycle and script execution.
- Updated ScriptOutput to utilize WebView2 for rendering, replacing legacy WebBrowser control.
- Added methods for JavaScript evaluation and renderer version retrieval.
- Refactored output handling in PyRevitOutputWindow to accommodate new rendering engine.
- Improved code formatting and consistency across output module.
@devloai

devloai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔
Please upgrade your plan or buy additional credits from the subscription page.

@jmcouffin
jmcouffin requested review from dosymep and romangolev and a balanced review from Copilot August 26, 2026 15:35
@jmcouffin jmcouffin added Backward Compatibility Backward Compatibility Issue with Older Revit Versions API Change Revit API change that breaks pyRevit [class->Upgraded #{number}: {title}] labels Aug 26, 2026
@jmcouffin
jmcouffin requested a review from Wurschdhaud August 26, 2026 15:56
@jmcouffin jmcouffin self-assigned this Aug 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 23 changed files in this pull request and generated 2 comments.

Comment thread dev/pyRevitLabs.PyRevit.Runtime/ScriptConsole.cs
Comment thread dev/pyRevitLabs.PyRevit.Runtime/ScriptConsole.cs Outdated
jmcouffin and others added 2 commits August 26, 2026 20:41
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@romangolev romangolev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with Revit 2021 - 2026

┌───────┬────────┬──────────────┬─────────────────┬────────────┐
│ Revit │  TFM   │ Bundled CEF  │ Profile created │  Outcome   │
├───────┼────────┼──────────────┼─────────────────┼────────────┤
│ 2021  │ net48  │ Chromium 65  │ 20:50:33        │ crash +16s │
├───────┼────────┼──────────────┼─────────────────┼────────────┤
│ 2022  │ net48  │ Chromium 65  │ 21:27:36        │ crash +21s │
├───────┼────────┼──────────────┼─────────────────┼────────────┤
│ 2023  │ net48  │ Chromium 92  │ 21:29:39        │ crash +21s │
├───────┼────────┼──────────────┼─────────────────┼────────────┤
│ 2024  │ net48  │ Chromium 105 │ 21:31:38        │ crash +21s │
├───────┼────────┼──────────────┼─────────────────┼────────────┤
│ 2025  │ net8.0 │ Chromium 119 │ 20:57:18        │ clean      │
├───────┼────────┼──────────────┼─────────────────┼────────────┤
│ 2026  │ net8.0 │ no libcef    │ 21:10:42        │ clean      │
└───────┴────────┴──────────────┴─────────────────┴────────────┘

I have a full debug mode and the highest verbose turned on at the moment of testing and a crash seems to be happening at the startup. Without debug mode it starts up fine. I'll try to apply a fix to it.

The previous implementation pumped a nested dispatcher frame inside WebView2 init to wait for the core, which re-entered Revit's message loop during session load and terminated the process. Schedule initialization via BeginInvoke, defer to the control's Loaded event when it isn't in the visual tree yet, and flush output from a DocumentReady event instead of polling. Reserve a blocking WaitUntilReady / EnsureDocumentReadyBlocking path for user commands only. Guard flush exceptions, log lifecycle events through NLog, and drop payloads with a one-time warning when the renderer is permanently broken.
Pull the inline rationale comments out of the ScriptConsole flush paths and the ScriptWebView lifecycle into Google-style <summary>, Important, Warning, and Invariant sections so the lifecycle invariants and re-entrancy hazards are documented where readers actually look. Also drop the now-unused waitForBrowser parameter from FlushPendingEntries, since every caller is fire-and-forget after the non-blocking init rework.
Direct _webView.PostScript calls were silently dropped whenever the document had not finished initializing yet — scroll-to-bottom, set-element-visibility, progress updates, inline-wait, and script/HTML injection all hit that path during session load. Add a PostScriptWhenReady helper that queues the script under the existing _pendingLock when IsDocumentReady is false and replays it in submission order from both DocumentReady and SyncFlushBlocking. Switch the affected callers over to the helper and drop the now-redundant EnsureDocumentReady calls they used to need.
@romangolev

Copy link
Copy Markdown
Member

@jmcouffin I got the crash settled,
but there is a bigger architectural question that pops up.

Do we ship WebView2 dlls straight away?
We cannot build our own WebView2 fork so there is no guarantee that they are going to be loaded and resolved correctly which may lead to the broken output window. There is an Evergreen that's settles in the profiles for WebView2 so it should minimize conflicts.

I believe there is nothing much we can do about except providing best effort. Revit 2026+ supports Dependency isolation for add-ins. I believe that this is something we are looking forward to use in order to minimize those possible collisions either in scope of this PR or in a scope of the follow-up PR

@jmcouffin

Copy link
Copy Markdown
Contributor Author

@jmcouffin

Copy link
Copy Markdown
Contributor Author

@romangolev Thanks for digging into this. You're right—we can't guarantee DLL resolution across add-ins without isolation. The Evergreen approach minimizes conflicts in practice (auto-updates, single system install), but it's not foolproof. For 7.0, I'm comfortable shipping with this as a known limitation and adding it to the release notes. We should plan dependency isolation for 2026+ in 7.1. For now, let's get this merged and gather real-world feedback on collisions. If we start seeing reports, we escalate the isolation work sooner.
Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API Change Revit API change that breaks pyRevit [class->Upgraded #{number}: {title}] Backward Compatibility Backward Compatibility Issue with Older Revit Versions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants