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}