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