Repository navigation
feat(window): working background opacity and blur on macOS, Windows and Linux - #36580
Conversation
4edcb65 to
040af97
Compare
Phase 2: cross-platform verification on real machinesI built this branch on each host from a fresh clone and ran it as a visible window. Every host used its own profile and its own debugging port. All screenshots are real OS captures (
Migration from the old Window Blur switch (old shape
Performance (visible windows):
WebGL / canvas leak check (toggling GPU acceleration off and on six times):
Pre-existing, dev-only, not caused by this PR: on Linux, Orca terminal shells kept alive across an app restart inherit the main process's Linux GNOME translucent (dark, light, resized, modal) · Reduce Visual Effects · Windows 11 over RDP: |
Phase 3: macOS visible-window verification and cost (m4air)m4air (macOS 26.6.2, Apple M4) was unlocked for this round. I built this branch ( Dark: origin/main · Opaque · Translucent · Blurred Light: origin/main · Opaque · Translucent · Blurred Modals, Reduce Visual Effects, macOS Reduce transparency Resize (Translucent, dark and light) and a zoomed Blurred terminal (light on top, dark below; the pattern's text is unreadable and only the colour bleeds through):
Cost
Each phase is 20 samples:
The window was 1300×820 pt and in front. A mid-run screenshot confirmed this for every sample. Values are medians of 5–7 runs, and the order was interleaved across 3 rounds. Other agents were using the machine at the same time, so single runs are noisy. With no dev app running, GPU was 0.0–0.1% (2.6% once).
Not done this round
No code bugs found, so there's no code change in this round. |
Phase 3 addendum:
|
| Config | Idle GPU power | Idle GPU active residency | Stream GPU power | Stream GPU residency | Stream WindowServer CPU (ms/s) | Stream Orca CPU (ms/s) |
|---|---|---|---|---|---|---|
| No dev app running (machine baseline) | 50 mW | 6.1% | n/a | n/a | n/a | n/a |
| origin/main (opacity 0.7 + Window Blur → opaque window) | 36 mW | 6.5% | 536 mW | 47.2% | 137 | 480 |
| PR Opaque (opacity 1) | 39 mW | 6.9% | 545 mW | 46.6% | 135 | 481 |
| PR Blurred (0.7) | 49 mW | 8.4% | 805 mW | 57.1% | 157 | 503 |
| PR Translucent (0.7) | 313 mW (50–890) | 35.6% (5.6–55.7) | 655 mW | 83.3% | 73 | 507 |
Ranges are shown where runs disagreed a lot.
Raw per-round samples
r1 none(no Orca Dev) idle GPUpower_mW=103 GPUresidency%=17.9 Combined_mW=5637 WindowServer_CPUms/s=53.4 Orca_CPUms/s=0.0
r1 pr-blurred(op.7) idle GPUpower_mW=144 GPUresidency%=20.4 Combined_mW=4638 WindowServer_CPUms/s=57.0 Orca_CPUms/s=6.6
r1 pr-blurred(op.7) stream GPUpower_mW=946 GPUresidency%=59.4 Combined_mW=10194 WindowServer_CPUms/s=148.3 Orca_CPUms/s=558.0
r1 pr-translucent(op.7) idle GPUpower_mW=890 GPUresidency%=55.7 Combined_mW=11209 WindowServer_CPUms/s=107.1 Orca_CPUms/s=3.0
r1 pr-translucent(op.7) stream GPUpower_mW=1184 GPUresidency%=87.6 Combined_mW=12298 WindowServer_CPUms/s=95.0 Orca_CPUms/s=590.1
r1 pr-opaque(op1) idle GPUpower_mW=45 GPUresidency%=7.9 Combined_mW=4217 WindowServer_CPUms/s=32.9 Orca_CPUms/s=5.1
r1 pr-opaque(op1) stream GPUpower_mW=572 GPUresidency%=46.6 Combined_mW=8849 WindowServer_CPUms/s=124.8 Orca_CPUms/s=508.8
r1 main(op.7,blur-switch) idle GPUpower_mW=48 GPUresidency%=7.6 Combined_mW=3473 WindowServer_CPUms/s=30.7 Orca_CPUms/s=7.5
r1 main(op.7,blur-switch) stream GPUpower_mW=551 GPUresidency%=47.2 Combined_mW=6710 WindowServer_CPUms/s=140.2 Orca_CPUms/s=480.2
r2 none(no Orca Dev) idle GPUpower_mW=50 GPUresidency%=6.1 Combined_mW=2415 WindowServer_CPUms/s=27.3 Orca_CPUms/s=0.0
r2 pr-blurred(op.7) idle GPUpower_mW=49 GPUresidency%=8.4 Combined_mW=2209 WindowServer_CPUms/s=34.8 Orca_CPUms/s=6.7
r2 pr-blurred(op.7) stream GPUpower_mW=805 GPUresidency%=57.1 Combined_mW=7646 WindowServer_CPUms/s=156.8 Orca_CPUms/s=502.5
r2 pr-translucent(op.7) idle GPUpower_mW=313 GPUresidency%=35.6 Combined_mW=2157 WindowServer_CPUms/s=56.4 Orca_CPUms/s=7.0
r2 pr-translucent(op.7) stream GPUpower_mW=655 GPUresidency%=83.3 Combined_mW=7511 WindowServer_CPUms/s=73.3 Orca_CPUms/s=507.0
r2 pr-opaque(op1) idle GPUpower_mW=39 GPUresidency%=6.9 Combined_mW=2102 WindowServer_CPUms/s=30.2 Orca_CPUms/s=6.7
r2 pr-opaque(op1) stream GPUpower_mW=545 GPUresidency%=47.5 Combined_mW=6788 WindowServer_CPUms/s=136.2 Orca_CPUms/s=480.7
r2 main(op.7,blur-switch) idle GPUpower_mW=36 GPUresidency%=6.5 Combined_mW=3135 WindowServer_CPUms/s=27.4 Orca_CPUms/s=6.7
r2 main(op.7,blur-switch) stream GPUpower_mW=536 GPUresidency%=47.6 Combined_mW=6910 WindowServer_CPUms/s=137.4 Orca_CPUms/s=484.3
r3 none(no Orca Dev) idle GPUpower_mW=13 GPUresidency%=3.9 Combined_mW=1906 WindowServer_CPUms/s=25.5 Orca_CPUms/s=0.0
r3 pr-blurred(op.7) idle GPUpower_mW=21 GPUresidency%=4.5 Combined_mW=1096 WindowServer_CPUms/s=9.1 Orca_CPUms/s=10.0
r3 pr-blurred(op.7) stream GPUpower_mW=135 GPUresidency%=55.5 Combined_mW=4874 WindowServer_CPUms/s=164.8 Orca_CPUms/s=466.1
r3 pr-translucent(op.7) idle GPUpower_mW=50 GPUresidency%=5.6 Combined_mW=522 WindowServer_CPUms/s=8.1 Orca_CPUms/s=9.9
r3 pr-translucent(op.7) stream GPUpower_mW=584 GPUresidency%=83.3 Combined_mW=6937 WindowServer_CPUms/s=64.8 Orca_CPUms/s=488.7
r3 pr-opaque(op1) idle GPUpower_mW=2 GPUresidency%=0.4 Combined_mW=1429 WindowServer_CPUms/s=3.6 Orca_CPUms/s=4.4
r3 pr-opaque(op1) stream GPUpower_mW=516 GPUresidency%=44.6 Combined_mW=6450 WindowServer_CPUms/s=134.9 Orca_CPUms/s=471.1
r3 main(op.7,blur-switch) idle GPUpower_mW=4 GPUresidency%=0.7 Combined_mW=1297 WindowServer_CPUms/s=4.3 Orca_CPUms/s=4.5
r3 main(op.7,blur-switch) stream GPUpower_mW=522 GPUresidency%=44.5 Combined_mW=7306 WindowServer_CPUms/s=130.2 Orca_CPUms/s=478.4
- Opaque (the default) matches origin/main on every metric.
- Blurred:
- idle is at noise level (+10 mW);
- under sustained full-speed output, GPU residency is about +10 points and WindowServer CPU about +16%;
- GPU power was 805 mW median, but the samples were noisy (135–946 mW).
- Translucent:
- Under output, GPU residency is about 83% vs 47%.
- The idle cost depends on what is changing behind the window. It was 890 / 313 / 50 mW across rounds, and the 50 mW round was the one where the machine (and the live Orca window behind ours) was quiet.
- With the window hidden it was 0, as measured before.
- WindowServer CPU drops under output because it becomes GPU-bound.
- This confirms the earlier
ioregfinding: on macOS Translucent is the costly option and Blurred the cheap one.
No code bugs found and no code changes. omarchy (Hyprland) is still behind its password lock screen. See the follow-up when it's unlocked.
Phase 3: Hyprland (omarchy) and perf scanHyprland 0.56 (omarchy, Wayland). These are real
Zoomed terminal area: Translucent with Hyprland blur off (left: sharp wallpaper) and on (right: blurred).
Perf scan over the whole diff: no findings. The PR body now opens with a self-contained Perf section. It covers:
The only renderer-side delta with the material on is about +7.5 ms/s of compositing during full-speed output (about 0.13 ms per frame). Total renderer work is unchanged. |
50db96a to
5b71d48
Compare
…nd Linux One window material model (opaque / translucent / blurred): macOS vibrancy without transparent, Windows 11 22H2+ acrylic, Linux transparent windows. Terminals paint their translucent background once, the transparent WebGL frame clears before drawing (#6491), and Reduce Visual Effects drops modal/backdrop blur and honors prefers-reduced-transparency.
af51398 to
c21f61f
Compare













Perf
TL;DR:
Where the cost comes from (plain language)
Which setting to choose
Window material + theme image (macOS,
sudo powermetrics)On a vibrancy window, the hidden material's cost shows up only under full-speed output. Three interleaved rounds of Blurred + Dusk test image vs Opaque + the same image:
With this change (vibrancy dropped while the image is shown), Blurred + image measured 55.9% and 150 ms/s, the same as Opaque + image. That's about 6.5 points of GPU residency and about 7% WindowServer CPU saved under heavy output, and nothing at idle. Restore was verified on screen: removing the image brings the blurred desktop back.
What was scanned (perf-scan over the full diff vs origin/main)
Paths reviewed, with their cost:
clear+blendFuncSeparateper rendered frame, only whenallowTransparency.applyTerminalAppearance: one extra theme composition per appearance apply, not per frame. It is value-gated, so a no-op apply doesn't rebuild the palette.main.cssrules::root[data-window-material]and:root[data-reduce-effects]selectors match nothing unless the attribute is set.backdrop-filter: nonerule applies only under Reduce Visual Effects.useMediaQuery/useReduceVisualEffects: onematchMedialistener per window (main, space, popout).activeWindowMaterial(): cached once per renderer.TabGroupPanel/TabGroupSplitLayout: constant attribute reads.resolveWindowMaterialwith oneos.release()and onenativeThemeread at window creation.Findings: none. I found no repeated, amplified or abandoned work. No per-frame JS, no new timers or polls, no growth with panes or terminals, and nothing runs when the feature is off. Off-path proof: with the material off the window gets no new option and no new argument, and every new CSS rule is attribute-gated.
macOS (Apple M4, macOS 26.6.2):
sudo powermetrics, visible 1300×820 window in front of a test patternMedian of 3 interleaved 20 s rounds, dark theme. Stream load is
yes '<80-char line>'into the terminal at full speed.ioregAGX-utilization run (5–7 runs per config) agrees: idle GPU utilization was 5.8% Opaque, 10.9% Blurred and 40.4% Translucent.Linux
Hyprland 0.56 (omarchy), Intel UHD 770, Wayland, visible tiled window. GPU busy is computed from i915 RC6 residency. Hyprland's blur was enabled at runtime only for the "+ blur" rows. The machine already had a ~42% GPU-busy baseline from other clients. Two rounds per config, 20 s each:
GNOME/Mutter X11 (openclaw, phase 2): compositor CPU idle 0.2–0.3% for both Opaque and Translucent. Under heavy output it is 22% → 37% (+15% of a core) for Translucent.
Windows 11 25H2 (awin, phase 2, over RDP)
Opaque vs acrylic:
These numbers are thin. Over RDP, DWM draws acrylic as its solid fallback tint, not live blur. A console-session measurement on Windows is still open.
Renderer-side cost (Orca's own work)
CDP trace on omarchy: a visible window, a dev build, and the same terminal workload. The trace categories were
devtools.timeline,devtools.timeline.frame,viz,gpuandcc.Performance.getMetricscovered layout, style, script and task. Each row is the mean of 2 rounds over 10 s of streaming, in ms of work per second (60 frames/s in every config).Limitations:
Summary
Background Opacity and Window Blur did nothing useful on any platform. This replaces the old macOS-skipping, Windows-only blur switch with one window material model — Opaque / Translucent / Blurred — that actually shows the desktop through translucent terminals on macOS, Windows and Linux. It also adds Reduce Visual Effects.
Root causes (one fix each)
createMainWindowalways set an opaquebackgroundColor; blur only mapped to Windows acrylic, and macOS/Linux had nothing behind the page.html/bodypainted an opaque--backgroundover whatever material the window had..xterm,.xterm-scrollable-elementand the WebGL canvas viewport rect. So0.82rendered as1 − 0.18³ ≈ 0.994everywhere, including Windows.SRC_ALPHAon the alpha channel turned opacityaintoa².What changed
src/shared/window-material.tsis the single model. It covers the stored preference (with a migration from the legacywindowBackgroundBlur), per-host capability, and resolution. The material is only engaged when something actually shows through: Background Opacity < 1, Reduce Visual Effects off, and the OS Reduce Transparency setting off. An occluded vibrancy or alpha window is pure WindowServer cost ([Bug]: Window Blur causes sustained high GPU power and heat on macOS #8482), so that case now resolves to Opaque.window-material-options.tssets the creation-timeBrowserWindowoptions per platform:vibrancy: 'under-window',visualEffectState: 'active'and a clearbackgroundColor. It deliberately sets notransparent: true: that flag suppressed the effect view ([Bug]: "Background Opacity" and "Window Blur" have no visible effect on macOS #8797) and forced full-window alpha compositing ([Bug]: Window Blur causes sustained high GPU power and heat on macOS #8482).activekeeps the glass when Orca is unfocused instead of turning flat grey.transparent: truewith a clear background, for Ghostty-style unblurred see-through.backgroundMaterial: 'acrylic'with a clear background. It is offered only on Windows 11 22H2+ (build 22621), where DWM system backdrops exist. Older builds keep Opaque instead of painting black.transparent: truewith a clear background. KWin, Mutter, Hyprland and picom composite it and apply their own blur rules. A stored "Blurred" maps to this.--orca-window-material=argument, which preload exposes asplatform.get().windowMaterial. It is never inferred from settings, so toggling before restart can't paint a clear page on an opaque window. When the material is off no argument is passed, so the window is byte-identical to today.:root[data-window-material] bodyis transparent and the pane split root keeps thergba(theme, opacity)paint. xterm gets the same theme background with alpha 0, which covers the DOM viewport, the scrollable element and the WebGL viewport rect in one place, with no!importantoverrides. RGB is preserved, so minimum-contrast and inverse video still use the real background colour. With the material off, nothing changes: same xterm theme, same layers, same cost.config/patches/xterm-src/@xterm__addon-webgl@0.20.0-beta.300.src.patch, built on fix(terminal): prevent transparent WebGL flicker (#6491) #6597. WhenallowTransparencyis set, each frame uses separate alpha blending (SRC_ALPHA, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA) and clears the framebuffer before drawing. Opaque terminals skip the block. The bundles and lockfile hash were regenerated withconfig/scripts/regenerate-xterm-patches.mjs --write.data-reduce-effectsswitch. The OSprefers-reduced-transparencysets the same switch. It does four things:filter: blur, the composer lag in [Feature]: option to disable modal backdrop blur #20936);backdrop-filter;--floating-surface,--dialog-surface, and the existing reduced-transparency menu fallbacks, now areduce-effects:variant);It applies to the main window, the workspace space window and the dashboard popout.
usePrefersReducedMotionnow shares a smalluseMediaQueryhook instead of a copy.background-blur(bool / radius /macos-glass-*) and the legacybackground-blur-radiusto Blurred or Translucent.windowMaterialis unset for existing profiles, sowindowBackgroundBlur: truereads as Blurred.windowMaterialalso mirrorswindowBackgroundBlurat the settings-store boundary (from the select or a Ghostty import), so a rollback to an older Orca keeps the closest behaviour.Closes #8797
Closes #36196
Closes #6491
Closes #20936
Refs #19301 (window/terminal transparency part; app-chrome transparency is not in scope)
Refs #23305 (opacity part only; no bindable toggle command added)
Refs #24751 (not addressed: per-panel opacity for the floating workspace is a separate in-window setting)
Supersedes / builds on #2791 by @edwintuan, #3972 by @MuathJ, #12470 by @nwparker, #24359 by @pawel-kotowski, #5425 by @TomBonness, #6597 by @nwparker, #36197 by @AdolfoCarneiro and #10349 by @innocarpe. #24359's finding that vibrancy works over a clear background without
transparent: trueis the macOS design here. #36197's Linux transparent window and single-paint analysis is the Linux design. #6597 is the WebGL fix, ported to the new source-patch pipeline.Visual proof
CDP screenshots carry the renderer's alpha but not the desktop behind the window. A background-launched test window is never shown, so the native vibrancy/acrylic pass can't be captured without stealing focus. Each "after" below is the real CDP PNG alpha composited over a synthetic wallpaper; the after images use a pre-blurred copy to stand in for vibrancy. The "before" images are origin/main with the same settings (Background Opacity 0.82, Window Blur on).
Dark (before → after)

Light (before → after)

Pixel alpha (CDP, opacity 0.82):
rgb(46,50,60)(≈0.994 stacked)rgba(40,44,52,0.82)exactlyLayers painting the terminal background, from DOM inspection:
.xterm-scrollable-element,.xterm, split root (each 0.82), plusbodyand the WebGL rect.rgba(…, 0).WebGL flicker (#6491): an empty cell sampled after six successive redraws, Background Opacity 0.3, material off, WebGL renderer:
rgb(38,42,51)— canvas alpha has already accumulated to ~1 (the configured 0.3 is lost)rgb(32,35,43)— stable, matching the analytic composite of the configured alphaReduce Visual Effects, New Workspace composer open, dark and light (default → reduced: no workspace blur):


Setting + reused restart banner

Merge-risk assessment
Low (was Medium-low, before that Medium). All three desktop OSes are now verified on real hardware with visible windows. Evidence is in the phase-2 comment and the phase-3 comment:
Linux, now verified on a real desktop (GNOME/Mutter X11):
Windows 11 25H2, now verified:
RDP shows the acrylic fallback tint, not live blur.
macOS, now verified on a visible window (M4, macOS 26.6.2, real
screencaptureover a test pattern):sudo powermetrics, plus a separateioregrun:Hyprland, now verified (omarchy, Hyprland 0.56, Wayland):
Perf scan: no findings. With the feature off nothing runs, and renderer work is unchanged with it on. See the Perf section.
Remaining unverified: a Windows console session, since the Windows numbers were taken over RDP.
Off path (the default): unchanged. No window option, no argument, the same layers.
Rebased onto feat(themes): background images for app themes #36589 (theme background images). feat(themes): background images for app themes #36589 landed the shared "translucent terminal over a backdrop" plumbing:
use-media-query.This PR now plugs the window material into that plumbing:
window-backdrop.tsis active for a window material or a theme image;:root:is([data-window-material], [data-theme-background]);Because the xterm patch is now identical to main's, this PR no longer changes
config/patchesorpnpm-lock.yaml.Shared hot files:
main.cssadditive rules;global-settings-types.ts(one line moved toGlobalSettingsBase).Negative user-facing tradeoffs
People who turned on Window Blur long ago will now actually get it, if their Background Opacity is < 1. On macOS that brings back some WindowServer blur cost while the window is visible. With opacity 1 they now get a plain opaque window. On Windows that removes the invisible acrylic they had.
macOS Translucent uses
transparent: true, which is the expensive choice on macOS. WindowServer alpha-composites the window against whatever is behind it. Measured on an M4 while visible:It costs nothing while hidden. The native shadow is kept. Blurred is the cheaper see-through option on macOS.
macOS Blurred costs almost nothing idle (+10 mW). Under sustained full-speed output it costs about +10 points of GPU residency and +16% WindowServer CPU vs Opaque. At Background Opacity 0.7 the vibrancy effect is subtle, because the
under-windowmaterial is fairly dense.On Windows 10 and 11 before 22H2 the setting is disabled ("Requires Windows 11 version 22H2 or later").
Linux without a compositor (bare X11) shows black where the desktop would show. The setting is opt-in and the hint says the compositor decides.
Unfocused split groups are not dimmed while a window material is active (dimming via group opacity would make chrome see-through).
macOS Blurred uses
visualEffectState: 'active', so the blur stays live while Orca is unfocused-but-visible (avoids a flat grey fill; costs WindowServer work in that state).Linux users who had the old Window Blur switch on are not migrated; they choose Translucent explicitly.
TUIs that set the background with OSC 11 paint an opaque background over the material (same as today's translucent-terminal behaviour), and that colour persists until the theme is reapplied.
Reduce Visual Effects makes menus and dialogs solid and removes the frosted look. When it changes the window material, that part applies after a restart.
The "Window Blur" switch is gone. It is replaced by the Window Material select in the same place, and its search keywords still match.
Only terminals become translucent. Sidebars, tab bar, editors and pages stay opaque; app-wide glass (feat(theme): app-wide window glass with vibrancy/acrylic #3972, feat(window): make macOS window blur a real glass effect, with opt-in Interface Glass #24359's Interface Glass) is not included.
Linux translucency costs compositor CPU under heavy output. On GNOME/Mutter X11, sustained full-screen output costs about +15% of a core in the compositor (22% → 37%), with no idle cost. This is inherent to ARGB windows and applies only when Translucent is chosen.
Over Remote Desktop, Windows acrylic shows as its solid fallback tint, not a live blur.
A window material and a theme image together: the image wins. The body keeps its opaque base, so the image's letterbox and veil never expose the desktop.
setVibrancy(null)) and restored when the image goes away.transparent: true, which can't change at runtime): it keeps paying alpha compositing under an image theme.Review
Two independent Opus reviews plus Codex (
gpt-6.1-sol, xhigh). Fixed:os.release(), rendererosRelease).windowBackgroundBluris mirrored on everywindowMaterialwrite at the store boundary. A Codex review of the rebase found that the Ghostty import skipped it before.window-material-runtime.ts(no twowindow-material.ts).Not changed, with reasoning: OSC 11 opaque backgrounds (same as today's translucent mode; preserving TUI colour mutations is intentional), renderer vs main reading reduced transparency through Chromium's media query vs
nativeTheme(same OS source), Plain SSH panel (sits in an opaque card, unaffected),visualEffectState: 'active'(documented tradeoff).Validation
pnpm tc(all projects) passes.oxlintandoxfmtpass on the changed files.pnpm sync:localization-catalog,pnpm sync:localization-runtime-catalogandpnpm verify:localization-catalogsall pass.pnpm testrun in a non-nested worktree:window-material.test.ts(new)createMainWindow*.test.ts(13 files)ghostty/mapper.test.tsterminal-appearance.test.ts,terminal-view-attributes-publisher.test.tspane-lifecycle.test.tsregenerate-xterm-patches.test.mjsuse-document-appearance.test.tsx,AppearancePane.test.tsxSidebarFeedbackDialog.test.tsxnode config/scripts/regenerate-xterm-patches.mjs --write, andpnpm install --frozen-lockfilere-applied them.ORCA_BACKGROUND_LAUNCH=1, CDP port 9317, throwaway profiles), on a folder workspace, both origin/main and this branch:filtergoes fromblur(2px)tonone);a visible-window macOS A/B for vibrancy versus opaque(done in phase 3 withpowermetrics);Hyprland(done in phase 3);top; PowerShellGPU Enginecounters).yes, A/B against origin/main.--ozone-platform=waylandand its own profile.grim.yes.5b71d48c9bf):pnpm tcpasses; oxlint and oxfmt are clean;verify:localization-catalogspasses.settings-update-window-material.test.ts, covers the legacy-switch mirror.blur, body transparent, xterm background alpha 0.data-reduce-effectsset.af51398cdaa):app:setWindowMaterialSuppressed. It only accepts a trusted UI renderer and a boolean, and only acts on a window created with macOS blur.applyThemeBackgroundtoggles. It re-sends after a reload, retries after a failed call, and feature-detects the method for mixed versions.pnpm tc, oxlint, oxfmt,src/main/window+src/main/ipc/app.test.ts(801 tests) and thesrc/renderer/src/libsuites.