Skip to main content

loam_app/session/
debug_layer.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use egui_wgpu::{Renderer, RendererOptions, ScreenDescriptor};
7use loam_egui::egui;
8use loam_render::device::GpuContext;
9use loam_render::pass::{FrameFormat, FramePass, FrameTarget, PassStage};
10use wgpu::{CommandBuffer, CommandEncoder, Device, Queue, TextureFormat, TextureView};
11#[cfg(not(target_arch = "wasm32"))]
12use winit::event::WindowEvent;
13#[cfg(not(target_arch = "wasm32"))]
14use winit::window::Window;
15
16#[cfg(any(target_arch = "wasm32", test))]
17use super::input::TouchCapture;
18
19pub const DEBUG_LAYER: &str = "debug-layer";
20
21enum Feed {
22    #[cfg(not(target_arch = "wasm32"))]
23    Winit(Box<WinitInput>),
24    Raw(RawFeed),
25}
26
27#[cfg(not(target_arch = "wasm32"))]
28struct WinitInput {
29    state: egui_winit::State,
30}
31
32#[cfg(not(target_arch = "wasm32"))]
33impl WinitInput {
34    fn new(ctx: &egui::Context, window: &Window) -> Self {
35        Self {
36            state: egui_winit::State::new(
37                ctx.clone(),
38                egui::ViewportId::ROOT,
39                window,
40                Some(window.scale_factor() as f32),
41                window.theme(),
42                None,
43            ),
44        }
45    }
46
47    fn on_event(&mut self, window: &Window, event: &WindowEvent) -> egui_winit::EventResponse {
48        self.state.on_window_event(window, event)
49    }
50
51    fn take(&mut self, window: &Window) -> egui::RawInput {
52        self.state.take_egui_input(window)
53    }
54
55    fn handle_output(&mut self, window: &Window, output: egui::PlatformOutput) {
56        self.state.handle_platform_output(window, output);
57    }
58}
59
60struct RawFeed {
61    events: Vec<egui::Event>,
62    pending: Vec<egui::Event>,
63    modifiers: egui::Modifiers,
64    canceled: bool,
65    quiet: bool,
66    #[cfg(any(target_arch = "wasm32", test))]
67    reset: bool,
68}
69
70impl RawFeed {
71    fn push(&mut self, event: egui::Event) {
72        let delayed = matches!(
73            event,
74            egui::Event::PointerMoved(_)
75                | egui::Event::PointerButton { .. }
76                | egui::Event::PointerGone
77                | egui::Event::Touch { .. }
78        );
79        if self.canceled && delayed {
80            self.pending.push(event);
81            return;
82        }
83        let cancellation = matches!(event, egui::Event::WindowFocused(false));
84        self.events.push(event);
85        self.canceled |= cancellation;
86    }
87
88    #[cfg(any(target_arch = "wasm32", test))]
89    fn cancel_touch(&mut self, id: u64, pos: egui::Pos2, pointer: bool) {
90        self.push(egui::Event::Touch {
91            device_id: egui::TouchDeviceId(0),
92            id: egui::TouchId::from(id),
93            phase: egui::TouchPhase::Cancel,
94            pos,
95            force: None,
96        });
97        self.canceled |= pointer;
98    }
99
100    #[cfg(any(target_arch = "wasm32", test))]
101    fn cancel_pointer(&mut self) {
102        self.canceled = true;
103    }
104
105    fn take(&mut self) -> Vec<egui::Event> {
106        let current = std::mem::take(&mut self.events);
107        if self.quiet {
108            self.quiet = false;
109            self.canceled = false;
110            self.events.append(&mut self.pending);
111        } else if self.canceled {
112            self.quiet = true;
113            #[cfg(any(target_arch = "wasm32", test))]
114            {
115                self.reset = true;
116            }
117        }
118        current
119    }
120}
121
122#[cfg(any(target_arch = "wasm32", test))]
123fn reset_pointer(context: &egui::Context) {
124    context.input_mut(|input| input.pointer = egui::PointerState::default());
125    context.stop_dragging();
126    egui::DragAndDrop::clear_payload(context);
127}
128
129struct ManagedTexture {
130    image: egui::ImageData,
131    options: egui::TextureOptions,
132}
133
134struct Layer {
135    ctx: egui::Context,
136    renderer: Renderer,
137    device: Device,
138    queue: Queue,
139    format: TextureFormat,
140    feed: Feed,
141    primitives: Vec<egui::ClippedPrimitive>,
142    textures: egui::TexturesDelta,
143    screen: ScreenDescriptor,
144    native_pixels_per_point: f32,
145    callbacks: Vec<CommandBuffer>,
146    managed_textures: HashMap<egui::TextureId, ManagedTexture>,
147}
148
149fn renderer(device: &Device, format: TextureFormat) -> Renderer {
150    Renderer::new(
151        device,
152        format,
153        RendererOptions {
154            msaa_samples: 1,
155            ..Default::default()
156        },
157    )
158}
159
160impl Layer {
161    fn new(
162        gpu: &GpuContext,
163        format: TextureFormat,
164        ctx: egui::Context,
165        feed: Feed,
166        size: (u32, u32),
167        scale: f32,
168    ) -> Self {
169        Self {
170            ctx,
171            renderer: renderer(&gpu.device, format),
172            device: gpu.device.clone(),
173            queue: gpu.queue.clone(),
174            format,
175            feed,
176            primitives: Vec::new(),
177            textures: egui::TexturesDelta::default(),
178            screen: ScreenDescriptor {
179                size_in_pixels: [size.0.max(1), size.1.max(1)],
180                pixels_per_point: scale,
181            },
182            native_pixels_per_point: scale,
183            callbacks: Vec::new(),
184            managed_textures: HashMap::new(),
185        }
186    }
187
188    fn raw(&mut self) -> Option<&mut RawFeed> {
189        match &mut self.feed {
190            Feed::Raw(feed) => Some(feed),
191            #[cfg(not(target_arch = "wasm32"))]
192            Feed::Winit(_) => None,
193        }
194    }
195
196    fn raw_input(
197        &mut self,
198        #[cfg(not(target_arch = "wasm32"))] window: Option<&Window>,
199    ) -> egui::RawInput {
200        let points = self.native_pixels_per_point * self.ctx.zoom_factor();
201        let size = self.screen.size_in_pixels;
202        match &mut self.feed {
203            #[cfg(not(target_arch = "wasm32"))]
204            Feed::Winit(input) => match window {
205                Some(window) => input.take(window),
206                None => egui::RawInput::default(),
207            },
208            Feed::Raw(feed) => {
209                let mut raw = egui::RawInput {
210                    screen_rect: Some(egui::Rect::from_min_size(
211                        egui::Pos2::ZERO,
212                        egui::vec2(size[0] as f32 / points, size[1] as f32 / points),
213                    )),
214                    events: feed.take(),
215                    modifiers: feed.modifiers,
216                    viewport_id: egui::ViewportId::ROOT,
217                    ..egui::RawInput::default()
218                };
219                raw.viewports
220                    .entry(egui::ViewportId::ROOT)
221                    .or_default()
222                    .native_pixels_per_point = Some(self.native_pixels_per_point);
223                raw
224            }
225        }
226    }
227
228    fn finish(&mut self, #[cfg(not(target_arch = "wasm32"))] window: Option<&Window>) {
229        let output = self.ctx.end_pass();
230        #[cfg(any(target_arch = "wasm32", test))]
231        if self
232            .raw()
233            .is_some_and(|feed| std::mem::take(&mut feed.reset))
234        {
235            reset_pointer(&self.ctx);
236        }
237        #[cfg(not(target_arch = "wasm32"))]
238        if let (Feed::Winit(input), Some(window)) = (&mut self.feed, window) {
239            input.handle_output(window, output.platform_output);
240        }
241        self.primitives.clear();
242        self.primitives
243            .extend(self.ctx.tessellate(output.shapes, output.pixels_per_point));
244        self.textures = output.textures_delta;
245        self.screen.pixels_per_point = output.pixels_per_point;
246    }
247
248    fn paint(&mut self, encoder: &mut CommandEncoder, target: &TextureView) -> anyhow::Result<()> {
249        for (id, delta) in &self.textures.set {
250            retain_managed_texture(&mut self.managed_textures, *id, delta)?;
251            self.renderer
252                .update_texture(&self.device, &self.queue, *id, delta);
253        }
254        self.callbacks.extend(self.renderer.update_buffers(
255            &self.device,
256            &self.queue,
257            encoder,
258            &self.primitives,
259            &self.screen,
260        ));
261        {
262            let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
263                label: Some("loam-app::session::debug-layer"),
264                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
265                    view: target,
266                    depth_slice: None,
267                    resolve_target: None,
268                    ops: wgpu::Operations {
269                        load: wgpu::LoadOp::Load,
270                        store: wgpu::StoreOp::Store,
271                    },
272                })],
273                depth_stencil_attachment: None,
274                timestamp_writes: None,
275                occlusion_query_set: None,
276            });
277            self.renderer
278                .render(&mut pass.forget_lifetime(), &self.primitives, &self.screen);
279        }
280        for id in &self.textures.free {
281            self.renderer.free_texture(id);
282            self.managed_textures.remove(id);
283        }
284        self.textures.set.clear();
285        self.textures.free.clear();
286        Ok(())
287    }
288}
289
290fn retain_managed_texture(
291    textures: &mut HashMap<egui::TextureId, ManagedTexture>,
292    id: egui::TextureId,
293    delta: &egui::epaint::ImageDelta,
294) -> anyhow::Result<()> {
295    if !matches!(id, egui::TextureId::Managed(_)) {
296        return Ok(());
297    }
298    let Some(pos) = delta.pos else {
299        textures.insert(
300            id,
301            ManagedTexture {
302                image: delta.image.clone(),
303                options: delta.options,
304            },
305        );
306        return Ok(());
307    };
308    let texture = textures
309        .get_mut(&id)
310        .ok_or_else(|| anyhow::anyhow!("managed texture {id:?} has no full image"))?;
311    let egui::ImageData::Color(image) = &mut texture.image;
312    let egui::ImageData::Color(patch) = &delta.image;
313    let end_x = pos[0]
314        .checked_add(patch.size[0])
315        .filter(|end| *end <= image.size[0]);
316    let end_y = pos[1]
317        .checked_add(patch.size[1])
318        .filter(|end| *end <= image.size[1]);
319    if end_x.is_none() || end_y.is_none() {
320        anyhow::bail!("managed texture {id:?} partial update is out of bounds");
321    }
322    let image = Arc::make_mut(image);
323    for row in 0..patch.size[1] {
324        let source = row * patch.size[0];
325        let target = (pos[1] + row) * image.size[0] + pos[0];
326        image.pixels[target..target + patch.size[0]]
327            .copy_from_slice(&patch.pixels[source..source + patch.size[0]]);
328    }
329    texture.options = delta.options;
330    Ok(())
331}
332
333#[derive(Clone)]
334pub struct DebugLayer {
335    shared: Rc<RefCell<Layer>>,
336    #[cfg(not(target_arch = "wasm32"))]
337    window: Option<Arc<Window>>,
338}
339
340impl DebugLayer {
341    #[cfg(not(target_arch = "wasm32"))]
342    pub fn on_window(
343        gpu: &GpuContext,
344        format: TextureFormat,
345        window: Arc<Window>,
346        size: (u32, u32),
347    ) -> Self {
348        let ctx = egui::Context::default();
349        let feed = Feed::Winit(Box::new(WinitInput::new(&ctx, window.as_ref())));
350        let scale = window.scale_factor() as f32;
351        let layer = Layer::new(gpu, format, ctx, feed, size, scale);
352        Self {
353            shared: Rc::new(RefCell::new(layer)),
354            window: Some(window),
355        }
356    }
357
358    pub fn offscreen(
359        gpu: &GpuContext,
360        format: TextureFormat,
361        size: (u32, u32),
362        scale: f32,
363    ) -> Self {
364        let feed = Feed::Raw(RawFeed {
365            events: Vec::new(),
366            pending: Vec::new(),
367            modifiers: egui::Modifiers::default(),
368            canceled: false,
369            quiet: false,
370            #[cfg(any(target_arch = "wasm32", test))]
371            reset: false,
372        });
373        let layer = Layer::new(gpu, format, egui::Context::default(), feed, size, scale);
374        Self {
375            shared: Rc::new(RefCell::new(layer)),
376            #[cfg(not(target_arch = "wasm32"))]
377            window: None,
378        }
379    }
380
381    pub fn pass(&self) -> Box<dyn FramePass> {
382        Box::new(LayerPass {
383            shared: self.shared.clone(),
384        })
385    }
386
387    pub fn context(&self) -> egui::Context {
388        self.shared.borrow().ctx.clone()
389    }
390
391    pub fn resize(&self, width: u32, height: u32, scale: f32) {
392        let mut layer = self.shared.borrow_mut();
393        layer.screen.size_in_pixels = [width.max(1), height.max(1)];
394        if scale.is_finite() && scale > 0.0 {
395            layer.native_pixels_per_point = scale;
396        }
397    }
398
399    #[cfg(not(target_arch = "wasm32"))]
400    pub fn on_window_event(&self, event: &WindowEvent) -> bool {
401        let Some(window) = self.window.clone() else {
402            return false;
403        };
404        let mut layer = self.shared.borrow_mut();
405        let Feed::Winit(input) = &mut layer.feed else {
406            return false;
407        };
408        input.on_event(window.as_ref(), event).consumed
409    }
410
411    pub fn push(&self, event: egui::Event) {
412        if let Some(feed) = self.shared.borrow_mut().raw() {
413            feed.push(event);
414        }
415    }
416
417    #[cfg(any(target_arch = "wasm32", test))]
418    pub(super) fn cancel_touch(&self, id: u64, pos: egui::Pos2, pointer: bool) {
419        if let Some(feed) = self.shared.borrow_mut().raw() {
420            feed.cancel_touch(id, pos, pointer);
421        }
422    }
423
424    #[cfg(any(target_arch = "wasm32", test))]
425    pub(super) fn cancel_touches(&self, touches: &mut TouchCapture) {
426        for (id, pos) in touches.cancel_all() {
427            self.cancel_touch(id, egui::pos2(pos[0], pos[1]), false);
428        }
429        self.cancel_pointer();
430    }
431
432    #[cfg(any(target_arch = "wasm32", test))]
433    fn cancel_pointer(&self) {
434        if let Some(feed) = self.shared.borrow_mut().raw() {
435            feed.cancel_pointer();
436        }
437    }
438
439    pub fn modifiers(&self) -> egui::Modifiers {
440        match &self.shared.borrow().feed {
441            Feed::Raw(feed) => feed.modifiers,
442            #[cfg(not(target_arch = "wasm32"))]
443            Feed::Winit(_) => egui::Modifiers::default(),
444        }
445    }
446
447    pub fn set_modifiers(&self, next: egui::Modifiers) {
448        if let Some(feed) = self.shared.borrow_mut().raw() {
449            feed.modifiers = next;
450        }
451    }
452
453    pub fn begin(&self) -> egui::Context {
454        let mut layer = self.shared.borrow_mut();
455        let raw = layer.raw_input(
456            #[cfg(not(target_arch = "wasm32"))]
457            self.window.as_deref(),
458        );
459        layer.ctx.begin_pass(raw);
460        layer.ctx.clone()
461    }
462
463    pub fn finish(&self) {
464        #[cfg(not(target_arch = "wasm32"))]
465        let window = self.window.clone();
466        self.shared.borrow_mut().finish(
467            #[cfg(not(target_arch = "wasm32"))]
468            window.as_deref(),
469        );
470    }
471
472    pub fn take_callbacks(&self, into: &mut Vec<CommandBuffer>) {
473        into.append(&mut self.shared.borrow_mut().callbacks);
474    }
475}
476
477struct LayerPass {
478    shared: Rc<RefCell<Layer>>,
479}
480
481impl FramePass for LayerPass {
482    fn name(&self) -> &'static str {
483        DEBUG_LAYER
484    }
485
486    fn stage(&self) -> PassStage {
487        PassStage::Overlay
488    }
489
490    fn record(
491        &mut self,
492        encoder: &mut CommandEncoder,
493        target: &FrameTarget<'_>,
494    ) -> anyhow::Result<()> {
495        self.shared.borrow_mut().paint(encoder, target.color)
496    }
497
498    fn attach(&mut self, gpu: &GpuContext, frame: FrameFormat) -> anyhow::Result<()> {
499        let mut layer = self.shared.borrow_mut();
500        layer.device = gpu.device.clone();
501        layer.queue = gpu.queue.clone();
502        layer.format = frame.color;
503        layer.renderer = renderer(&gpu.device, frame.color);
504        let Layer {
505            renderer,
506            managed_textures,
507            ..
508        } = &mut *layer;
509        for (id, texture) in managed_textures {
510            renderer.update_texture(
511                &gpu.device,
512                &gpu.queue,
513                *id,
514                &egui::epaint::ImageDelta::full(texture.image.clone(), texture.options),
515            );
516        }
517        Ok(())
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use loam_render::device::FeatureRequest;
524    use wgpu::{
525        BackendOptions, Backends, Extent3d, Instance, InstanceDescriptor, NoopBackendOptions,
526        TextureDescriptor, TextureDimension, TextureUsages, TextureViewDescriptor,
527    };
528
529    use crate::wasm::input_queue::PointerPhase;
530
531    use super::*;
532
533    fn noop_gpu() -> GpuContext {
534        let instance = Instance::new(&InstanceDescriptor {
535            backends: Backends::NOOP,
536            backend_options: BackendOptions {
537                noop: NoopBackendOptions { enable: true },
538                ..Default::default()
539            },
540            ..Default::default()
541        });
542        pollster::block_on(GpuContext::new(instance, FeatureRequest::default(), None))
543            .expect("the noop backend always yields a context")
544    }
545
546    #[test]
547    fn browser_zoom_does_not_feed_output_scale_back_as_native_dpr() {
548        let gpu = noop_gpu();
549        let layer = DebugLayer::offscreen(&gpu, TextureFormat::Rgba8UnormSrgb, (600, 300), 2.0);
550        let mut layer = layer.shared.borrow_mut();
551        layer.ctx.set_zoom_factor(1.5);
552
553        let first = layer.raw_input(None);
554        layer.ctx.begin_pass(first);
555        layer.finish(None);
556        let second = layer.raw_input(None);
557
558        assert_eq!(layer.native_pixels_per_point, 2.0);
559        assert_eq!(layer.screen.pixels_per_point, 3.0);
560        assert_eq!(
561            second.screen_rect.map(|rect| rect.size()),
562            Some(egui::vec2(200.0, 100.0))
563        );
564    }
565
566    #[test]
567    fn managed_partial_texture_after_recovery_uses_replay_and_free_removes_it() {
568        let gpu = noop_gpu();
569        let format = TextureFormat::Rgba8UnormSrgb;
570        let layer = DebugLayer::offscreen(&gpu, format, (16, 16), 1.0);
571        let target = gpu.device.create_texture(&TextureDescriptor {
572            label: Some("debug layer texture recovery probe"),
573            size: Extent3d {
574                width: 16,
575                height: 16,
576                depth_or_array_layers: 1,
577            },
578            mip_level_count: 1,
579            sample_count: 1,
580            dimension: TextureDimension::D2,
581            format,
582            usage: TextureUsages::RENDER_ATTACHMENT,
583            view_formats: &[],
584        });
585        let target = target.create_view(&TextureViewDescriptor::default());
586        let paint = || {
587            let mut encoder = gpu
588                .device
589                .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
590            layer
591                .shared
592                .borrow_mut()
593                .paint(&mut encoder, &target)
594                .expect("texture deltas applied");
595            gpu.queue.submit(Some(encoder.finish()));
596        };
597        let red = egui::Color32::from_rgb(255, 0, 0);
598        let blue = egui::Color32::from_rgb(0, 0, 255);
599        let mut texture = layer.context().load_texture(
600            "managed recovery probe",
601            egui::ColorImage::filled([2, 2], red),
602            egui::TextureOptions::LINEAR,
603        );
604        let id = texture.id();
605        layer.begin();
606        layer.finish();
607        paint();
608
609        let frame = FrameFormat {
610            color: format,
611            depth: TextureFormat::Depth32Float,
612        };
613        let mut pass = layer.pass();
614        pass.attach(&gpu, frame).expect("renderer recreated");
615        texture.set_partial(
616            [1, 0],
617            egui::ColorImage::filled([1, 2], blue),
618            egui::TextureOptions::NEAREST,
619        );
620        layer.begin();
621        layer.finish();
622        paint();
623
624        {
625            let layer = layer.shared.borrow();
626            let cached = layer.managed_textures.get(&id).expect("texture retained");
627            let egui::ImageData::Color(image) = &cached.image;
628            assert_eq!(image.pixels, [red, blue, red, blue]);
629            assert_eq!(cached.options, egui::TextureOptions::NEAREST);
630        }
631
632        drop(texture);
633        layer.begin();
634        layer.finish();
635        paint();
636        pass.attach(&gpu, frame)
637            .expect("renderer recreated after free");
638        assert!(!layer.shared.borrow().managed_textures.contains_key(&id));
639    }
640    #[test]
641    fn canceled_touch_never_clicks_and_later_input_lands_in_the_next_pass() {
642        struct Observed {
643            a: egui::Rect,
644            b: egui::Rect,
645            c: egui::Rect,
646            clicked_a: bool,
647            clicked_b: bool,
648            clicked_c: bool,
649            released: usize,
650            any_released: bool,
651            down: bool,
652            touches: bool,
653            payload: bool,
654        }
655
656        fn observe(layer: &DebugLayer) -> Observed {
657            let context = layer.begin();
658            let mut a = egui::Rect::NOTHING;
659            let mut b = egui::Rect::NOTHING;
660            let mut c = egui::Rect::NOTHING;
661            let mut clicked_a = false;
662            let mut clicked_b = false;
663            let mut clicked_c = false;
664            egui::CentralPanel::default().show(&context, |ui| {
665                let (_, response) =
666                    ui.allocate_exact_size(egui::vec2(120.0, 48.0), egui::Sense::click_and_drag());
667                a = response.rect;
668                clicked_a = response.clicked();
669                if response.is_pointer_button_down_on() {
670                    context.set_dragged_id(response.id);
671                    egui::DragAndDrop::set_payload(&context, 7_u32);
672                }
673                let (_, response) =
674                    ui.allocate_exact_size(egui::vec2(120.0, 48.0), egui::Sense::click());
675                b = response.rect;
676                clicked_b = response.clicked();
677                let (_, response) =
678                    ui.allocate_exact_size(egui::vec2(120.0, 48.0), egui::Sense::click());
679                c = response.rect;
680                clicked_c = response.clicked();
681            });
682            let (released, any_released, down, touches) = context.input(|input| {
683                let released = input
684                    .raw
685                    .events
686                    .iter()
687                    .filter(|event| {
688                        matches!(event, egui::Event::PointerButton { pressed: false, .. })
689                    })
690                    .count();
691                (
692                    released,
693                    input.pointer.any_released(),
694                    input.pointer.any_down(),
695                    input.any_touches(),
696                )
697            });
698            let payload = egui::DragAndDrop::has_any_payload(&context);
699            layer.finish();
700            Observed {
701                a,
702                b,
703                c,
704                clicked_a,
705                clicked_b,
706                clicked_c,
707                released,
708                any_released,
709                down,
710                touches,
711                payload,
712            }
713        }
714
715        fn down(layer: &DebugLayer, touches: &mut TouchCapture, id: u64, pos: egui::Pos2) {
716            assert!(touches.route(id, PointerPhase::Down, true, [pos.x, pos.y]));
717            layer.push(egui::Event::Touch {
718                device_id: egui::TouchDeviceId(0),
719                id: egui::TouchId::from(id),
720                phase: egui::TouchPhase::Start,
721                pos,
722                force: None,
723            });
724            layer.push(egui::Event::PointerMoved(pos));
725            layer.push(egui::Event::PointerButton {
726                pos,
727                button: egui::PointerButton::Primary,
728                pressed: true,
729                modifiers: egui::Modifiers::default(),
730            });
731        }
732
733        fn cancel(layer: &DebugLayer, touches: &mut TouchCapture, id: u64, pos: egui::Pos2) {
734            let pointer = touches.is_pointer(id);
735            assert!(touches.route(id, PointerPhase::Cancel, false, [pos.x, pos.y]));
736            layer.cancel_touch(id, pos, pointer);
737        }
738
739        fn up(layer: &DebugLayer, touches: &mut TouchCapture, id: u64, pos: egui::Pos2) {
740            let pointer = touches.is_pointer(id);
741            assert!(touches.route(id, PointerPhase::Up, false, [pos.x, pos.y]));
742            layer.push(egui::Event::Touch {
743                device_id: egui::TouchDeviceId(0),
744                id: egui::TouchId::from(id),
745                phase: egui::TouchPhase::End,
746                pos,
747                force: None,
748            });
749            if pointer {
750                layer.push(egui::Event::PointerButton {
751                    pos,
752                    button: egui::PointerButton::Primary,
753                    pressed: false,
754                    modifiers: egui::Modifiers::default(),
755                });
756                layer.push(egui::Event::PointerGone);
757            }
758        }
759
760        let gpu = noop_gpu();
761        let layer = DebugLayer::offscreen(&gpu, TextureFormat::Rgba8UnormSrgb, (256, 192), 1.0);
762        let mut touches = TouchCapture::default();
763        let initial = observe(&layer);
764
765        down(&layer, &mut touches, 1, initial.a.center());
766        let held_a = observe(&layer);
767        assert!(!held_a.clicked_a && !held_a.clicked_b && !held_a.clicked_c);
768        assert!(held_a.down && held_a.touches && held_a.payload);
769
770        layer.cancel_touches(&mut touches);
771        down(&layer, &mut touches, 2, initial.c.center());
772        cancel(&layer, &mut touches, 2, initial.c.center());
773        down(&layer, &mut touches, 3, initial.b.center());
774        up(&layer, &mut touches, 3, initial.b.center());
775
776        let canceled = observe(&layer);
777        assert!(!canceled.clicked_a && !canceled.clicked_b && !canceled.clicked_c);
778        assert_eq!(canceled.released, 0);
779        assert!(!canceled.any_released && !canceled.touches);
780
781        let quiet = observe(&layer);
782        assert!(!quiet.clicked_a && !quiet.clicked_b && !quiet.clicked_c);
783        assert!(!quiet.down && !quiet.touches);
784
785        let valid_b = observe(&layer);
786        assert!(!valid_b.clicked_a && valid_b.clicked_b && !valid_b.clicked_c);
787        assert_eq!(valid_b.released, 1);
788        assert!(valid_b.any_released);
789        assert!(!valid_b.down && !valid_b.touches && !valid_b.payload);
790
791        down(&layer, &mut touches, 4, initial.a.center());
792        let held_a = observe(&layer);
793        assert!(held_a.down && held_a.touches && held_a.payload);
794
795        up(&layer, &mut touches, 4, initial.a.center());
796        down(&layer, &mut touches, 5, initial.c.center());
797        cancel(&layer, &mut touches, 5, initial.c.center());
798        down(&layer, &mut touches, 6, initial.b.center());
799        up(&layer, &mut touches, 6, initial.b.center());
800
801        let valid_a = observe(&layer);
802        assert!(valid_a.clicked_a && !valid_a.clicked_b && !valid_a.clicked_c);
803        assert_eq!(valid_a.released, 1);
804        assert!(valid_a.any_released && !valid_a.touches);
805
806        let quiet = observe(&layer);
807        assert!(!quiet.clicked_a && !quiet.clicked_b && !quiet.clicked_c);
808        assert!(!quiet.down && !quiet.touches);
809
810        let valid_b = observe(&layer);
811        assert!(!valid_b.clicked_a && valid_b.clicked_b && !valid_b.clicked_c);
812        assert_eq!(valid_b.released, 1);
813        assert!(!valid_b.down && !valid_b.touches && !valid_b.payload);
814        assert!(!touches.is_pointer(6));
815        assert_eq!(touches.cancel_all().count(), 0);
816    }
817}