mandara

Mandara patches to gpui_wgpu

Vendored from zed-industries/zed at ec86047d67acd6fddd81392f44ed6ebcc6e7f531 (crates/gpui_wgpu, Apache-2.0, see LICENSE-APACHE) and wired in through the workspace’s [patch."https://github.com/zed-industries/zed"]. Every source change is marked Mandara patch in a comment. The goal is the per-window GPU memory of a desktop shell: a few large, mostly transparent layer surfaces that rarely draw paths.

Measured with cargo run -p mandara-core --example vram_probe -- W H 2 (invisible background-layer windows on a 1.5× output, process VRAM from DRM fdinfo; “second window” excludes the one-time device / pipeline cost):

Window (logical px) Upstream, 2nd window Patched, 2nd window
2560 × 1440 (3840 × 2160 device) 290 MiB 131 MiB
1280 × 720 129 MiB 37 MiB
420 × 520 40 MiB 12 MiB
100 × 30 4 MiB 4 MiB

The first window also carries the device, pipelines and allocator blocks: 43 → 39 MiB at 100 × 30. The live shell (bar/panel surface + wallpaper surface, both full-output, plus exclusion strips) went from 677 to 342 MiB.

vram_probe … paths=<side> keeps a small ring path drawn (like the bar’s progress rings) and animates a big one in a side² box (like an opened sidebar), then reports what is left 150 frames later: 167–183 MiB after vs. 170–234 MiB with wgpu’s MemoryUsage blocks (patch 5).

What remains at full size is the swapchain: Mesa’s Wayland WSI reports minImageCount = 4, so wgpu creates 4 images whatever the present mode or frame latency (patch 3 cannot lower it without changes in wgpu-hal).

1. Lazy, bounding-box-sized path textures (wgpu_renderer.rs, shaders.wgsl)

Upstream ensure_intermediate_textures allocated, on the first frame of every window, a full-frame path intermediate texture (4 B/px) plus a 4× MSAA target (16 B/px) — ~166 MiB for a 3840 × 2160 surface that may never draw a path.

2. Configurable MSAA sample count (memory_tuning.rs)

gpui_wgpu::set_path_sample_count(n) (or GPUI_PATH_SAMPLE_COUNT=1|2|4, which wins) picks the path MSAA count; the renderer uses the largest count ≤ n the device accepts. WebGPU guarantees 1 and 4; 2 and 8 also need TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, which GPUI does not request, so the candidates are filtered by it (taking the adapter’s word for 2 produced a validation error every frame on RADV). Default unchanged (4): at 1× the bar’s progress rings are visibly stair-stepped, and with patch 1 the MSAA target only covers the paths’ box (n × 4 bytes per pixel of it), so 4× costs ~1 MiB for the bar.

3. Present mode / frame latency knobs (memory_tuning.rs)

set_present_mode(Option<PresentMode>) / GPUI_PRESENT_MODE override the platform’s choice (Wayland: Mailbox), set_max_frame_latency(n) / GPUI_MAX_FRAME_LATENCY replace the hard-coded desired_maximum_frame_latency: 2. Defaults unchanged. On Mesa/RADV + Wayland every combination still yields 4 swapchain images (wgpu-hal sizes the swapchain from the mode-independent minImageCount), so Mandara keeps the defaults; the knobs exist for drivers where they do help and for experiments.

4. Smaller initial atlas textures and instance buffer

There are no depth/stencil buffers to trim.

5. Smaller suballocation blocks (wgpu_context.rs)

GPUI asked wgpu for MemoryHints::MemoryUsage: resources are suballocated from device-memory blocks growing to 64 MiB, and gpu-allocator keeps its last block even when empty. A shell that briefly needed a large path texture (the sidebar opening while the bar draws rings: a 3328 × 640 box) kept 64 MiB of VRAM forever. MemoryHints::Manual with 4–16 MiB blocks (MIN/MAX_DEVICE_MEMORY_BLOCK_BYTES) caps that at 16 MiB; larger resources get their own allocation, returned to the driver when dropped.

6. Script fallback chain and variable font weights (cosmic_text_system.rs)

Arabic set in Inter at semibold (Studio section headings, the selected nav row, card titles) rendered in Reem Kufi, a decorative Kufic display face: upstream passes no fallback chain for fonts without FontFallbacks, and cosmic-text’s own fallback only accepts a face whose weight matches exactly (or a variable face whose wght axis covers it). Noto Sans Arabic ships no 600, so the search fell through to any installed face with a 600 that covers Arabic. Which face that is depends on the machine.

7. Vulkan first, OpenGL only as a fallback (wgpu_context.rs, wgpu_renderer.rs)

Upstream creates one wgpu::Instance with VULKAN | GL and enumerates every adapter. Creating the GL half initializes EGL and the Mesa GL driver even when a Vulkan adapter is then selected: on the RX 9070 XT (RADV + radeonsi) that mapped libgallium (~9.5 MiB resident), had radeonsi touch ~30 MiB of libLLVM and started 7 driver threads (:gl, :sh, :cs, :gdrv, traceq, sh_opt, one more disk-cache worker) that idle for the life of the process. WgpuContext::backend_attempts() now tries a Vulkan-only instance first and creates a GL-only one only when no Vulkan adapter can configure the window’s surface (new_context_and_surface, also used by device-loss recovery). WGPU_BACKEND (gl, vulkan, vulkan,gl) replaces the list, so WGPU_BACKEND=gl forces the GL path.

Measured with the release shell in a headless MangoWM session (3840 × 2160 at 1.5, 3 min settled, same config and data, only WGPU_BACKEND differing; cargo xtask resource-check runs the same setup):

  Threads Anonymous GL libraries mapped VRAM
VULKAN \| GL instance (upstream) 38 25.6 MiB libgallium, libEGL_mesa, radeonsi’s libLLVM pages 176.6 MiB
Vulkan first (patch 7) 31 22.3 MiB none 162.8 MiB

Without the shell’s cold-page drop the GL driver’s file pages were resident too: libLLVM 13–31 MiB and libgallium 2.7–9.5 MiB, depending on the page cache. WGPU_BACKEND=gl still selects the radeonsi adapter and renders (the fallback path); unit test vulkan_is_tried_alone_before_gl pins the order.

8. Colour emoji as a primary font (cosmic_text_system.rs)

load_family skips any face without a Latin m as a primary font (the editor measures with it), and that included Noto Color Emoji, so asking for it by name failed and the text fell back to the UI font, whose fallback reached a flat symbol (DejaVu Sans, Noto Sans Symbols 2, Adwaita Mono) before the colour font. A face check_is_known_emoji_font recognises is now kept. Mandara requests the emoji family only for text that is nothing but emoji (mandara_ui::fonts::emoji_only, e.g. a plugin’s moon-phase glyphs), which then draws in colour; every other run is unchanged.

9. Rounded content masks (shaders*.wgsl, wgpu_renderer.rs)

The GPU half of vendor/gpui’s patch 1: gpui::ContentMask gained corner_radii after its bounds, so every primitive’s mask is 8 words instead of 4.

New test rounded_content_masks_clip_to_the_rounded_shape (headless renderer): a quad and a path drawn through a 64 px mask with 16 px corners fill its inside and straight edges fully, leave all four corners outside the arcs empty, antialias the arc pixel, and a zero-radius mask still clips hard.

10. Pipelines shared between windows (wgpu_renderer.rs, wgpu_context.rs)

Every WgpuRendererCore parsed and validated the WGSL shaders (naga keeps the module and its validation info alive with the pipelines) and built all its render pipelines and bind group layouts itself, so each window held its own copy: the bar / panel surface, every exclusion strip, the dock, each popover. They depend only on the device and on PipelineKey (target format, alpha mode, path sample count, dual-source blending, WebGL instance data), so WgpuContext::shared_pipelines now builds one SharedPipelines per key the first time a renderer asks and hands every later renderer clones of the same wgpu handles. A recovered device is a new WgpuContext with an empty cache. update_transparency still rebuilds against the shared layouts.

Measured with cargo xtask resource-check (headless 4K at 1.5, Vulkan / RADV, default config: bar surface and one exclusion strip), 15–90 s after start: heap 10.0–10.2 → 8.7–8.8 MiB, VRAM unchanged (166 MiB). The demo sandbox (OpenGL there, bar, dock and a strip) went from 36.4 to 32.4 MiB anonymous. Windows opened later (popovers, the launcher, floating cards) also skip the shader parse and pipeline build.

11. One face per default-chain family (cosmic_text_system.rs)

Patch 6 resolved each family of the default fallback chain with load_family, which loads every face of the family (each weight, width and slant: a cosmic_text::Font with its shaper data, cached by cosmic-text for the life of the process, and the face’s file mapped) only to keep one of them: the upright face nearest to regular, whose charmap decides coverage. Text in the chain’s script asks cosmic-text for the face it needs at its weight anyway (fallback_weight), so the rest were never used unless that text appeared. For an English interface the chain is the Arabic one: Cairo, Noto Sans Arabic (UI), Noto Naskh / Kufi Arabic, 53 files mapped and parsed at the first frame.

load_chain_face ranks faces upright first, then nearest to 400, keeping font-database order for ties, and loads only the selected face. Fallback-only faces can lack Latin glyphs; primary fonts still require the measurement glyph m, with the existing emoji and icon-font exceptions. The same guard also applies when a previously loaded fallback is requested as a primary font.

The original patch was measured in Mandara’s demo sandbox (Vulkan / RADV, 3840 × 2160 at 1.5, default config, release builds, three runs each; the shell’s lock::paint::TextPainter got the same treatment, see docs/releasing.md § Start-up memory): font files mapped 52 → 2, their resident pages 4 s after start 11.0 → 4.8 MiB, and the start-up peak (VmHWM) 112.0 (112.0–112.1) → 101.0 (100.9–101.8) MiB. Arabic, Persian, Urdu, Korean and Bengali text still renders in its faces at every weight (the mandara-ui fonts::tests regression tests pass unchanged).

12. Explicit native backend features (Cargo.toml)

The native renderer exposes vulkan and gles features. Mandara requests both with defaults disabled, retaining Vulkan-first selection and the GL fallback. Linux no longer enables Metal or the browser WebGPU backend; WebGPU otherwise enables Naga’s WGSL writer even in a native build. Browser targets retain WebGPU and WebGL. This removes unused shader translation from native compilation without changing its rendering path. The workspace renderer_features test checks the native and browser contracts.

13. Upload atlas pixels straight to wgpu (wgpu_atlas.rs)

The atlas copied every upload into a PendingUpload byte vector, then copied it again into wgpu’s staging memory at the next frame. It now calls Queue::write_texture at insertion: wgpu copies the slice before returning and schedules the transfer before the next submitted draw. Ordinary BGRA textures borrow the original slice; devices using RGBA allocate only the swizzled bytes. There is no second atlas-owned CPU copy of an image or glyph. Texture removal and device recovery still drop their atlas handles; wgpu keeps queued transfers alive until submission finishes.

Tests check borrowed BGRA allocation identity, RGBA channel conversion, removal before submission, and actual headless GPU pixels after changing the source bytes and reusing the released texture id, in both formats.

14. Accept script-only fallback faces (cosmic_text_system.rs)

Devanagari and Telugu faces often lack Latin m. Applying the primary-font measurement guard to the default fallback chain rejected these faces and left cosmic-text to search unrelated families. Fallback faces now join the chain without that guard. Requesting such a face as a primary font still fails, including when it was already cached as a fallback. Face selection uses one borrowed pass without a candidate vector or sorting; the selected face supplies its emoji classification directly from the database.

Regression tests use a small SIL Open Font License Devanagari fixture, loaded only in tests. They cover fallback acceptance, cached primary rejection and actual Hindi/Marathi shaping with a Latin primary and no extra loaded faces. The font and its license live under test_data/; neither enters the release binary. Release measurements are required before claiming a memory saving.

Tests

Packaging changes

Cargo.toml spells out the workspace-inherited fields and dependencies with the exact specs from Zed’s root manifest at that revision and declares its own empty [workspace] so it stays outside Mandara’s. Benches and the criterion dev-dependency are dropped; the other dev-dependencies (only resolved when the crate’s own tests run) are kept.

Upstreaming notes

Patches 1, 2, 4, 5 and 10 are self-contained and behaviour-preserving (identical output, less memory); patch 2 also fixes a latent validation error. Patch 3’s knobs are opt-in. Further steps not taken here: