Skip to content

Inject macos-background-from-layer only for compositing backgrounds - #9802

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
feat-conditional-layer-bg
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
feat-conditional-layer-bg

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Part of the ghostty rendering-parity work (hq docs/ghostty-web-parity.md, D10): make cmux desktop render pixel-identically to stock desktop Ghostty for opaque (default) configs.

Problem

cmux unconditionally injected macos-background-from-layer = true (inline config, prefix cmux-renderer-bg). That flag makes Ghostty skip its in-Metal background fill and zero the default-background alpha, so every default-background pixel is composited by CoreAnimation against a host CALayer. CA compositing drifts by +/-1/255 per channel (occasionally +/-2) versus Ghostty's stock deterministic in-Metal fill, so cmux could never bit-match stock Ghostty on default-background cells.

Mechanism

The flag was added in #2378 to eliminate a transparent-window flash during sidebar toggles and to unify separate translucent fills for terminal and chrome. Both motivations only exist when the background actually composites against other layers. The injection is now conditional:

  • inject macos-background-from-layer = true (and set usesHostLayerBackground) only when the finalized config has background-opacity < 1, background-blur enabled (radius or macOS glass), or a background-image;
  • plain opaque backgrounds omit the flag, so Ghostty uses its stock in-Metal fill and TerminalSurfaceBackgroundFillPlan resolves to the existing .ghosttyNativeRenderer owner (host layer goes clear);
  • the decision probes a finalized ghostty_config_clone because theme expansion at finalize time may set background keys, while the real config must receive the injection before finalize;
  • both the app config path and the surface reload path go through loadDefaultConfigFilesWithLegacyFallback, so live opacity/blur/image/theme changes re-evaluate the decision on cmux's existing config-reload path; the minimal fallback config (invalid user config) takes the same decision.

The sidebar-toggle flash cannot regress for opaque configs: during Metal layer resize lag the window backdrop behind it is the same opaque theme color.

Verification

  • Unit tests for the pure decision (HostLayerBackgroundDecisionTests in cmuxTests/GhosttyConfigTests.swift): opaque default stays on the stock in-Metal fill; opacity < 1, blur radius, both glass modes, and background image each flip to the host layer.
  • Pixel-diff verification vs a stock Ghostty.app build (same pinned ghostty tree, Menlo 12, DPR 2, 80x24, opaque): results table to follow in a comment.
  • Translucency check (background-opacity < 1 still composites through the host layer): to follow.

Note on test commits: the decision function is new, so a red-first test commit could not compile; tests and fix land together.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Note

Medium Risk
Changes terminal background rendering ownership and Ghostty config injection on every init/reload; behavior is well-tested for the decision matrix but opaque vs translucent edge cases affect visible pixels and window chrome compositing.

Overview
cmux no longer always forces macos-background-from-layer = true, so opaque default configs can match stock Ghostty’s in-Metal background fill instead of CoreAnimation compositing (which drifts ±1/255 per channel).

The host CALayer path and inline macos-background-from-layer injection now run only when a finalized config probe shows translucent background (background-opacity < 1), blur (radius or macOS glass), or a background-image. Theme expansion is evaluated via a cloned finalized config before the real config is finalized. The same logic applies on the main config load path and the minimal fallback init path; usesHostLayerBackground follows that decision.

GhosttyApp.shouldUseHostLayerBackground is the pure rule, with HostLayerBackgroundDecisionTests covering opaque default, translucency, blur modes, background image, and over-range opacity.

Reviewed by Cursor Bugbot for commit 0422832. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Conditionally inject macos-background-from-layer so only compositing backgrounds use the host CALayer; opaque backgrounds keep Ghostty’s in‑Metal fill for pixel-identical output to stock desktop Ghostty. This removes CoreAnimation drift on default-background cells while preserving translucency and blur behavior.

  • Bug Fixes
    • Inject macos-background-from-layer only when background-opacity < 1, background-blur is enabled, or a background-image is set; otherwise omit and set usesHostLayerBackground to false.
    • Probe a finalized config clone to honor theme-expanded background keys before injection.
    • Apply the same decision to the fallback config and re-evaluate on config reloads for live changes.
    • Added HostLayerBackgroundDecisionTests covering opaque, translucent, blur, glass, image, and overrange opacity cases.

