Skip to main content

agg_gui_wgpu/
lib.rs

1//! `WgpuGfxCtx` — a hardware-accelerated [`DrawCtx`](agg_gui::draw_ctx::DrawCtx)
2//! implementation via `wgpu`.
3//!
4//! # Platform coverage
5//!
6//! | Target | Backend |
7//! |---|---|
8//! | Windows | Vulkan, DX12 |
9//! | macOS / iOS | Metal |
10//! | Linux / Android | Vulkan |
11//! | WASM (`wasm32-unknown-unknown`) | WebGL2 (via `wgpu` `webgl` feature) |
12//!
13//! # Scope
14//!
15//! This crate is the **renderer and nothing else**: the
16//! [`DrawCtx`](agg_gui::draw_ctx::DrawCtx)
17//! implementation, its pipelines and shaders, offscreen framebuffers,
18//! screenshot capture, the [`custom_render`] hook for widgets that want their
19//! own GPU passes, and the [`Gpu`] surface bundle that platform shells build
20//! their swap-chain on.  It contains no widgets, no event loop, and no demo
21//! content, so any app can depend on it directly.
22//!
23//! Platform shells (winit event loop, browser canvas + rAF loop) live in
24//! `demo-wgpu`, which re-exports this crate for backwards compatibility.
25//!
26//! # Per-frame usage
27//!
28//! ```text
29//! ctx.reset(width, height);
30//! ctx.begin_frame(surface_view);   // clear + stash the target view
31//! app.paint(&mut ctx);             // 2-D commands accumulate
32//! ctx.end_frame();                 // flush everything to the GPU
33//! ctx.present(surface_texture);    // drop per-frame handles, then present
34//! ```
35//!
36//! Always present through [`WgpuGfxCtx::present`] rather than
37//! `SurfaceTexture::present()` directly: the ctx may hold a clone of the back
38//! buffer (for screenshots), and on DX12 a clone surviving present makes the
39//! next swap-chain resize fail.
40//!
41//! # Deferred draw command model
42//!
43//! Unlike the GL backend which submits draw calls immediately, `WgpuGfxCtx`
44//! accumulates `DrawCommand` enums during `fill()` / `stroke()` / etc., then
45//! flushes them all in [`WgpuGfxCtx::end_frame`] using a single
46//! `wgpu::CommandEncoder`.  This avoids the render-pass borrow lifetime
47//! conflict: a `RenderPass` exclusively borrows its encoder, preventing both
48//! from living in the same struct simultaneously.
49//!
50//! # Coordinate system
51//!
52//! All incoming coordinates are **Y-up pixel space**: origin at the bottom-left
53//! of the viewport, positive Y upward.  The vertex shader converts to NDC with
54//! `ndc = (pos / resolution) * 2 - 1`.  Scissor rects are stored in Y-up form
55//! and converted to wgpu's Y-down framebuffer convention inside `end_frame`.
56
57/// wgpu device + surface bundle and the surface-acquire recovery policy the
58/// platform shells share.
59#[cfg(not(target_arch = "wasm32"))]
60pub mod gpu;
61#[cfg(not(target_arch = "wasm32"))]
62pub use gpu::{
63    clamp_surface_size, surface_acquire_action, CopySrc, FrameAcquire, Gpu, GpuConfig,
64    GpuInitError, RetryWake, SurfaceAcquire, SurfaceError,
65};
66/// When to retry a failed `Surface::configure`, when to give up, and what to
67/// log — the pure policy behind [`Gpu`]'s surface recovery.
68#[cfg(not(target_arch = "wasm32"))]
69mod surface_retry;
70
71pub mod custom_render;
72pub use custom_render::{SharedCustomRenderer, WgpuCustomRender, WgpuCustomRenderCtx};
73
74pub mod ssaa;
75pub use ssaa::{ssaa_linear_scale, SsaaFramebuffer};
76
77/// Screenshot read-back methods on [`WgpuGfxCtx`] (GPU→CPU frame copy).
78mod screenshot_readback;
79
80/// GPU handle passed to widgets via `DrawCtx::gl_paint` on the wgpu backend.
81///
82/// All fields are owned (cloned `Arc<...>` for device/queue, `wgpu::TextureView`
83/// is internally ref-counted) so the struct is `'static` and works with the
84/// `&dyn std::any::Any` plumbing of [`agg_gui::GlPaint`].
85///
86/// Painters create their own `wgpu::CommandEncoder` and submit it via
87/// `queue.submit(...)`.  `WgpuGfxCtx` flushes any pending 2-D commands
88/// before invoking the painter, so submissions interleave in the natural
89/// paint order without an explicit barrier.
90///
91/// Constructed by the renderer and handed to painters — `#[non_exhaustive]`
92/// so more GPU state can be threaded through without a breaking release.
93#[derive(Clone)]
94#[non_exhaustive]
95pub struct WgpuPaintContext {
96    /// Device used to build pipelines, buffers, and textures.
97    pub device: Arc<wgpu::Device>,
98    /// Queue used to submit the painter's command encoder.
99    pub queue: Arc<wgpu::Queue>,
100    /// Render-target view — same surface or layer texture the 2-D pipeline
101    /// is rendering to this frame.  Painters open render passes against it
102    /// with `LoadOp::Load` to overlay on existing content.
103    pub target_view: wgpu::TextureView,
104    /// Format of `target_view` — needed for pipeline `ColorTargetState`.
105    pub surface_format: wgpu::TextureFormat,
106    /// Full target dimensions in physical pixels.
107    pub target_size: (u32, u32),
108}
109
110mod aa_step;
111mod buffer_arena;
112mod ctx_core;
113/// The deferred draw-command enum — see [`DrawCommand`].
114mod draw_command;
115pub(crate) use draw_command::DrawCommand;
116mod draw_ctx_impl;
117mod end_frame;
118mod end_frame_prepare;
119mod gradient;
120mod image_blit;
121mod layer_mask;
122mod layers;
123pub mod pipelines;
124mod primitives;
125mod screenshot_capture;
126/// Scaled / region capture readback — small-output companion to
127/// `screenshot_capture`'s full-surface readbacks.
128mod screenshot_scaled;
129pub use screenshot_scaled::RectInPixels;
130mod shaders;
131mod text_render;
132
133#[cfg(test)]
134mod clip_path_readback_tests;
135#[cfg(test)]
136mod frame_release_tests;
137#[cfg(test)]
138mod image_blit_readback_tests;
139#[cfg(test)]
140mod layer_text_readback_tests;
141#[cfg(test)]
142mod lcd_arc_cache_tests;
143#[cfg(test)]
144mod screenshot_scaled_tests;
145
146use std::collections::{HashMap, VecDeque};
147use std::sync::{Arc, Weak};
148
149use agg_gui::color::Color;
150use agg_gui::draw_ctx::{FillRule, LinearGradientPaint, RadialGradientPaint};
151use agg_gui::gl_renderer::GlyphCache;
152use agg_gui::text::Font;
153use agg_gui::TransAffine;
154use agg_gui::{LineCap, LineJoin};
155use agg_rust::path_storage::PathStorage;
156
157use pipelines::WgpuPipelines;
158
159// ---------------------------------------------------------------------------
160// Arc-keyed texture cache entry
161// ---------------------------------------------------------------------------
162
163/// One entry in the Arc-keyed wgpu texture cache.  The `Weak` serves as a
164/// liveness sentinel: when all strong refs to the source `Vec<u8>` are dropped
165/// (typically because the widget's L1 pixel cache evicted the entry),
166/// `weak.upgrade()` returns `None` and the entry is swept on the next access.
167pub(crate) struct ArcTextureEntry {
168    pub(crate) weak: Weak<Vec<u8>>,
169    pub(crate) texture: Arc<wgpu::Texture>,
170    pub(crate) view: wgpu::TextureView,
171    pub(crate) w: u32,
172    pub(crate) h: u32,
173}
174
175/// One entry in the **LCD** Arc-keyed wgpu texture cache.
176///
177/// Distinct from [`ArcTextureEntry`] because the LCD cache is keyed on the
178/// source `Vec`'s heap **buffer address** (`data.as_ref().as_ptr()`), not on
179/// `Arc::as_ptr`.  The TextArea dirty-strip path mutates a retained LCD plane
180/// *in place* via `Arc::make_mut`; when a `Weak` is outstanding that relocates
181/// the Arc control block (so `Arc::as_ptr` changes every keystroke) while only
182/// *moving* the `Vec` header — the byte buffer stays put.  Keying on the buffer
183/// address therefore keeps the entry stable across in-place edits so the texture
184/// allocation is reused instead of destroyed and rebuilt every keystroke.
185///
186/// `version` is the backbuffer's globally-unique `content_version` (from
187/// `next_content_version()` in agg-gui, one atomic counter across all caches).
188/// A freed buffer address can be recycled by an unrelated allocation, but the
189/// version it carries will always differ from any this entry has seen, so a
190/// stale entry can never falsely match — the mismatch forces a fresh write.
191/// Dimensions are checked as an additional guard.  Content-addressed *immutable*
192/// masks pass `version == 0` and are instead verified by Arc pointer identity
193/// via the stored `weak` (those buffers never relocate).
194pub(crate) struct LcdArcTextureEntry {
195    pub(crate) weak: Weak<Vec<u8>>,
196    pub(crate) texture: Arc<wgpu::Texture>,
197    pub(crate) view: wgpu::TextureView,
198    pub(crate) w: u32,
199    pub(crate) h: u32,
200    pub(crate) version: u64,
201}
202
203// ---------------------------------------------------------------------------
204// Saved draw state (for push_layer)
205// ---------------------------------------------------------------------------
206
207#[derive(Clone)]
208pub(crate) struct SavedWgpuDrawState {
209    pub(crate) viewport: (f32, f32),
210    pub(crate) fill_color: Color,
211    pub(crate) fill_linear_gradient: Option<LinearGradientPaint>,
212    pub(crate) fill_radial_gradient: Option<RadialGradientPaint>,
213    pub(crate) stroke_color: Color,
214    pub(crate) stroke_linear_gradient: Option<LinearGradientPaint>,
215    pub(crate) stroke_radial_gradient: Option<RadialGradientPaint>,
216    pub(crate) line_width: f64,
217    pub(crate) line_join: LineJoin,
218    pub(crate) line_cap: LineCap,
219    pub(crate) fill_rule: FillRule,
220    pub(crate) miter_limit: f64,
221    pub(crate) line_dash: Vec<f64>,
222    pub(crate) dash_offset: f64,
223    pub(crate) global_alpha: f64,
224    pub(crate) state_stack: Vec<(TransAffine, Option<[i32; 4]>)>,
225    pub(crate) font: Option<Arc<Font>>,
226    pub(crate) font_size: f64,
227    pub(crate) lcd_mode: bool,
228}
229
230// ---------------------------------------------------------------------------
231// Layer types
232// ---------------------------------------------------------------------------
233
234#[derive(Clone, Copy)]
235pub(crate) struct LayerRoundedClip {
236    pub(crate) x: f32,
237    pub(crate) y: f32,
238    pub(crate) w: f32,
239    pub(crate) h: f32,
240    pub(crate) r: f32,
241}
242
243/// One transient wgpu compositing layer.
244pub(crate) struct WgpuLayerEntry {
245    /// Render-attachment + sampler texture for this layer.
246    pub(crate) texture: Arc<wgpu::Texture>,
247    pub(crate) view: wgpu::TextureView,
248    pub(crate) width: u32,
249    pub(crate) height: u32,
250    pub(crate) origin_x: f64,
251    pub(crate) origin_y: f64,
252    pub(crate) alpha: f64,
253    pub(crate) saved: SavedWgpuDrawState,
254    /// Non-None when this layer will be stored in `retained_layers` on pop.
255    pub(crate) retained_key: Option<u64>,
256    pub(crate) rounded_clip: Option<LayerRoundedClip>,
257    /// Scissor active in the parent at push time — applied to the composite
258    /// blit on pop so the layer can't paint outside the parent's clip.
259    pub(crate) parent_clip: Option<[i32; 4]>,
260    /// Set via `set_layer_opaque_backdrop` once the caller has covered this
261    /// layer with opaque content; re-enables LCD subpixel text inside it.
262    ///
263    /// The default (`false`) is the safe one. A fresh layer texture is
264    /// cleared to alpha 0 and the 3-pass subpixel composite writes only
265    /// colour, so glyphs would keep alpha 0 and the pop composite would
266    /// blend them additively toward white — the wash-out this flag's
267    /// absence is designed to avoid. Once opaque content covers the
268    /// region, destination alpha is already 1 where glyphs land and
269    /// subpixel geometry is exactly as valid as against the backbuffer.
270    pub(crate) opaque_backdrop: bool,
271    /// `Some` when this is a **clip layer** created by `DrawCtx::clip_path`:
272    /// the tessellated clip path (parent-space positions + per-vertex
273    /// coverage) that composites the layer back through the path instead of
274    /// through a plain quad.  See `layer_mask.rs`.
275    pub(crate) clip_mask: Option<crate::layer_mask::ClipMaskMesh>,
276    /// For a clip layer, the current path as it was when `clip_path()` ran.
277    /// Canvas `clip()` leaves the path alone, so it is restored on pop (after
278    /// `restore_draw_state`, which resets `path`).
279    pub(crate) clip_saved_path: Option<PathStorage>,
280}
281
282/// A retained layer that persists across frames (keyed by `u64` handle).
283pub(crate) struct RetainedWgpuLayer {
284    pub(crate) texture: Arc<wgpu::Texture>,
285    pub(crate) view: wgpu::TextureView,
286    pub(crate) width: u32,
287    pub(crate) height: u32,
288    pub(crate) rounded_clip: Option<LayerRoundedClip>,
289}
290
291// ---------------------------------------------------------------------------
292// WgpuGfxCtx
293// ---------------------------------------------------------------------------
294
295/// An in-flight, non-blocking readback of the capture texture for the **web**
296/// screen-share sender.  A blocking `map_async` + `poll(Wait)` (as
297/// `DrawCtx::read_captured_screenshot` does for native Save/Copy) deadlocks the
298/// single-threaded browser: the map can only complete once control returns to the
299/// JS event loop, but the blocking wait never yields.  So the sender starts a
300/// readback one frame and harvests it a later frame via
301/// [`WgpuGfxCtx::poll_capture_readback`].
302pub(crate) struct PendingReadback {
303    staging: wgpu::Buffer,
304    w: u32,
305    h: u32,
306    padded_bpr: u32,
307    done: std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
308}
309
310/// A [`DrawCtx`](agg_gui::draw_ctx::DrawCtx) that renders via `wgpu`
311/// (Vulkan / DX12 / Metal / WebGL2).
312///
313/// Create with [`WgpuGfxCtx::new`], passing a `wgpu::Device` and `wgpu::Queue`
314/// that were obtained by the platform shell.  Each frame: call
315/// `render_app_frame` (which calls [`reset`][WgpuGfxCtx::reset] and
316/// `app.paint(ctx)`), then call [`end_frame`][WgpuGfxCtx::end_frame] with the
317/// current surface texture view to flush all deferred draw commands.
318pub struct WgpuGfxCtx {
319    // ── wgpu core ────────────────────────────────────────────────────────────
320    pub(crate) device: Arc<wgpu::Device>,
321    pub(crate) queue: Arc<wgpu::Queue>,
322    pub(crate) surface_format: wgpu::TextureFormat,
323    pub(crate) viewport: (f32, f32),
324
325    // ── render pipelines (created in Phase 2) ────────────────────────────────
326    pub(crate) pipelines: WgpuPipelines,
327
328    // ── deferred draw commands (flushed in end_frame — Phase 4) ──────────────
329    pub(crate) commands: Vec<DrawCommand>,
330
331    // ── texture caches (populated in Phase 6) ────────────────────────────────
332    /// Generic slice-keyed cache: key is a FNV-like hash of (ptr, len, w, h, head/tail bytes).
333    pub(crate) texture_cache: HashMap<u64, (Arc<wgpu::Texture>, wgpu::TextureView, u32, u32)>,
334    pub(crate) texture_cache_order: VecDeque<u64>,
335    /// Arc-pointer-keyed cache for `draw_image_rgba_arc` (Label backbuffers).
336    pub(crate) arc_texture_cache: HashMap<usize, ArcTextureEntry>,
337    /// Buffer-address-keyed cache for LCD coverage masks and backbuffer planes.
338    pub(crate) lcd_arc_texture_cache: HashMap<usize, LcdArcTextureEntry>,
339
340    // ── layer stack (wired in Phase 8) ────────────────────────────────────────
341    pub(crate) layer_stack: Vec<WgpuLayerEntry>,
342    pub(crate) retained_layers: HashMap<u64, RetainedWgpuLayer>,
343
344    // ── drawing state ────────────────────────────────────────────────────────
345    pub(crate) fill_color: Color,
346    pub(crate) fill_linear_gradient: Option<LinearGradientPaint>,
347    pub(crate) fill_radial_gradient: Option<RadialGradientPaint>,
348    pub(crate) stroke_color: Color,
349    pub(crate) stroke_linear_gradient: Option<LinearGradientPaint>,
350    pub(crate) stroke_radial_gradient: Option<RadialGradientPaint>,
351    pub(crate) line_width: f64,
352    pub(crate) line_join: LineJoin,
353    pub(crate) line_cap: LineCap,
354    pub(crate) fill_rule: FillRule,
355    pub(crate) miter_limit: f64,
356    pub(crate) line_dash: Vec<f64>,
357    pub(crate) dash_offset: f64,
358    pub(crate) global_alpha: f64,
359    /// Each entry is `(transform, scissor_yup)` — scissor stored in Y-up screen
360    /// coordinates; converted to Y-down at `end_frame` time.
361    pub(crate) state_stack: Vec<(TransAffine, Option<[i32; 4]>)>,
362    /// Path builder — stored in local Y-up coordinates.
363    pub(crate) path: PathStorage,
364    pub(crate) font: Option<Arc<Font>>,
365    pub(crate) font_size: f64,
366    pub(crate) lcd_mode: bool,
367
368    /// Tessellated-glyph cache shared with the GL backend — produces XY
369    /// triangles per `(font, glyph_id, size)` key.  Lives on the context so
370    /// glyph tessellations persist across frames.
371    pub(crate) glyph_cache: GlyphCache,
372
373    /// Surface texture view for the current frame — set by [`begin_frame`],
374    /// cleared by [`Self::end_frame`].  Required so widgets that issue raw GPU
375    /// draws via `DrawCtx::gl_paint` can target the same attachment as the
376    /// deferred 2-D pipeline without the platform shell having to plumb the
377    /// view through every call.
378    pub(crate) surface_view: Option<wgpu::TextureView>,
379    /// Cloned handle to the current frame's surface texture (the underlying
380    /// resource is internally ref-counted, so cloning the handle is cheap).
381    /// Used by [`Self::read_screenshot`] and `capture_screenshot` to copy
382    /// from the frame after `end_frame` has flushed the render — the
383    /// platform shell wires this up by calling `set_surface_texture` before
384    /// paint.  It must NOT outlive the frame: holding a back-buffer clone
385    /// past present breaks DX12 swap-chain resize, so it is dropped by
386    /// [`Self::present`] / [`Self::release_frame_texture`].
387    pub(crate) surface_texture: Option<wgpu::Texture>,
388    /// Set once `set_surface_texture` has found the previous frame's texture
389    /// still stashed (the shell never presented via [`Self::present`]) and
390    /// logged the warning — keeps that warning to one per ctx.
391    pub(crate) warned_unreleased_frame: bool,
392    /// Pixels captured during the active frame for the screenshot UI.  The
393    /// platform shell must call [`Self::read_screenshot`] BEFORE
394    /// [`Self::present`] (which releases the stashed texture),
395    /// stash the result here, then the screenshot orchestration picks it
396    /// up via [`Self::take_pending_screenshot`] in its read-back closure.
397    pub(crate) pending_screenshot: Option<(Vec<u8>, u32, u32)>,
398
399    /// GPU-resident copy of the most recent surface contents — populated by
400    /// [`DrawCtx::capture_screenshot`], sampled directly by
401    /// [`DrawCtx::draw_captured_screenshot`].  Lives on the GPU so the
402    /// screenshot preview pane can render it every frame with no CPU
403    /// readback (the previous `Vec<u8>` + re-upload + mipmap gen path was
404    /// blowing the frame budget under continuous capture).
405    ///
406    /// Pixels are pulled back to system memory only when the user clicks
407    /// Save or Copy — see [`DrawCtx::read_captured_screenshot`].
408    pub(crate) capture_texture: Option<(Arc<wgpu::Texture>, wgpu::TextureView, u32, u32)>,
409
410    /// In-flight async capture readback for the web screen-share sender.
411    /// See [`PendingReadback`].
412    pub(crate) pending_readback: Option<PendingReadback>,
413
414    /// In-flight async *scaled / region* capture readback — a separate slot so
415    /// periodic small thumbnails and a full-surface screenshot can be in
416    /// flight at the same time.  See `screenshot_scaled`.
417    pub(crate) pending_scaled_readback: Option<PendingReadback>,
418
419    /// Pipeline + sampler + destination texture for the scaled capture blit.
420    /// Built on first scaled capture; `None` until then.
421    pub(crate) scaled_blit: Option<screenshot_scaled::ScaledBlit>,
422
423    /// Per-frame chunked buffer pool — see [`buffer_arena`] module docs.
424    /// All `DrawCommand`s in a single flush share these three buffers
425    /// instead of allocating their own, which is the single biggest lever
426    /// against the per-command `create_buffer_init` cost.
427    pub(crate) frame_arenas: buffer_arena::FrameArenas,
428
429    /// 1024×1 RGBA8 alpha-step texture for the AA-texture pipeline.
430    /// Column 0 = `(255, 255, 255, 0)`, columns 1..1023 =
431    /// `(255, 255, 255, 255)`.  Sampled LINEAR — produces the
432    /// sub-texel-wide AA transition right on the polygon edge, exactly
433    /// like agg-sharp's `aATextureImages[255]`.  See
434    /// `agg_gui::gl_renderer::aa_texture_mesh` for the texcoord scheme
435    /// that drives it.
436    #[allow(dead_code)]
437    pub(crate) aa_step_texture: Arc<wgpu::Texture>,
438    #[allow(dead_code)]
439    pub(crate) aa_step_view: wgpu::TextureView,
440    pub(crate) aa_step_bg1: Arc<wgpu::BindGroup>,
441
442    /// Per-phase wall-clock timings from the most recent `end_frame`. Populated
443    /// inside `flush_to_surface` so platform shells (atomartist, marbles) can
444    /// surface a true breakdown of where wgpu-side time goes without needing
445    /// to fork the renderer for instrumentation. All numbers are wall-clock
446    /// microseconds; `command_count` is the number of `DrawCommand`s walked.
447    pub(crate) last_end_frame_stats: LastEndFrameStats,
448}
449
450/// Wall-clock breakdown of the most recent `WgpuGfxCtx::end_frame` call.
451///
452/// `prepare_us` is the per-command CPU walk that allocates wgpu buffers and
453/// bind groups (often the dominant cost for command-heavy scenes).
454/// `execute_us` is the render-pass walk that issues draw calls into the
455/// command encoder. `submit_us` is the `queue.submit` cost — usually tiny on
456/// native, occasionally large on WebGPU or when the driver is back-pressured.
457#[derive(Debug, Default, Clone, Copy)]
458pub struct LastEndFrameStats {
459    pub prepare_us: u32,
460    pub execute_us: u32,
461    pub submit_us: u32,
462    pub command_count: u32,
463}
464
465impl WgpuGfxCtx {
466    /// Create a new `WgpuGfxCtx`.
467    ///
468    /// `device` and `queue` must come from a `wgpu::Adapter` whose surface
469    /// is already configured with `surface_format`.  The caller retains
470    /// ownership of the surface; this struct only receives `Arc` refs to
471    /// the device and queue so both it and the platform shell can drive
472    /// buffer writes and texture uploads on the same queue.
473    pub fn new(
474        device: Arc<wgpu::Device>,
475        queue: Arc<wgpu::Queue>,
476        surface_format: wgpu::TextureFormat,
477        width: f32,
478        height: f32,
479    ) -> Self {
480        // 2-D pipelines stay sample_count=1: text and shapes already have
481        // analytic edge AA via the tess2 halo strip + per-vertex alpha, so
482        // hardware MSAA wouldn't add visible quality and would cost a full-
483        // surface MSAA buffer (and per-layer ones) every frame.  MSAA belongs
484        // scoped to the bar-grid renderer, which manages its own multi-sample
485        // attachments and resolves into the active 1-sample target view.
486        let pipelines = WgpuPipelines::new(&device, surface_format, 1);
487        let frame_arenas = buffer_arena::FrameArenas::new(&device);
488
489        // Build the 1024×1 RGBA8 alpha-step texture once and stash a
490        // ready-to-bind bind group on the context — every AA-texture
491        // draw can reuse this exact `bg1` without ever rebuilding it,
492        // since the texture itself is immutable.
493        let aa_step_texture = aa_step::build_aa_step_texture(&device, &queue);
494        let aa_step_view = aa_step_texture.create_view(&wgpu::TextureViewDescriptor::default());
495        let aa_step_bg1 = Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
496            label: Some("aa_step_bg1"),
497            layout: &pipelines.aa_texture_bgl1,
498            entries: &[
499                wgpu::BindGroupEntry {
500                    binding: 0,
501                    resource: wgpu::BindingResource::TextureView(&aa_step_view),
502                },
503                wgpu::BindGroupEntry {
504                    binding: 1,
505                    resource: wgpu::BindingResource::Sampler(&pipelines.linear_sampler),
506                },
507            ],
508        }));
509        let aa_step_texture = Arc::new(aa_step_texture);
510
511        Self {
512            device,
513            queue,
514            surface_format,
515            viewport: (width, height),
516            pipelines,
517            commands: Vec::new(),
518            texture_cache: HashMap::new(),
519            texture_cache_order: VecDeque::new(),
520            arc_texture_cache: HashMap::new(),
521            lcd_arc_texture_cache: HashMap::new(),
522            layer_stack: Vec::new(),
523            retained_layers: HashMap::new(),
524            fill_color: Color::rgba(0.0, 0.0, 0.0, 1.0),
525            fill_linear_gradient: None,
526            fill_radial_gradient: None,
527            stroke_color: Color::rgba(0.0, 0.0, 0.0, 1.0),
528            stroke_linear_gradient: None,
529            stroke_radial_gradient: None,
530            line_width: 1.0,
531            line_join: LineJoin::Miter,
532            line_cap: LineCap::Butt,
533            fill_rule: FillRule::NonZero,
534            miter_limit: 4.0,
535            line_dash: Vec::new(),
536            dash_offset: 0.0,
537            global_alpha: 1.0,
538            state_stack: vec![(TransAffine::new(), None)],
539            path: PathStorage::new(),
540            font: None,
541            font_size: 16.0,
542            lcd_mode: false,
543            glyph_cache: GlyphCache::new(),
544            surface_view: None,
545            surface_texture: None,
546            warned_unreleased_frame: false,
547            pending_screenshot: None,
548            capture_texture: None,
549            pending_readback: None,
550            pending_scaled_readback: None,
551            scaled_blit: None,
552            frame_arenas,
553            aa_step_texture,
554            aa_step_view,
555            aa_step_bg1,
556            last_end_frame_stats: LastEndFrameStats::default(),
557        }
558    }
559
560    /// Wall-clock breakdown of the most recent `end_frame` flush. Returns
561    /// zeroes until the first frame has been flushed. Designed for live perf
562    /// HUDs in platform shells — see atomartist's `record_frame_timings` for
563    /// an example consumer.
564    pub fn last_end_frame_stats(&self) -> LastEndFrameStats {
565        self.last_end_frame_stats
566    }
567
568    /// Reset drawing state for a new frame.  Preserves GPU resources.
569    pub fn reset(&mut self, width: f32, height: f32) {
570        self.viewport = (width, height);
571        self.fill_color = Color::rgba(0.0, 0.0, 0.0, 1.0);
572        self.fill_linear_gradient = None;
573        self.fill_radial_gradient = None;
574        self.stroke_color = Color::rgba(0.0, 0.0, 0.0, 1.0);
575        self.stroke_linear_gradient = None;
576        self.stroke_radial_gradient = None;
577        self.line_width = 1.0;
578        self.fill_rule = FillRule::NonZero;
579        self.miter_limit = 4.0;
580        self.line_dash.clear();
581        self.dash_offset = 0.0;
582        self.global_alpha = 1.0;
583        self.state_stack = vec![(TransAffine::new(), None)];
584        self.path = PathStorage::new();
585        self.font = None;
586        self.font_size = 16.0;
587        self.commands.clear();
588        self.layer_stack.clear();
589    }
590
591    /// Enable / disable LCD subpixel text for this context.  Called each frame
592    /// from `render_app_frame` with `font_settings::lcd_enabled()`.
593    pub fn set_lcd_mode(&mut self, on: bool) {
594        self.lcd_mode = on;
595    }
596
597    /// Start a frame: take ownership of this frame's render target and queue
598    /// the clear.
599    ///
600    /// `view` is the `wgpu::TextureView` for the surface texture (or any other
601    /// target) being painted this frame.  It is stashed on the context so that
602    /// mid-frame GPU work — custom render passes, layer compositing — targets
603    /// the same attachment as the deferred 2-D pipeline without the caller
604    /// plumbing it through every method; [`Self::end_frame`] consumes it.
605    ///
606    /// The clear itself happens inside `end_frame`, when the leading
607    /// `DrawCommand::Clear` is flushed into the first render pass; this
608    /// method only pushes the current theme's background colour onto the
609    /// command list so the frame starts from a clean framebuffer.
610    pub fn begin_frame(&mut self, view: wgpu::TextureView) {
611        self.surface_view = Some(view);
612        let bg = agg_gui::current_visuals().bg_color;
613        self.commands.push(DrawCommand::Clear(bg));
614        // Sync the per-ctx LCD flag from the global typography setting so
615        // any subsequent `fill_text` routes through the cached subpixel mask
616        // path when LCD is enabled (default at scale ≤ 1.25).  Without this
617        // shells that drive the loop via `begin_frame` instead of a helper
618        // that sets it silently fall through to the aliased tessellated-
619        // outline path even when the global says LCD is on.
620        self.set_lcd_mode(agg_gui::font_settings::lcd_enabled());
621    }
622
623    /// Flush all deferred draw commands into a single wgpu command submission.
624    ///
625    /// Must be called after the frame's painting and before
626    /// [`Self::present`].  The render target was stashed by
627    /// [`Self::begin_frame`] — the platform shell does not need to pass it
628    /// again here.
629    pub fn end_frame(&mut self) {
630        let Some(view) = self.surface_view.take() else {
631            return;
632        };
633        self.flush_to_surface(&view);
634    }
635
636    /// Borrow the shared 2-D pipeline collection.  Exposed so platform
637    /// shells (currently `demo-wasm`) can drive a [`SsaaFramebuffer::blit_to`]
638    /// when they need to composite an intermediate scene texture onto the
639    /// real swap-chain surface — see the comment on
640    /// [`SsaaFramebuffer::resolve_texture`] for the WebGL2 scene-buffer
641    /// pattern.  The returned `&WgpuPipelines` only exposes the fields
642    /// `pub(crate)` library code already uses; external callers can pass
643    /// it back into other library APIs but cannot reach in directly.
644    pub fn pipelines(&self) -> &pipelines::WgpuPipelines {
645        &self.pipelines
646    }
647
648    /// Borrow a clone-able handle to the wgpu device used for resource
649    /// allocation.  Exposed alongside [`Self::pipelines`] so platform
650    /// shells driving an external blit pass (currently `demo-wasm`'s
651    /// scene-buffer → surface composite) don't need to hold a duplicate
652    /// `Arc<wgpu::Device>` themselves.
653    pub fn device(&self) -> &Arc<wgpu::Device> {
654        &self.device
655    }
656
657    /// Borrow a clone-able handle to the wgpu queue.  Same rationale as
658    /// [`Self::device`] — the shell submits the scene-blit encoder
659    /// through this queue.
660    pub fn queue(&self) -> &Arc<wgpu::Queue> {
661        &self.queue
662    }
663
664    /// Format of the render target this context was configured for.  Custom
665    /// renderers need it to build pipelines whose `ColorTargetState` matches
666    /// the surface they will be composited onto.
667    pub fn surface_format(&self) -> wgpu::TextureFormat {
668        self.surface_format
669    }
670
671    /// Queue a custom wgpu render pass to run at the current point in the
672    /// frame's draw order. The user-supplied [`WgpuCustomRender`] is invoked
673    /// from `end_frame` after the active 2-D pass closes; subsequent 2-D
674    /// content reopens with `LoadOp::Load` so it composites on top.
675    ///
676    /// `screen_rect` is the widget's logical-pixel rect in agg-gui Y-up
677    /// coords. The implementor receives it via
678    /// [`WgpuCustomRenderCtx::screen_rect`] and is responsible for any
679    /// scissor / viewport conversions to wgpu's Y-down convention.
680    pub fn push_custom_render(
681        &mut self,
682        renderer: custom_render::SharedCustomRenderer,
683        screen_rect: agg_gui::Rect,
684    ) {
685        let parent_clip = self.current_clip();
686        self.commands.push(DrawCommand::Custom {
687            renderer,
688            screen_rect,
689            parent_clip,
690        });
691    }
692}