radiantkit_core/render/
renderer.rs

1use crate::ScreenDescriptor;
2use epaint::emath::NumExt;
3use epaint::{Primitive, Vertex};
4use std::borrow::Cow;
5use std::collections::HashMap;
6use std::num::NonZeroU64;
7use std::ops::Range;
8use wgpu::util::DeviceExt;
9
10/// Uniform buffer used when rendering.
11#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
12#[repr(C)]
13struct UniformBuffer {
14    screen_size_in_points: [f32; 2],
15    // Uniform buffers need to be at least 16 bytes in WebGL.
16    // See https://github.com/gfx-rs/wgpu/issues/2072
17    _padding: [u32; 2],
18}
19
20impl PartialEq for UniformBuffer {
21    fn eq(&self, other: &Self) -> bool {
22        self.screen_size_in_points == other.screen_size_in_points
23    }
24}
25
26struct SlicedBuffer {
27    buffer: wgpu::Buffer,
28    slices: Vec<Range<usize>>,
29    capacity: wgpu::BufferAddress,
30}
31
32pub struct RadiantRenderer {
33    pipeline: wgpu::RenderPipeline,
34
35    index_buffer: SlicedBuffer,
36    vertex_buffer: SlicedBuffer,
37
38    uniform_buffer: wgpu::Buffer,
39    previous_uniform_buffer_content: UniformBuffer,
40    uniform_bind_group: wgpu::BindGroup,
41    texture_bind_group_layout: wgpu::BindGroupLayout,
42
43    /// Map of egui texture IDs to textures and their associated bindgroups (texture view +
44    /// sampler). The texture may be None if the TextureId is just a handle to a user-provided
45    /// sampler.
46    textures: HashMap<epaint::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
47    // next_user_texture_id: u64,
48    samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
49}
50
51impl RadiantRenderer {
52    pub fn new(
53        device: &wgpu::Device,
54        output_color_format: wgpu::TextureFormat,
55        output_depth_format: Option<wgpu::TextureFormat>,
56        msaa_samples: u32,
57    ) -> Self {
58        let module = device.create_shader_module(wgpu::include_wgsl!("egui.wgsl"));
59
60        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
61            label: Some("egui_uniform_buffer"),
62            contents: bytemuck::cast_slice(&[UniformBuffer {
63                screen_size_in_points: [0.0, 0.0],
64                _padding: Default::default(),
65            }]),
66            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
67        });
68
69        let uniform_bind_group_layout =
70            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
71                label: Some("egui_uniform_bind_group_layout"),
72                entries: &[wgpu::BindGroupLayoutEntry {
73                    binding: 0,
74                    visibility: wgpu::ShaderStages::VERTEX,
75                    ty: wgpu::BindingType::Buffer {
76                        has_dynamic_offset: false,
77                        min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
78                        ty: wgpu::BufferBindingType::Uniform,
79                    },
80                    count: None,
81                }],
82            });
83
84        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
85            label: Some("egui_uniform_bind_group"),
86            layout: &uniform_bind_group_layout,
87            entries: &[wgpu::BindGroupEntry {
88                binding: 0,
89                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
90                    buffer: &uniform_buffer,
91                    offset: 0,
92                    size: None,
93                }),
94            }],
95        });
96
97        let texture_bind_group_layout =
98            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
99                label: Some("egui_texture_bind_group_layout"),
100                entries: &[
101                    wgpu::BindGroupLayoutEntry {
102                        binding: 0,
103                        visibility: wgpu::ShaderStages::FRAGMENT,
104                        ty: wgpu::BindingType::Texture {
105                            multisampled: false,
106                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
107                            view_dimension: wgpu::TextureViewDimension::D2,
108                        },
109                        count: None,
110                    },
111                    wgpu::BindGroupLayoutEntry {
112                        binding: 1,
113                        visibility: wgpu::ShaderStages::FRAGMENT,
114                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
115                        count: None,
116                    },
117                ],
118            });
119
120        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
121            label: Some("egui_pipeline_layout"),
122            bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
123            push_constant_ranges: &[],
124        });
125
126        let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
127            format,
128            depth_write_enabled: false,
129            depth_compare: wgpu::CompareFunction::Always,
130            stencil: wgpu::StencilState::default(),
131            bias: wgpu::DepthBiasState::default(),
132        });
133
134        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
135            label: Some("egui_pipeline"),
136            layout: Some(&pipeline_layout),
137            vertex: wgpu::VertexState {
138                entry_point: "vs_main",
139                module: &module,
140                buffers: &[wgpu::VertexBufferLayout {
141                    array_stride: 5 * 4,
142                    step_mode: wgpu::VertexStepMode::Vertex,
143                    // 0: vec2 position
144                    // 1: vec2 texture coordinates
145                    // 2: uint color
146                    attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32],
147                }],
148            },
149            primitive: wgpu::PrimitiveState {
150                topology: wgpu::PrimitiveTopology::TriangleList,
151                unclipped_depth: false,
152                conservative: false,
153                cull_mode: None,
154                front_face: wgpu::FrontFace::default(),
155                polygon_mode: wgpu::PolygonMode::default(),
156                strip_index_format: None,
157            },
158            depth_stencil,
159            multisample: wgpu::MultisampleState {
160                alpha_to_coverage_enabled: false,
161                count: msaa_samples,
162                mask: !0,
163            },
164
165            fragment: Some(wgpu::FragmentState {
166                module: &module,
167                entry_point: if output_color_format.is_srgb() {
168                    log::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
169                    "fs_main_linear_framebuffer"
170                } else {
171                    "fs_main_gamma_framebuffer" // this is what we prefer
172                },
173                targets: &[Some(wgpu::ColorTargetState {
174                    format: output_color_format,
175                    blend: Some(wgpu::BlendState {
176                        color: wgpu::BlendComponent {
177                            src_factor: wgpu::BlendFactor::One,
178                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
179                            operation: wgpu::BlendOperation::Add,
180                        },
181                        alpha: wgpu::BlendComponent {
182                            src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
183                            dst_factor: wgpu::BlendFactor::One,
184                            operation: wgpu::BlendOperation::Add,
185                        },
186                    }),
187                    write_mask: wgpu::ColorWrites::ALL,
188                })],
189            }),
190            multiview: None,
191        });
192
193        const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
194            (std::mem::size_of::<Vertex>() * 1024) as _;
195        const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
196            (std::mem::size_of::<u32>() * 1024 * 3) as _;
197
198        Self {
199            pipeline,
200            vertex_buffer: SlicedBuffer {
201                buffer: create_vertex_buffer(device, VERTEX_BUFFER_START_CAPACITY),
202                slices: Vec::with_capacity(64),
203                capacity: VERTEX_BUFFER_START_CAPACITY,
204            },
205            index_buffer: SlicedBuffer {
206                buffer: create_index_buffer(device, INDEX_BUFFER_START_CAPACITY),
207                slices: Vec::with_capacity(64),
208                capacity: INDEX_BUFFER_START_CAPACITY,
209            },
210            uniform_buffer,
211            // Buffers on wgpu are zero initialized, so this is indeed its current state!
212            previous_uniform_buffer_content: UniformBuffer {
213                screen_size_in_points: [0.0, 0.0],
214                _padding: [0, 0],
215            },
216            uniform_bind_group,
217            texture_bind_group_layout,
218            textures: HashMap::default(),
219            // next_user_texture_id: 0,
220            samplers: HashMap::default(),
221        }
222    }
223
224    pub fn update_buffers(
225        &mut self,
226        device: &wgpu::Device,
227        queue: &wgpu::Queue,
228        screen_descriptor: &ScreenDescriptor,
229        paint_jobs: &[epaint::ClippedPrimitive],
230    ) {
231        let screen_size_in_points = screen_descriptor.screen_size_in_points();
232
233        let uniform_buffer_content = UniformBuffer {
234            screen_size_in_points,
235            _padding: Default::default(),
236        };
237        if uniform_buffer_content != self.previous_uniform_buffer_content {
238            // crate::profile_scope!("update uniforms");
239            queue.write_buffer(
240                &self.uniform_buffer,
241                0,
242                bytemuck::cast_slice(&[uniform_buffer_content]),
243            );
244            self.previous_uniform_buffer_content = uniform_buffer_content;
245        }
246
247        let (vertex_count, index_count) = {
248            // crate::profile_scope!("count_vertices_indices");
249            paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
250                match &clipped_primitive.primitive {
251                    Primitive::Mesh(mesh) => {
252                        (acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len())
253                    }
254                    Primitive::Callback(_callback) => {
255                        // if let Some(c) = callback.callback.downcast_ref::<Callback>() {
256                        //     callbacks.push(c.0.as_ref());
257                        // } else {
258                        //     log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
259                        // };
260                        // acc
261                        (0, 0)
262                    }
263                }
264            })
265        };
266
267        if index_count > 0 {
268            // crate::profile_scope!("indices");
269
270            self.index_buffer.slices.clear();
271            let required_index_buffer_size = (std::mem::size_of::<u32>() * index_count) as u64;
272            if self.index_buffer.capacity < required_index_buffer_size {
273                // Resize index buffer if needed.
274                self.index_buffer.capacity =
275                    (self.index_buffer.capacity * 2).at_least(required_index_buffer_size);
276                self.index_buffer.buffer = create_index_buffer(device, self.index_buffer.capacity);
277            }
278
279            let mut index_buffer_staging = queue
280                .write_buffer_with(
281                    &self.index_buffer.buffer,
282                    0,
283                    NonZeroU64::new(required_index_buffer_size).unwrap(),
284                )
285                .expect("Failed to create staging buffer for index data");
286            let mut index_offset = 0;
287            for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
288                match primitive {
289                    Primitive::Mesh(mesh) => {
290                        let size = mesh.indices.len() * std::mem::size_of::<u32>();
291                        let slice = index_offset..(size + index_offset);
292                        index_buffer_staging[slice.clone()]
293                            .copy_from_slice(bytemuck::cast_slice(&mesh.indices));
294                        self.index_buffer.slices.push(slice);
295                        index_offset += size;
296                    }
297                    Primitive::Callback(_) => {}
298                }
299            }
300        }
301        if vertex_count > 0 {
302            // crate::profile_scope!("vertices");
303
304            self.vertex_buffer.slices.clear();
305            let required_vertex_buffer_size = (std::mem::size_of::<Vertex>() * vertex_count) as u64;
306            if self.vertex_buffer.capacity < required_vertex_buffer_size {
307                // Resize vertex buffer if needed.
308                self.vertex_buffer.capacity =
309                    (self.vertex_buffer.capacity * 2).at_least(required_vertex_buffer_size);
310                self.vertex_buffer.buffer =
311                    create_vertex_buffer(device, self.vertex_buffer.capacity);
312            }
313
314            let mut vertex_buffer_staging = queue
315                .write_buffer_with(
316                    &self.vertex_buffer.buffer,
317                    0,
318                    NonZeroU64::new(required_vertex_buffer_size).unwrap(),
319                )
320                .expect("Failed to create staging buffer for vertex data");
321            let mut vertex_offset = 0;
322            for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
323                match primitive {
324                    Primitive::Mesh(mesh) => {
325                        let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
326                        let slice = vertex_offset..(size + vertex_offset);
327                        vertex_buffer_staging[slice.clone()]
328                            .copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
329                        self.vertex_buffer.slices.push(slice);
330                        vertex_offset += size;
331                    }
332                    Primitive::Callback(_) => {}
333                }
334            }
335        }
336    }
337
338    pub fn update_texture(
339        &mut self,
340        device: &wgpu::Device,
341        queue: &wgpu::Queue,
342        id: epaint::TextureId,
343        image_delta: &epaint::ImageDelta,
344    ) {
345        // crate::profile_function!();
346
347        let width = image_delta.image.width() as u32;
348        let height = image_delta.image.height() as u32;
349
350        let size = wgpu::Extent3d {
351            width,
352            height,
353            depth_or_array_layers: 1,
354        };
355
356        let data_color32 = match &image_delta.image {
357            epaint::ImageData::Color(image) => {
358                assert_eq!(
359                    width as usize * height as usize,
360                    image.pixels.len(),
361                    "Mismatch between texture size and texel count"
362                );
363                Cow::Borrowed(&image.pixels)
364            }
365            epaint::ImageData::Font(image) => {
366                assert_eq!(
367                    width as usize * height as usize,
368                    image.pixels.len(),
369                    "Mismatch between texture size and texel count"
370                );
371                Cow::Owned(image.srgba_pixels(None).collect::<Vec<_>>())
372            }
373        };
374        let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
375
376        let queue_write_data_to_texture = |texture, origin| {
377            queue.write_texture(
378                wgpu::ImageCopyTexture {
379                    texture,
380                    mip_level: 0,
381                    origin,
382                    aspect: wgpu::TextureAspect::All,
383                },
384                data_bytes,
385                wgpu::ImageDataLayout {
386                    offset: 0,
387                    bytes_per_row: Some(4 * width),
388                    rows_per_image: Some(height),
389                },
390                size,
391            );
392        };
393
394        if let Some(pos) = image_delta.pos {
395            // update the existing texture
396            let (texture, _bind_group) = self
397                .textures
398                .get(&id)
399                .expect("Tried to update a texture that has not been allocated yet.");
400            let origin = wgpu::Origin3d {
401                x: pos[0] as u32,
402                y: pos[1] as u32,
403                z: 0,
404            };
405            queue_write_data_to_texture(
406                texture.as_ref().expect("Tried to update user texture."),
407                origin,
408            );
409        } else {
410            // allocate a new texture
411            // Use same label for all resources associated with this texture id (no point in retyping the type)
412            let label_str = format!("egui_texid_{id:?}");
413            let label = Some(label_str.as_str());
414            let texture = device.create_texture(&wgpu::TextureDescriptor {
415                label,
416                size,
417                mip_level_count: 1,
418                sample_count: 1,
419                dimension: wgpu::TextureDimension::D2,
420                format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
421                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
422                view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
423            });
424            let sampler = self
425                .samplers
426                .entry(image_delta.options)
427                .or_insert_with(|| create_sampler(image_delta.options, device));
428            let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
429                label,
430                layout: &self.texture_bind_group_layout,
431                entries: &[
432                    wgpu::BindGroupEntry {
433                        binding: 0,
434                        resource: wgpu::BindingResource::TextureView(
435                            &texture.create_view(&wgpu::TextureViewDescriptor::default()),
436                        ),
437                    },
438                    wgpu::BindGroupEntry {
439                        binding: 1,
440                        resource: wgpu::BindingResource::Sampler(sampler),
441                    },
442                ],
443            });
444            let origin = wgpu::Origin3d::ZERO;
445            queue_write_data_to_texture(&texture, origin);
446            self.textures.insert(id, (Some(texture), bind_group));
447        };
448    }
449
450    pub fn render<'a>(
451        &'a self,
452        render_pass: &mut wgpu::RenderPass<'a>,
453        screen_descriptor: &ScreenDescriptor,
454        paint_jobs: &'a [epaint::ClippedPrimitive],
455    ) {
456        let pixels_per_point = screen_descriptor.pixels_per_point;
457        let size_in_pixels = screen_descriptor.size_in_pixels;
458
459        // Whether or not we need to reset the render pass because a paint callback has just
460        // run.
461        let mut needs_reset = true;
462
463        let mut index_buffer_slices = self.index_buffer.slices.iter();
464        let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
465
466        for epaint::ClippedPrimitive {
467            clip_rect,
468            primitive,
469        } in paint_jobs
470        {
471            if needs_reset {
472                render_pass.set_viewport(
473                    0.0,
474                    0.0,
475                    size_in_pixels[0] as f32,
476                    size_in_pixels[1] as f32,
477                    0.0,
478                    1.0,
479                );
480                render_pass.set_pipeline(&self.pipeline);
481                render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
482                needs_reset = false;
483            }
484
485            {
486                let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels);
487
488                if rect.width == 0 || rect.height == 0 {
489                    // Skip rendering zero-sized clip areas.
490                    if let Primitive::Mesh(_) = primitive {
491                        // If this is a mesh, we need to advance the index and vertex buffer iterators:
492                        index_buffer_slices.next().unwrap();
493                        vertex_buffer_slices.next().unwrap();
494                    }
495                    continue;
496                }
497
498                render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
499            }
500
501            match primitive {
502                Primitive::Mesh(mesh) => {
503                    let index_buffer_slice = index_buffer_slices.next().unwrap();
504                    let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
505
506                    if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) {
507                        render_pass.set_bind_group(1, bind_group, &[]);
508                        render_pass.set_index_buffer(
509                            self.index_buffer.buffer.slice(
510                                index_buffer_slice.start as u64..index_buffer_slice.end as u64,
511                            ),
512                            wgpu::IndexFormat::Uint32,
513                        );
514                        render_pass.set_vertex_buffer(
515                            0,
516                            self.vertex_buffer.buffer.slice(
517                                vertex_buffer_slice.start as u64..vertex_buffer_slice.end as u64,
518                            ),
519                        );
520                        render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
521                    } else {
522                        log::warn!("Missing texture: {:?}", mesh.texture_id);
523                    }
524                }
525                Primitive::Callback(_callback) => {
526                    // let Some(cbfn) = callback.callback.downcast_ref::<Callback>() else {
527                    //     // We already warned in the `prepare` callback
528                    //     continue;
529                    // };
530
531                    // if callback.rect.is_positive() {
532                    //     crate::profile_scope!("callback");
533
534                    //     needs_reset = true;
535
536                    //     {
537                    //         // We're setting a default viewport for the render pass as a
538                    //         // courtesy for the user, so that they don't have to think about
539                    //         // it in the simple case where they just want to fill the whole
540                    //         // paint area.
541                    //         //
542                    //         // The user still has the possibility of setting their own custom
543                    //         // viewport during the paint callback, effectively overriding this
544                    //         // one.
545
546                    //         let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
547                    //         let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
548
549                    //         render_pass.set_viewport(
550                    //             min.x,
551                    //             min.y,
552                    //             max.x - min.x,
553                    //             max.y - min.y,
554                    //             0.0,
555                    //             1.0,
556                    //         );
557                    //     }
558
559                    //     cbfn.0.paint(
560                    //         PaintCallbackInfo {
561                    //             viewport: callback.rect,
562                    //             clip_rect: *clip_rect,
563                    //             pixels_per_point,
564                    //             screen_size_px: size_in_pixels,
565                    //         },
566                    //         render_pass,
567                    //         &self.callback_resources,
568                    //     );
569                    // }
570                }
571            }
572        }
573
574        render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
575    }
576}
577
578fn create_sampler(
579    options: epaint::textures::TextureOptions,
580    device: &wgpu::Device,
581) -> wgpu::Sampler {
582    let mag_filter = match options.magnification {
583        epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
584        epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
585    };
586    let min_filter = match options.minification {
587        epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
588        epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
589    };
590    device.create_sampler(&wgpu::SamplerDescriptor {
591        label: Some(&format!(
592            "egui sampler (mag: {mag_filter:?}, min {min_filter:?})"
593        )),
594        mag_filter,
595        min_filter,
596        ..Default::default()
597    })
598}
599
600fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
601    // crate::profile_function!();
602    device.create_buffer(&wgpu::BufferDescriptor {
603        label: Some("egui_vertex_buffer"),
604        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
605        size,
606        mapped_at_creation: false,
607    })
608}
609
610fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
611    // crate::profile_function!();
612    device.create_buffer(&wgpu::BufferDescriptor {
613        label: Some("egui_index_buffer"),
614        usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
615        size,
616        mapped_at_creation: false,
617    })
618}
619/// A Rect in physical pixel space, used for setting clipping rectangles.
620struct ScissorRect {
621    x: u32,
622    y: u32,
623    width: u32,
624    height: u32,
625}
626
627impl ScissorRect {
628    fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
629        // Transform clip rect to physical pixels:
630        let clip_min_x = pixels_per_point * clip_rect.min.x;
631        let clip_min_y = pixels_per_point * clip_rect.min.y;
632        let clip_max_x = pixels_per_point * clip_rect.max.x;
633        let clip_max_y = pixels_per_point * clip_rect.max.y;
634
635        // Round to integer:
636        let clip_min_x = clip_min_x.round() as u32;
637        let clip_min_y = clip_min_y.round() as u32;
638        let clip_max_x = clip_max_x.round() as u32;
639        let clip_max_y = clip_max_y.round() as u32;
640
641        // Clamp:
642        let clip_min_x = clip_min_x.clamp(0, target_size[0]);
643        let clip_min_y = clip_min_y.clamp(0, target_size[1]);
644        let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]);
645        let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]);
646
647        Self {
648            x: clip_min_x,
649            y: clip_min_y,
650            width: clip_max_x - clip_min_x,
651            height: clip_max_y - clip_min_y,
652        }
653    }
654}