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}