Written for commit 0422832. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved terminal background rendering for translucent, blurred, glass-effect, and image-based backgrounds.
    • Preserved in-terminal rendering for opaque plain backgrounds.
    • Added safer fallback behavior when background configuration cannot be finalized.
  • Tests

    • Added coverage for background rendering decisions across opacity, blur, glass effects, and image backgrounds.

cmux unconditionally set macos-background-from-layer=true, routing
default-background pixels through CoreAnimation host-layer compositing,
which drifts +/-1/255 per channel vs Ghostty's stock in-Metal fill. The
flag exists to unify translucent fills (#2378), so inject it only when
the effective background actually composites against the host layer:
background-opacity < 1, background-blur enabled, or a background image.
The decision probes a finalized clone of the config (theme expansion at
finalize may set background keys) and re-evaluates on every config
reload, so live opacity/theme changes flip the mode. Plain opaque
backgrounds now render bit-identical to stock desktop Ghostty.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c6f8ce5d-dd3d-4ef9-b176-8d95abad1431

📥 Commits

Reviewing files that changed from the base of the PR and between 3faf795 and 0422832.

📒 Files selected for processing (2)
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/GhosttyConfigTests.swift

📝 Walkthrough

Walkthrough

Ghostty terminal background ownership now uses finalized configuration values. Host-layer rendering applies to translucent, blurred, and image-backed backgrounds. Opaque plain backgrounds retain Ghostty’s in-Metal fill. Tests cover the decision rules.

Changes

Terminal background ownership

Layer / File(s) Summary
Finalize and evaluate background settings
Sources/GhosttyTerminalView.swift, cmuxTests/GhosttyConfigTests.swift
The configuration probe finalizes a clone before reading opacity, blur, and image settings. Tests cover translucent, blurred, glass-blurred, image-backed, opaque, and clamped opacity cases.
Apply rendering decisions
Sources/GhosttyTerminalView.swift
Fallback and normal configuration loading inject macos-background-from-layer only when required. Rendering-mode state stores the computed decision.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#9103: Changes related terminal background ownership and host-layer compositing in GhosttyTerminalView.swift.

Suggested reviewers: austinywang, azooz2003-bit, ejc3


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error The diff adds a pure host-layer background policy to app-only GhosttyApp; it uses only Double, GhosttyBackgroundBlur, and Bool and is unit-tested without Ghostty config or lifecycle. Extract the policy and its tests from GhosttyApp into the existing CmuxFoundation target; expose a small public GhosttyBackgroundOwnershipPolicy API and keep clone/config injection in app glue.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the conditional injection of macos-background-from-layer for compositing backgrounds.
Description check ✅ Passed The description clearly explains the change, rationale, implementation, and verification, but omits the template's demo video, checklist, and review-trigger sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed GhosttyApp remains unannotated, and the new static decision helper adds no actor or Sendable state. The diff adds no protocols, async code, or background access to UI-bound stores.
Cmux Swift Blocking Runtime ✅ Passed The production diff adds configuration cloning/finalization and conditional loading only; it adds no semaphore, wait, sleep, delayed dispatch, sync, polling, or lock usage. Existing timing/lock sit...
Cmux Browser Automation Off-Main ✅ Passed HEAD changes only Ghostty background configuration and tests; no browser socket, WebKit wait, worker-router, or main processV2 routing lines were added.
Cmux Expensive Synchronous Load ✅ Passed The production diff adds only Ghostty config clone/finalize and background decision logic; it adds no agent-history loads, transcript parsing, directory scans, or prohibited calls.
Cmux Cache Substitution Correctness ✅ Passed The diff changes Ghostty background rendering and config reload only. It adds a temporary finalized clone probe and no cached-value substitution in persistence, history, undo, or snapshot paths.
Cmux No Hacky Sleeps ✅ Passed The diff changes only Swift production code and Swift tests; no TypeScript, JavaScript, shell, or build/runtime script delays are introduced.
Cmux Algorithmic Complexity ✅ Passed The production change performs one config clone/finalize and three constant-time key reads per load; no scalable collection scans, nested loops, or batch rescans were added. Tests use fixed inputs.
Cmux Swift Concurrency ✅ Passed The diff adds synchronous configuration probing and XCTest coverage only; it adds no Dispatch queues, Combine state, completion-handler API, or fire-and-forget Task pattern.
Cmux Swift @Concurrent ✅ Passed The diff adds only synchronous helpers and call sites; it introduces no nonisolated async work or @concurrent annotations, and no async UI-bound helper changes require an actor hop.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes only Sources/GhosttyTerminalView.swift and cmuxTests/GhosttyConfigTests.swift; it changes no Package.swift, Package.resolved, Xcode project, .gitignore, workflow, or dependency metad...
Cmux Swift Logging ✅ Passed The diff adds no print, debugPrint, dump, NSLog, file logging, Logger declaration, or sensitive diagnostic. Existing logLabel arguments are only conditionally retained.
Cmux User-Facing Error Privacy ✅ Passed The production diff only changes internal Ghostty background configuration and developer comments; it adds no user-facing errors, alerts, command output, recovery copy, or exposed implementation de...
Cmux Full Internationalization ✅ Passed The diff changes only Ghostty background logic, config-key literals, internal comments, and unit tests; it adds no user-facing text or localization/catalog/web message entries.
Cmux Swiftui State Layout ✅ Passed The diff changes GhosttyApp configuration logic and adds pure unit tests; it adds no SwiftUI state, GeometryReader, lazy-row store reference, or render-time state mutation.
Cmux Architecture Rethink ✅ Passed The patch adds a pure decision helper and keeps GhosttyApp as the state owner; it adds no timing, blocking, observer, lock, or polling repair path.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The Swift diff only changes Ghostty background configuration and pure tests; it adds or changes no standalone NSWindow, NSPanel, controller, SwiftUI Window, identifier, or close-shortcut routing.
Cmux Source Artifacts ✅ Passed The only changed paths are intentional Swift source and unit-test files; no logs, caches, build output, scratch directories, or other source-control artifacts enter the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The production diff adds no test/debug accessor, guard, or widened wrapper. shouldUseHostLayerBackground has a real production caller; tests call the same product decision function.
Cmux No Ambient Global State ✅ Passed The production diff adds a private instance helper and one pure static method on existing GhosttyApp; it adds no top-level mutable state, namespace type, or singleton.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-conditional-layer-bg

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0422832. Configure here.

let usesLayerBackground = hostLayerBackgroundDecision(probing: config)
let renderingModeChanged = setUsesHostLayerBackground(
true,
usesLayerBackground,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Surface reload desyncs host layer

High Severity

loadDefaultConfigFilesWithLegacyFallback now writes the conditional app-wide usesHostLayerBackground flag even when a surface-only full reload calls it. That path installs a temporary config on one surface and intentionally leaves GhosttyApp.config unchanged, so chrome can drop the host backdrop while other surfaces still zero Metal default-background alpha via macos-background-from-layer, leaving those terminals transparent.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0422832. Configure here.

@lawrencecchen

lawrencecchen commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor Author

cmux inline Ghostty config injection audit (D10)

Every loadInlineGhosttyConfig / config-mutation site in Sources/GhosttyTerminalView.swift (main path loadDefaultConfigFilesWithLegacyFallback + init fallback path), plus font-resolution deltas.

Injection (prefix) Purpose Rendering-visible? Parity-safe?
macos-background-from-layer = true (cmux-renderer-bg) Host CALayer owns window backdrop; added in #2378 to kill transparent-window flash on sidebar toggle and unify translucent fills Yes: default-bg cells leave Metal transparent and are CA-composited, +/-1/255 (sometimes 2) drift on every default-bg pixel Was unsafe; fixed this PR: injected only when background-opacity < 1, background-blur enabled, or background-image set. Opaque default now uses stock in-Metal fill
cmux default appearance theme (cmux-default-appearance / theme file) cmux-branded default colors when the user has no appearance directives Yes (colors) when user config has no theme/color keys Safe by design: any explicit user theme or color directive suppresses it; with the pinned parity config (explicit background/foreground/palette) it never applies. Deliberate product default, not a drift
Conditional theme override (cmux-conditional-theme) Resolves ghostty theme = light:X,dark:Y pairs against cmux's app appearance Yes (colors) only for split light/dark themes Safe: reproduces ghostty's own conditional-theme semantics under cmux's appearance control; single-theme and explicit-color configs unaffected
macos-titlebar-proxy-icon = hidden (cmux-titlebar-proxy-icon) Hide AppKit proxy icon in cmux's custom titlebar Window chrome only, zero terminal-cell pixels Safe
shell-integration = none (cmux-shell-integration-override) cmux does shell integration itself via .zshenv bootstrap (#2594); user's preference saved first No raster effect for identical byte streams (affects OSC 133 marks emitted by shells, not how bytes render) Safe
copy-on-select (cmux-managed-terminal-settings) Mirror cmux Settings toggle No (input/selection behavior) Safe
font-size scale (cmux-global-font-magnification) cmux global font magnification setting Yes when user sets non-default magnification (same class as changing font-size) Safe: no-op at default; explicit user-visible setting
keybind unbinds (cmux-owned-keybind-overrides) cmux owns split/close/workspace shortcuts via KeyboardShortcutSettings No Safe
window-vsync = false (cmux-no-active-display-vsync-fallback) Keep renderer ticking with zero active displays (headless CI) Timing only, never pixel values; only fires with no displays Safe
font-codepoint-map CJK ranges (cmux-cjk-font-fallback) Deterministic CJK fallback (PR #1017): stock CTFontCollection scoring can pick decorative monospace-flagged fonts (AB_appare, LingWai) depending on installed fonts Yes for CJK codepoints when it applies Intentional, conditional delta: only injected when the user's config has no codepoint-map/fallback chain covering those ranges; suppressible by user config. Deliberate product feature; keep-vs-drop is a pending user decision and is not changed in this PR. Excluded fixtures (widechars/CJK) are outside the D10 opaque gate
Legacy + app-support config file loads (config.ghostty, legacy config) Config discovery breadth (cmux app-support dir, ghostty legacy file) Only reflects the user's own directives Safe: loads user content, injects nothing

Font resolution: cmux registers no fonts. No CTFontManagerRegisterFonts* anywhere in Sources/, Packages/, or CLI/, and no ATSApplicationFontsPath in Resources/Info.plist. The renderer resolves fonts through the same GhosttyKit CoreText discovery as stock Ghostty.app in an app-process context. The previously observed AssetsV2 on-demand-font effect (BIZ UDGothic for U+2025) is a CLI-process vs app-process CoreText matching difference — both the cmux app and stock Ghostty.app sit on the same app-process side, so app-renderer font resolution cannot differ from stock because of cmux font registration. The only cmux-specific resolution delta is the conditional CJK font-codepoint-map row above.

cmux == stock Ghostty pixel verification (D10)

Setup: cmux d10bg tagged build = this branch + #9708 merged locally (its debug.surface.screenshot RPC is the occlusion-proof capture path; ghostty submodule at its 7a07007 SHA). Stock leg = Ghostty.app built unmodified from that same ghostty tree (macos/Ghostty.xcodeproj, scheme Ghostty, Debug), launched with an isolated $HOME so no user config is read. Both sides pinned: Menlo 12, DPR 2, 80x24, window-padding 0, opaque background #1d1f21, harness contract theme, cursor block/no-blink, minimum-contrast 1, bold-is-bright false. cmux captured via the RPC with window_occlusion_visible=false and app_active=false on every capture (never surfaced); stock captured via screencapture -l <windowid>, window never activated, one batched visible interval, app killed after. Diff = raw Display P3 8-bit values, zero tolerance, on the 1120x672 device-pixel content region; excluded_alpha=584 is the stock window capture's rounded-corner window-server mask (alpha<255 pixels, outside the renderer's output).

fixture cmux d10bg vs stock Ghostty (differing px / 752,640)
calibration 0
plain-text 0
colors-16-256 0
truecolor 0
atlas-probe 0
calibration-abg 0
plain-text-abg 0
truecolor-abg 0

The base (non-abg) rows are the ones this PR changes: they route default-background pixels through the renderer background path, which previously drifted +/-1/255 under CoreAnimation compositing and now takes Ghostty's stock in-Metal fill.

Translucency preserved, re-evaluated live

Same running d10bg app, straight-alpha RPC screenshots of the whole surface (1,957,760 px):

  • background-opacity = 0.9 -> 1,956,217 px with alpha < 255 (min 0): default-bg cells leave Metal transparent, host CALayer composites, i.e. macos-background-from-layer engaged.
  • reload-config back to opacity 1 -> 0 px with alpha < 255 (all 255): stock in-Metal fill.
  • reload-config to 0.9 again -> 1,956,217 px with alpha < 255: the decision flips both directions through the existing config-reload path, no restart.

e2e

Hosted test-e2e.yml dispatches for SettingsTerminalBehaviorUITests: three runs on the warp-15 pool were cancelled mid-run by runner preemption and one blacksmith-26 run failed on runner GUI/recording infra (0 tests executed). A blacksmith-15 no-video run is in flight: https://github.com/manaflow-ai/cmux/actions/runs/31169098020

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants