damascene-wgpu 0.4.3

Damascene — wgpu backend (native + wasm)
Documentation

damascene-wgpu

Liquid-glass section — custom shader sampling the wgpu backdrop

wgpu backend for Damascene.

Most applications should not start here. Implement damascene_core::App and run it through damascene-winit-wgpu for a native window.

Use this crate directly when you are writing a custom host or embedding Damascene into an existing wgpu render loop:

  1. Create a Runner with the target texture format.
  2. Register any app shaders.
  3. Forward pointer, keyboard, text-input, modifier, and wheel events to the runner.
  4. Call prepare with a fresh El tree before drawing.
  5. Call render when Damascene owns pass boundaries, especially for backdrop-sampling shaders; call draw only inside a pass you own and only when backdrop sampling is not needed.

Coordinates passed to interaction methods are logical pixels. Render targets are physical pixels; pass the host scale factor to prepare.

Custom-host checklist

Contracts every out-of-tree host (raw Wayland layer-shell bars, notification daemons, multi-window composit-or-bust setups) has had to reverse-engineer. Work through these once and your host matches the in-tree winit host's behavior:

Surface format. Prefer an sRGB swapchain format (*UnormSrgb) so the hardware encodes on write; create the Runner with the same format you configure the surface with.

Alpha mode. Damascene's blending assumes the compositor reads premultiplied alpha when the surface is transparent. Negotiate CompositeAlphaMode::PreMultiplied when the surface capabilities offer it; if you fall back to Opaque, transparent themes will composite incorrectly — log it rather than failing silently.

Surface loss. get_current_texture() returning Lost / Outdated means reconfigure-and-retry: reconfigure the surface with the current size, mark the frame dirty, and try again next loop iteration. Expect a burst of these during interactive resize on Wayland; coalesce resizes so surface.configure() runs once per frame, not once per event.

Scale factor. Layout runs in logical pixels: pass a logical-size viewport to prepare along with the scale factor; configure the wgpu surface at physical size; convert pointer coordinates to logical before forwarding. On raw Wayland, also call wl_surface::set_buffer_scale when the output scale changes, then reconfigure.

Glyph warming. Runner::warm_default_glyphs() pre-rasterizes the ASCII set so the first text frame doesn't hitch — ~40 ms optimized, but ~19 s in an unoptimized debug build (measured; see the dev-profile section in the workspace README for the fix). Never call it inside a Wayland dispatch callback in a debug build: starving the socket that long gets you disconnected by the compositor. Multi-window hosts: warm a SharedText pool once per device instead (next item).

Shared text atlases. A Runner built with Runner::new/with_caps owns private glyph/MSDF atlases — N windows pay N× atlas VRAM, N× glyph rasterization, and N× warmup. Create one SharedText::new(&device) per device, warm_default_glyphs() it once, and build every window's runner with Runner::with_shared_text(.., &pool): fonts, the shaping cache, and the atlas GPU pages are then shared (the pool is format/sample-count independent, so mixed SDR/HDR windows share too). An existing runner's pool is reachable via runner.shared_text(). Composes with the pooled-Runner window-open path (WindowGfx::with_surface_and_renderer): build pooled runners from the shared pool and both costs collapse.

Runner pooling. A Runner is not bound to any surface or window — it depends only on the (target format, sample count) it was built with, and render takes the target texture every frame. A resident multi-window daemon can build and warm Runners off the open path (Runner::with_caps + warm_default_glyphs, ~360 ms measured) and re-point one at each new window (set_target_format / set_surface_size / set_working_color_space / set_output_luminance), making window-open latency negotiation-only. winit hosts get this packaged as damascene_winit_wgpu::host::WindowGfx::with_surface_and_renderer.

Per-frame calls. set_theme, set_hotkeys, set_selection, and the push_* request drains are cheap per-frame snapshots — call them every frame before prepare, in any order.

Hotkeys per window. The hotkey registry is per-Runner, and a multi-window host owns one Runner per window — feed each window's set_hotkeys only that window's list and route key events by window. A chord then fires in the OS-focused window only; for app-global accelerators, register the chord in every window's list and treat the per-window Hotkey event as one action.

Redraw scheduling. prepare() returns next_layout_redraw_in (animations settling, tooltip fades — needs a full rebuild + layout) and next_paint_redraw_in (time-driven shaders — Runner::repaint reuses cached ops). Schedule timers from both; None means no future frame is needed until input arrives.

Measure passes. To size a surface before it exists (layer-shell daemons sizing to content), run damascene_core::layout::layout against a separate UiState — reusing the live runner's state would leak hover/focus/animation state between the headless measure and the real frame.

Drop order. A wgpu surface created from a raw window handle borrows the native surface it was created over. Declare the wgpu surface field before the native surface/window in your struct (Rust drops fields in declaration order) or document the manual teardown order.

Input mapping. damascene_core::PointerButton::from_linux_button maps evdev BTN_* codes for raw Wayland hosts; damascene_core::Cursor::css_name() bridges cursors to any windowing layer (winit's CursorIcon parses the CSS names directly). winit hosts can reuse the pure mappers in damascene_winit_wgpu::host::input.