1#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
7pub enum ScaleMode {
8 #[default]
14 Integer,
15 Fit,
19 Stretch,
21}
22
23#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
25pub enum ScaleFilter {
26 #[default]
28 Nearest,
29 Linear,
31}
32
33impl ScaleFilter {
34 pub(crate) const fn index(self) -> usize {
37 match self {
38 Self::Nearest => 0,
39 Self::Linear => 1,
40 }
41 }
42}
43
44#[derive(Copy, Clone, Debug, PartialEq)]
46pub struct CanvasRect {
47 pub origin: [f32; 2],
49 pub size: [f32; 2],
51}
52
53impl CanvasRect {
54 pub fn window_to_canvas(&self, canvas: [u32; 2], pos: [f32; 2]) -> [f32; 2] {
57 let s = self.canvas_per_window(canvas);
58 [
59 (pos[0] - self.origin[0]) * s[0],
60 (pos[1] - self.origin[1]) * s[1],
61 ]
62 }
63
64 pub fn canvas_per_window(&self, canvas: [u32; 2]) -> [f32; 2] {
66 [
67 canvas[0] as f32 / self.size[0].max(1e-6),
68 canvas[1] as f32 / self.size[1].max(1e-6),
69 ]
70 }
71}
72
73pub(crate) fn layout(window: [u32; 2], canvas: [u32; 2], mode: ScaleMode) -> CanvasRect {
76 let (ww, wh) = (window[0].max(1) as f32, window[1].max(1) as f32);
77 let (cw, ch) = (canvas[0].max(1) as f32, canvas[1].max(1) as f32);
78 let fit = (ww / cw).min(wh / ch);
79 let scale = match mode {
80 ScaleMode::Stretch => {
81 return CanvasRect {
82 origin: [0.0, 0.0],
83 size: [ww, wh],
84 };
85 }
86 ScaleMode::Integer if fit >= 1.0 => fit.floor(),
87 ScaleMode::Integer | ScaleMode::Fit => fit,
88 };
89 let size = [(cw * scale).round(), (ch * scale).round()];
90 CanvasRect {
91 origin: [
92 ((ww - size[0]) * 0.5).floor(),
93 ((wh - size[1]) * 0.5).floor(),
94 ],
95 size,
96 }
97}
98
99pub(crate) struct RenderTexture {
102 pub size: [u32; 2],
103 pub texture: wgpu::Texture,
104 pub view: wgpu::TextureView,
105 pub blit_bind_groups: [wgpu::BindGroup; 2],
107}
108
109impl RenderTexture {
110 pub fn new(
111 device: &wgpu::Device,
112 blit: &Blit,
113 format: wgpu::TextureFormat,
114 size: [u32; 2],
115 label: &str,
116 ) -> Self {
117 let size = [size[0].max(1), size[1].max(1)];
118 let texture = device.create_texture(&wgpu::TextureDescriptor {
119 label: Some(label),
120 size: wgpu::Extent3d {
121 width: size[0],
122 height: size[1],
123 depth_or_array_layers: 1,
124 },
125 mip_level_count: 1,
126 sample_count: 1,
127 dimension: wgpu::TextureDimension::D2,
128 format,
129 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
130 | wgpu::TextureUsages::TEXTURE_BINDING
131 | wgpu::TextureUsages::COPY_SRC
132 | wgpu::TextureUsages::COPY_DST,
133 view_formats: &[],
134 });
135 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
136 let blit_bind_groups = blit.bind_groups(device, &view);
137 Self {
138 size,
139 texture,
140 view,
141 blit_bind_groups,
142 }
143 }
144}
145
146pub(crate) struct Canvas {
149 pub tex: RenderTexture,
150 pub follows_window: bool,
153}
154
155impl Canvas {
156 pub fn size(&self) -> [u32; 2] {
157 self.tex.size
158 }
159}
160
161pub(crate) struct Blit {
163 pipeline: wgpu::RenderPipeline,
164 layout: wgpu::BindGroupLayout,
165 samplers: [wgpu::Sampler; 2],
166}
167
168impl Blit {
169 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
170 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
171 label: Some("blit.wgsl"),
172 source: wgpu::ShaderSource::Wgsl(include_str!("blit.wgsl").into()),
173 });
174 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
175 label: Some("blit.bgl"),
176 entries: &[
177 wgpu::BindGroupLayoutEntry {
178 binding: 0,
179 visibility: wgpu::ShaderStages::FRAGMENT,
180 ty: wgpu::BindingType::Texture {
181 sample_type: wgpu::TextureSampleType::Float { filterable: true },
182 view_dimension: wgpu::TextureViewDimension::D2,
183 multisampled: false,
184 },
185 count: None,
186 },
187 wgpu::BindGroupLayoutEntry {
188 binding: 1,
189 visibility: wgpu::ShaderStages::FRAGMENT,
190 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
191 count: None,
192 },
193 ],
194 });
195 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
196 label: Some("blit.layout"),
197 bind_group_layouts: &[Some(&layout)],
198 immediate_size: 0,
199 });
200 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
201 label: Some("blit.pipeline"),
202 layout: Some(&pipeline_layout),
203 vertex: wgpu::VertexState {
204 module: &shader,
205 entry_point: Some("vs_main"),
206 compilation_options: Default::default(),
207 buffers: &[],
208 },
209 fragment: Some(wgpu::FragmentState {
210 module: &shader,
211 entry_point: Some("fs_main"),
212 compilation_options: Default::default(),
213 targets: &[Some(wgpu::ColorTargetState {
214 format,
215 blend: None,
216 write_mask: wgpu::ColorWrites::ALL,
217 })],
218 }),
219 primitive: wgpu::PrimitiveState::default(),
220 depth_stencil: None,
221 multisample: wgpu::MultisampleState::default(),
222 multiview_mask: None,
223 cache: None,
224 });
225 let sampler = |filter| {
226 device.create_sampler(&wgpu::SamplerDescriptor {
227 label: Some("blit.sampler"),
228 mag_filter: filter,
229 min_filter: filter,
230 ..Default::default()
231 })
232 };
233 Self {
234 pipeline,
235 layout,
236 samplers: [
237 sampler(wgpu::FilterMode::Nearest),
238 sampler(wgpu::FilterMode::Linear),
239 ],
240 }
241 }
242
243 pub fn bind_groups(
246 &self,
247 device: &wgpu::Device,
248 src: &wgpu::TextureView,
249 ) -> [wgpu::BindGroup; 2] {
250 self.samplers.each_ref().map(|sampler| {
251 device.create_bind_group(&wgpu::BindGroupDescriptor {
252 label: Some("blit.bg"),
253 layout: &self.layout,
254 entries: &[
255 wgpu::BindGroupEntry {
256 binding: 0,
257 resource: wgpu::BindingResource::TextureView(src),
258 },
259 wgpu::BindGroupEntry {
260 binding: 1,
261 resource: wgpu::BindingResource::Sampler(sampler),
262 },
263 ],
264 })
265 })
266 }
267
268 pub fn draw(
270 &self,
271 encoder: &mut wgpu::CommandEncoder,
272 bind_group: &wgpu::BindGroup,
273 dst: &wgpu::TextureView,
274 rect: CanvasRect,
275 clear: wgpu::Color,
276 ) {
277 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
278 label: Some("blit.pass"),
279 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
280 view: dst,
281 depth_slice: None,
282 resolve_target: None,
283 ops: wgpu::Operations {
284 load: wgpu::LoadOp::Clear(clear),
285 store: wgpu::StoreOp::Store,
286 },
287 })],
288 depth_stencil_attachment: None,
289 occlusion_query_set: None,
290 timestamp_writes: None,
291 multiview_mask: None,
292 });
293 if rect.size[0] < 1.0 || rect.size[1] < 1.0 {
294 return;
295 }
296 pass.set_viewport(
297 rect.origin[0],
298 rect.origin[1],
299 rect.size[0],
300 rect.size[1],
301 0.0,
302 1.0,
303 );
304 pass.set_pipeline(&self.pipeline);
305 pass.set_bind_group(0, bind_group, &[]);
306 pass.draw(0..3, 0..1);
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn integer_scale_centres_with_border() {
316 let r = layout([1920, 1080], [320, 180], ScaleMode::Integer);
318 assert_eq!(r.origin, [0.0, 0.0]);
319 assert_eq!(r.size, [1920.0, 1080.0]);
320 let r = layout([1000, 700], [320, 180], ScaleMode::Integer);
322 assert_eq!(r.size, [960.0, 540.0]);
323 assert_eq!(r.origin, [20.0, 80.0]);
324 }
325
326 #[test]
327 fn integer_falls_back_to_fit_when_window_is_smaller() {
328 let r = layout([160, 90], [320, 180], ScaleMode::Integer);
329 assert_eq!(r.size, [160.0, 90.0]);
330 }
331
332 #[test]
333 fn fit_is_fractional() {
334 let r = layout([1920, 1080], [800, 600], ScaleMode::Fit);
336 assert_eq!(r.size, [1440.0, 1080.0]);
337 assert_eq!(r.origin, [240.0, 0.0]);
338 assert_eq!(
339 layout([1920, 1080], [800, 600], ScaleMode::Integer).size,
340 [800.0, 600.0]
341 );
342 }
343
344 #[test]
345 fn stretch_fills_window() {
346 let r = layout([1000, 700], [320, 180], ScaleMode::Stretch);
347 assert_eq!(r.origin, [0.0, 0.0]);
348 assert_eq!(r.size, [1000.0, 700.0]);
349 }
350
351 #[test]
352 fn window_to_canvas_inverts_layout() {
353 let canvas = [320, 180];
354 let r = layout([1000, 700], canvas, ScaleMode::Integer);
355 assert_eq!(r.window_to_canvas(canvas, [20.0, 80.0]), [0.0, 0.0]);
356 assert_eq!(r.window_to_canvas(canvas, [980.0, 620.0]), [320.0, 180.0]);
357 assert_eq!(r.window_to_canvas(canvas, [23.0, 83.0]), [1.0, 1.0]);
358 assert_eq!(r.canvas_per_window(canvas), [1.0 / 3.0, 1.0 / 3.0]);
359 }
360}