Skip to main content

unlit3d/winit/
presenter.rs

1//! The presentation path: the window surface, and how a frame reaches it.
2//!
3//! A scene presents one of two ways, chosen with [`PresentationPath`]. The plain
4//! one draws straight into the swap chain: one pass, no copy. The offscreen
5//! one draws the scene into a persistent target
6//! ([`offscreen::PersistentTarget`](crate::offscreen::PersistentTarget))
7//! and adds a blit pass after it that copies the target over the swap
8//! chain. The target survives present, so a screenshot reads the last
9//! frame back from it, and the swap chain's format or size changes never
10//! touch the scene's pipeline -- which is why an MCP-driven scene asks for
11//! it.
12//!
13//! [`build_presenter`] constructs the [`Presenter`] component once the GPU
14//! context exists, and [`setup_presenter`] spawns it. A single-world
15//! application drives the result with [`present_frames`],
16//! [`pump_presented`] and [`resize_presented`]; a host that keeps the
17//! render world in a sub-world -- multi-world runtimes -- spawns its own
18//! pump and calls [`Presenter::present`] itself.
19
20use std::cell::RefCell;
21use std::rc::Rc;
22use std::sync::Arc;
23use std::sync::Mutex;
24use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
25
26use unlit_ecs::World;
27use unlit_ecs::bundle;
28use unlit_ecs::sync;
29use unlit_ecs::{Command, Entity, EntityAccessError};
30
31use crate::attachments::FrameAttachments;
32use crate::blit::BlitSource;
33use crate::frame::{Frame, FrameBehaviourOrder, FrameSkip, OnFrame, dispatch_frame};
34use crate::offscreen::{PersistentTarget, TargetConfig};
35use crate::pass::{FramePass, FramePassOrder, Pass};
36use crate::renderer::Renderer;
37use crate::source::Source;
38use crate::winit::builtin::{Resumed, window};
39use crate::winit::event::{OnAboutToWait, OnWindowEvent};
40use crate::winit::surface::WindowSurface;
41use winit::event::WindowEvent;
42use winit::window::Window;
43
44/// Why the presentation path could not be built, or a frame could not be
45/// presented.
46#[derive(Debug, thiserror::Error)]
47#[non_exhaustive]
48pub enum PresenterError {
49    /// The host world carries no window to present to.
50    #[error("the scene setup found no window")]
51    NoWindow,
52    /// The window surface could not be built from the display handle.
53    #[error("failed to build the presentation surface: {0}")]
54    Surface(#[from] wgpu::CreateSurfaceError),
55    /// The render sub-world is not reachable from the host.
56    #[error("the scene setup found no render world: {0}")]
57    NoRenderWorld(EntityAccessError),
58    /// The render world carries no renderer at the named entity.
59    #[error("the scene setup found no renderer: {0}")]
60    NoRenderer(EntityAccessError),
61    /// The offscreen target could not be bound to the scene pass.
62    #[error("failed to bind the offscreen target: {0}")]
63    Target(EntityAccessError),
64    /// A resource the render world was expected to carry is gone at runtime.
65    #[error("the presentation path lost a render-world resource: {0}")]
66    MissingResource(#[from] EntityAccessError),
67}
68
69/// The order a scene's own pass records at, ahead of the copying pass.
70///
71/// A caller building the render world spawns its pass with this order, so
72/// the pass the offscreen path adds lands after it.
73pub const SCENE_PASS_ORDER: FramePassOrder = FramePassOrder(0);
74
75/// The order the offscreen path's copying pass records at: after the
76/// scene's own, so the target it samples holds this frame's picture.
77pub const BLIT_PASS_ORDER: FramePassOrder = FramePassOrder(1);
78
79/// The frame-behaviour order [`present_frames`] runs at: after the default
80/// band, so every behaviour that feeds the frame has run, and before
81/// behaviours that read its result state an order of their own.
82pub const PRESENT_ORDER: i32 = 10;
83
84/// How a scene reaches the window's swap chain.
85///
86/// This does not choose the final render destination: both paths present to
87/// the window. They differ only in whether the scene first draws into a
88/// persistent intermediate texture. Pure offscreen and empty-target rendering
89/// use [`PersistentTarget`] or an unattached pass directly, without a presenter.
90///
91/// [`PresentationPath::Surface`] draws straight into the swap chain.
92/// [`PresentationPath::Intermediate`] adds a persistent texture and a blit;
93/// that texture is readable after present, which makes it suitable for MCP
94/// screenshots, post-processing and scaled presentation.
95#[derive(Clone, Copy, Debug)]
96pub enum PresentationPath {
97    /// Draw the scene straight into the swap chain.
98    Surface {
99        /// The sample count the swap-chain frames resolve with.
100        samples: u32,
101        /// Whether swap-chain frames carry a depth-stencil attachment.
102        with_depth: bool,
103    },
104    /// Draw the scene into a persistent intermediate texture, then blit it
105    /// over the swap chain. The texture remains readable after presentation,
106    /// and its size can scale with the window or stay fixed.
107    Intermediate(TargetConfig),
108}
109
110impl Default for PresentationPath {
111    /// The plain window: a swap-chain frame at four samples with depth, the
112    /// presentation path's long-standing default.
113    fn default() -> Self {
114        Self::Surface {
115            samples: crate::offscreen::OFFSCREEN_SAMPLES,
116            with_depth: true,
117        }
118    }
119}
120
121/// Everything needed to render a frame and present it.
122///
123/// The presenter is a component of its own entity, built by
124/// [`build_presenter`]: everything it holds arrives after the GPU request
125/// resolves, and a component set fixed at spawn has no room for that.
126pub struct Presenter {
127    /// The render sub-world the frames are presented from; `None` when the
128    /// renderer, the passes and the presenter share the host world itself.
129    render: Option<Entity>,
130    /// The renderer inside the render world.
131    renderer: Entity,
132    /// The pass the scene's own sources draw into.
133    scene_pass: Entity,
134    /// The pass that copies the offscreen target over the surface; `None`
135    /// when the scene presents itself.
136    blit: Option<Blit>,
137    /// The swap chain wrapper.
138    surface: WindowSurface,
139    /// The presenting pass's attachments to the swap chain: the scene's own
140    /// sample count without the offscreen path, one sample and no depth
141    /// with it, since the blit covers the target with one triangle.
142    attachments: FrameAttachments,
143    /// The persistent target the scene draws into, when the scene's frames
144    /// land offscreen.
145    target: Option<PersistentTarget>,
146    /// The size used until the window reports its own.
147    pub fallback_size: (u32, u32),
148}
149
150/// The pass that copies the offscreen target into the swap chain.
151struct Blit {
152    /// The pass in the render world, ordered after every other pass.
153    pass: Entity,
154}
155
156impl Presenter {
157    /// The pass that draws into the swap chain.
158    ///
159    /// The offscreen target sits in front of it when the blit pass exists;
160    /// otherwise the scene's own pass draws straight into the surface.
161    fn presenting(&self) -> Entity {
162        self.blit.as_ref().map_or(self.scene_pass, |blit| blit.pass)
163    }
164
165    /// Draw the frame's result into the swap chain and present it.
166    ///
167    /// The presenting pass parks first and takes the frame's view only
168    /// after the swap chain image has been acquired. Between the two, and
169    /// after the image has been presented and destroyed, the pass states
170    /// no attachment -- so a frame rendered by anything other than this
171    /// loop, an MCP tool's screenshot of the scene, skips the pass rather
172    /// than recording into a dead swap-chain image.
173    ///
174    /// A surface released by [`Self::release`] is opened again here, so a host
175    /// that suspended only has to keep presenting.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error when the render world's renderer or its resources
180    /// are gone, or the window can no longer back a surface. A render
181    /// sub-world that is gone presents nothing and returns `Ok`.
182    pub fn present(&mut self, host: &World) -> Result<(), PresenterError> {
183        let mut nested = match self.render {
184            Some(render) => match host.root_scope().query_one::<&World>(render) {
185                Ok(nested) => Some(nested),
186                Err(_) => return Ok(()),
187            },
188            None => None,
189        };
190        let world = match nested.as_mut() {
191            Some(nested) => nested.get(),
192            None => host,
193        };
194        let presenting = self.presenting();
195        if self.surface.is_released() {
196            let mut renderer_row = world.root_scope().query_one::<&Renderer>(self.renderer)?;
197            self.surface
198                .build(world, renderer_row.get())
199                .map_err(PresenterError::Surface)?;
200        }
201        let Some(swap_frame) =
202            self.surface
203                .acquire_for(world, self.renderer, presenting, Some(&self.attachments))
204        else {
205            return Ok(());
206        };
207        let mut renderer_row = world
208            .root_scope()
209            .query_one::<&mut Renderer>(self.renderer)?;
210        renderer_row.get().render(world);
211        let context = renderer_row.get().context();
212        let queue = context.queue(&world.root_scope())?;
213        swap_frame.present_and_park(world, &queue, presenting);
214        Ok(())
215    }
216
217    /// Match the surface, the swap-chain attachments and the offscreen
218    /// target to a new size.
219    ///
220    /// The surface resizes first and reports the size it ended up with; the
221    /// swap chain's attachments follow it, and the offscreen target is
222    /// rebuilt for that size. The target's texture and view are replaced in
223    /// place -- their resource nodes keep their ids -- so the blit reading
224    /// the view rebuilds its bind group through its own dependency, without
225    /// the presenter touching the source.
226    ///
227    /// # Errors
228    ///
229    /// Returns an error when the render world or its resources are gone.
230    pub fn resize(&mut self, host: &World, size: (u32, u32)) -> Result<(), PresenterError> {
231        let mut nested = match self.render {
232            Some(render) => Some(
233                host.root_scope()
234                    .query_one::<&World>(render)
235                    .map_err(PresenterError::MissingResource)?,
236            ),
237            None => None,
238        };
239        let world = match nested.as_mut() {
240            Some(nested) => nested.get(),
241            None => host,
242        };
243        let size = {
244            let mut renderer = world
245                .root_scope()
246                .query_one::<&mut Renderer>(self.renderer)?;
247            self.surface
248                .resize(world, &mut *renderer.get(), size.0, size.1);
249            self.surface.size()
250        };
251        self.attachments.resize(world, size);
252        let Some(target) = self.target.as_mut() else {
253            return Ok(());
254        };
255        let context = {
256            let mut renderer = world.root_scope().query_one::<&Renderer>(self.renderer)?;
257            renderer.get().context()
258        };
259        let device = context.device(&world.root_scope())?;
260        target
261            .resize(world, context, &device, size)
262            .map_err(PresenterError::MissingResource)?;
263        Ok(())
264    }
265
266    /// Drop the swap chain the platform is about to invalidate, keeping
267    /// everything the scene drew from.
268    ///
269    /// The presenting pass parks first: it still names a view of the swap
270    /// chain, and that id is what keeps the view alive -- parking it drops
271    /// the view, so the view goes with the surface this release collects.
272    /// The scene keeps its state and presents again through a surface built
273    /// from the same window.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error when the render world or its renderer is gone.
278    pub fn release(&mut self, host: &World) -> Result<(), PresenterError> {
279        let mut nested = match self.render {
280            Some(render) => Some(
281                host.root_scope()
282                    .query_one::<&World>(render)
283                    .map_err(PresenterError::MissingResource)?,
284            ),
285            None => None,
286        };
287        let world = match nested.as_mut() {
288            Some(nested) => nested.get(),
289            None => host,
290        };
291        let presenting = self.presenting();
292        let _ = world
293            .root_scope()
294            .query_one::<&mut FramePass>(presenting)
295            .map(|mut pass| pass.get().park());
296        let mut renderer_row = world
297            .root_scope()
298            .query_one::<&mut Renderer>(self.renderer)?;
299        self.surface.release(world, &mut *renderer_row.get());
300        Ok(())
301    }
302}
303
304/// Build the presentation path for a live render world, as a
305/// [`Presenter`] component value.
306///
307/// `render` names the sub-world the renderer and the passes live in, or
308/// `None` when they share `world` itself; `renderer` and `scene_pass` name
309/// the renderer and the pass the scene's own sources draw into, all already
310/// filled in. The window comes from `world`; when it has not reported a
311/// size yet, `fallback_size` stands in.
312///
313/// `path` chooses direct presentation or an intermediate texture plus blit.
314///
315/// # Errors
316///
317/// Returns an error when `world` has no window, the surface cannot be
318/// built, or the render world, its renderer or its targets are gone.
319#[expect(
320    clippy::too_many_arguments,
321    reason = "one parameter per scene fixture the host already holds"
322)]
323pub fn build_presenter(
324    world: &mut World,
325    render: Option<Entity>,
326    renderer: Entity,
327    scene_pass: Entity,
328    surface: wgpu::Surface<'static>,
329    instance: &wgpu::Instance,
330    adapter: &wgpu::Adapter,
331    fallback_size: (u32, u32),
332    path: PresentationPath,
333) -> Result<Presenter, PresenterError> {
334    let window = window(world).ok_or(PresenterError::NoWindow)?;
335    let raw_size = window.inner_size();
336    let size = if raw_size.width == 0 || raw_size.height == 0 {
337        fallback_size
338    } else {
339        (raw_size.width, raw_size.height)
340    };
341    let (blit, surface, attachments, target) = match render {
342        Some(render) => {
343            let mut nested = world
344                .root_scope()
345                .query_one::<&mut World>(render)
346                .map_err(PresenterError::NoRenderWorld)?;
347            build_in(
348                nested.get(),
349                renderer,
350                scene_pass,
351                instance,
352                adapter,
353                window,
354                surface,
355                size,
356                path,
357            )?
358        }
359        None => build_in(
360            world, renderer, scene_pass, instance, adapter, window, surface, size, path,
361        )?,
362    };
363    Ok(Presenter {
364        render,
365        renderer,
366        scene_pass,
367        blit,
368        surface,
369        attachments,
370        target,
371        fallback_size,
372    })
373}
374
375/// Build the presentation path inside the render world itself.
376///
377/// Returns the blit pass when the scene draws offscreen, the opened window
378/// surface, the presenting pass's swap-chain attachments, and the bound
379/// offscreen target when there is one.
380#[expect(
381    clippy::too_many_arguments,
382    reason = "one parameter per scene fixture the host already holds"
383)]
384fn build_in(
385    world: &mut World,
386    renderer: Entity,
387    scene_pass: Entity,
388    instance: &wgpu::Instance,
389    adapter: &wgpu::Adapter,
390    window: Arc<Window>,
391    surface: wgpu::Surface<'static>,
392    size: (u32, u32),
393    path: PresentationPath,
394) -> Result<
395    (
396        Option<Blit>,
397        WindowSurface,
398        FrameAttachments,
399        Option<PersistentTarget>,
400    ),
401    PresenterError,
402> {
403    let context = world
404        .root_scope()
405        .query_one::<&Renderer>(renderer)
406        .map_err(PresenterError::NoRenderer)
407        .map(|mut row| row.get().context())?;
408    // The presenting pass's swap-chain attachments: without the offscreen
409    // path it is the scene's own pass, with the scene's sample count and
410    // depth; with it, the blit covers the target with one triangle -- one
411    // sample and no depth.
412    let (samples, with_depth) = match path {
413        PresentationPath::Surface {
414            samples,
415            with_depth,
416        } => (samples, with_depth),
417        PresentationPath::Intermediate(_) => (1, false),
418    };
419    let target = match path {
420        PresentationPath::Surface { .. } => None,
421        PresentationPath::Intermediate(config) => {
422            let device = context.device(&world.root_scope())?;
423            Some(
424                PersistentTarget::bind(
425                    world,
426                    context,
427                    scene_pass,
428                    &device,
429                    config.size.resolve(size),
430                    config,
431                )
432                .map_err(PresenterError::Target)?,
433            )
434        }
435    };
436    // The blit pass reads the target's view. A resize replaces that view in
437    // place, so the source is never patched: the bind group rebuilds
438    // through its own dependency on the view.
439    let blit = match target.as_ref() {
440        None => None,
441        Some(target) => {
442            let blit_pass = world.root_scope_mut().spawn(Pass::new(BLIT_PASS_ORDER));
443            world
444                .scope_mut(blit_pass)?
445                .spawn(bundle![Source::new(BlitSource::new(target.view.clone()))]);
446            Some(Blit { pass: blit_pass })
447        }
448    };
449    let (surface, attachments) = {
450        let mut renderer_row = world
451            .root_scope()
452            .query_one::<&mut Renderer>(renderer)
453            .map_err(PresenterError::NoRenderer)?;
454        let renderer = renderer_row.get();
455        {
456            let surface = WindowSurface::new(world, renderer, instance, adapter, window, surface);
457            let attachments = FrameAttachments::new(
458                world,
459                renderer.context(),
460                surface.color_format(),
461                surface.size(),
462                samples,
463                with_depth,
464            );
465            (surface, attachments)
466        }
467    };
468    Ok((blit, surface, attachments, target))
469}
470
471/// Build the presentation path and spawn its [`Presenter`], answering the
472/// entity it landed on.
473///
474/// See [`build_presenter`] for the arguments.
475///
476/// # Errors
477///
478/// Returns the same errors as [`build_presenter`].
479#[expect(
480    clippy::too_many_arguments,
481    reason = "one parameter per scene fixture the host already holds"
482)]
483pub fn setup_presenter(
484    world: &mut World,
485    render: Option<Entity>,
486    renderer: Entity,
487    scene_pass: Entity,
488    surface: wgpu::Surface<'static>,
489    instance: &wgpu::Instance,
490    adapter: &wgpu::Adapter,
491    fallback_size: (u32, u32),
492    path: PresentationPath,
493) -> Result<Entity, PresenterError> {
494    let presenter = build_presenter(
495        world,
496        render,
497        renderer,
498        scene_pass,
499        surface,
500        instance,
501        adapter,
502        fallback_size,
503        path,
504    )?;
505    let entity = world.root_scope_mut().spawn(bundle![presenter]);
506    Ok(entity)
507}
508
509/// One frame's dispatch, deferred to the command apply.
510///
511/// The frame behaviours need a mutable world, and they run between two
512/// event callbacks -- so the pump and the redraw request queue this command
513/// instead of drawing inline.
514#[derive(Clone)]
515struct DrawFrame {
516    /// The instant the previous frame dispatched, shared by both producers.
517    last: Arc<Mutex<Option<web_time::Instant>>>,
518    /// The index the next dispatched frame takes.
519    index: Arc<AtomicU32>,
520    /// Set while a draw is queued but not yet applied, so the pump and a
521    /// redraw request in the same turn coalesce into one frame.
522    pending: Arc<AtomicBool>,
523    /// Work the host does right before the dispatch, when it has any.
524    pre_dispatch: Option<SharedHostPump>,
525}
526
527/// Work the host does before a frame dispatches, for the world being dispatched.
528pub type HostPump = Box<dyn FnMut(&mut World)>;
529
530/// The same work, shared by the two producers that queue a frame.
531type SharedHostPump = Rc<RefCell<HostPump>>;
532
533impl Command for DrawFrame {
534    fn apply(self: Box<Self>, world: &mut World) -> Result<(), EntityAccessError> {
535        let now = web_time::Instant::now();
536        let delta_time = sync::lock(&self.last)
537            .replace(now)
538            .map_or(0.0, |last| (now - last).as_secs_f32().min(0.1));
539        let size = window(world)
540            .map(|window| {
541                let size = window.inner_size();
542                (size.width, size.height)
543            })
544            .or_else(|| {
545                world
546                    .root_scope()
547                    .query::<(Entity, &Presenter)>()
548                    .iter()
549                    .next()
550                    .map(|(_, presenter)| presenter.fallback_size)
551            })
552            .unwrap_or((1, 1));
553        let mut frame = Frame {
554            delta_time,
555            index: self.index.fetch_add(1, Ordering::Relaxed),
556            size,
557        };
558        if let Some(pre_dispatch) = &self.pre_dispatch {
559            (pre_dispatch.borrow_mut())(world);
560        }
561        dispatch_frame(world, &mut frame);
562        // A frame has been dispatched, so the next producer may queue one.
563        self.pending.store(false, Ordering::Release);
564        Ok(())
565    }
566}
567
568/// The frame behaviour that presents every dispatched frame: draw the
569/// frame's result into the swap chain.
570///
571/// A single-world application spawns this once; the frame loop that
572/// dispatches frames is [`pump_presented`]. The renderer walks every pass
573/// of the world here, so the scene's own pass -- and the offscreen path's
574/// copying pass, when there is one -- both record on this one call.
575///
576/// A frame the host skipped -- because its scene or its presentation path is
577/// not up yet -- is not presented either, so the behaviour that decides
578/// states an earlier order. Neither is a frame the winit host reports the
579/// app suspended for; a world with no winit host counts as resumed. When a
580/// world carries more than one presenter, the first one the query yields
581/// presents.
582pub fn present_frames() -> OnFrame {
583    OnFrame::with_order(
584        FrameBehaviourOrder(PRESENT_ORDER),
585        move |world, _entity, _frame| {
586            // A suspended winit host has no surface worth presenting into.
587            let resumed = world
588                .root_scope()
589                .query::<&Resumed>()
590                .iter()
591                .next()
592                .is_none_or(|state| state.0);
593            if !resumed || FrameSkip::is_skipped(world) {
594                return;
595            }
596            let mut rows = world.root_scope().query::<(Entity, &mut Presenter)>();
597            let Some((_, presenter)) = rows.iter().next() else {
598                return;
599            };
600            if let Err(error) = presenter.present(world) {
601                log::error!("failed to present a frame: {error}");
602            }
603        },
604    )
605}
606
607/// The behaviours that keep frames coming: one per turn of the event loop,
608/// plus one per compositor redraw request.
609///
610/// Redraw requests alone go quiet whenever the window stops being visible
611/// to the compositor, and a simulation left waiting on them freezes with
612/// it; the pump keeps the scene advancing on its own.
613pub fn pump_presented() -> (OnAboutToWait, OnWindowEvent) {
614    pump_presented_with(None)
615}
616
617/// `pump_presented`, with host work that runs before every frame dispatch.
618///
619/// A runtime that keeps its scene in a sub-world polls that world's own tasks
620/// and drains its command queue first; `pre_dispatch` receives the host world
621/// so it can do that on the same turn, before the behaviours that read what
622/// the work landed are dispatched.
623pub fn pump_presented_with(pre_dispatch: Option<HostPump>) -> (OnAboutToWait, OnWindowEvent) {
624    let pre_dispatch: Option<SharedHostPump> = pre_dispatch.map(|pump| Rc::new(RefCell::new(pump)));
625    let last: Arc<Mutex<Option<web_time::Instant>>> = Arc::default();
626    let index: Arc<AtomicU32> = Arc::default();
627    let pending: Arc<AtomicBool> = Arc::default();
628    let pump = {
629        let (last, index, pending) = (Arc::clone(&last), Arc::clone(&index), Arc::clone(&pending));
630        let pre_dispatch = pre_dispatch.clone();
631        OnAboutToWait::new(move |world, _entity, ()| {
632            queue_draw(world, &last, &index, &pending, &pre_dispatch);
633        })
634    };
635    let redraw = {
636        let (last, index, pending) = (Arc::clone(&last), Arc::clone(&index), Arc::clone(&pending));
637        let pre_dispatch = pre_dispatch.clone();
638        OnWindowEvent::new(move |world, _entity, payload| {
639            if matches!(payload.event, WindowEvent::RedrawRequested) {
640                queue_draw(world, &last, &index, &pending, &pre_dispatch);
641            }
642        })
643    };
644    (pump, redraw)
645}
646
647/// Queue one frame unless a queued one has not been applied yet.
648///
649/// The pump and a redraw request can both fire in the same turn of the
650/// event loop; without the flag that turn would dispatch two frames and
651/// advance the simulation twice.
652fn queue_draw(
653    world: &World,
654    last: &Arc<Mutex<Option<web_time::Instant>>>,
655    index: &Arc<AtomicU32>,
656    pending: &Arc<AtomicBool>,
657    pre_dispatch: &Option<SharedHostPump>,
658) {
659    if pending.swap(true, Ordering::AcqRel) {
660        return;
661    }
662    world.root_scope().queue().push(DrawFrame {
663        last: Arc::clone(last),
664        index: Arc::clone(index),
665        pending: Arc::clone(pending),
666        pre_dispatch: pre_dispatch.clone(),
667    });
668}
669
670/// The resize behaviour: match the presenter to the window's new size,
671/// skipping the degenerate zero size windows report on minimize.
672pub fn resize_presented() -> OnWindowEvent {
673    OnWindowEvent::new(move |world, _entity, payload| {
674        let WindowEvent::Resized(size) = &payload.event else {
675            return;
676        };
677        if size.width == 0 || size.height == 0 {
678            return;
679        }
680        let mut rows = world.root_scope().query::<(Entity, &mut Presenter)>();
681        let Some((_, presenter)) = rows.iter().next() else {
682            return;
683        };
684        if let Err(error) = presenter.resize(world, (size.width, size.height)) {
685            log::error!("failed to resize the presentation path: {error}");
686        }
687    })
688}