maplibre/render/resource/
surface.rs

1//! Utilities for handling surfaces which can be either headless or headed. A headed surface has
2//! a handle to a window. A headless surface renders to a texture.
3
4use std::{mem::size_of, sync::Arc};
5
6use wgpu::TextureFormatFeatures;
7
8use crate::{
9    render::{
10        error::RenderError,
11        eventually::HasChanged,
12        resource::texture::TextureView,
13        settings::{Msaa, RendererSettings},
14    },
15    window::{HeadedMapWindow, MapWindow, PhysicalSize},
16};
17
18pub struct BufferDimensions {
19    pub width: u32,
20    pub height: u32,
21    pub unpadded_bytes_per_row: u32,
22    pub padded_bytes_per_row: u32,
23}
24
25impl BufferDimensions {
26    fn new(size: PhysicalSize) -> Self {
27        let bytes_per_pixel = size_of::<u32>() as u32;
28        let unpadded_bytes_per_row = size.width() * bytes_per_pixel;
29
30        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
31        let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align;
32        let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding;
33        Self {
34            width: size.width(),
35            height: size.height(),
36            unpadded_bytes_per_row,
37            padded_bytes_per_row,
38        }
39    }
40}
41
42pub struct WindowHead {
43    surface: wgpu::Surface<'static>,
44    size: PhysicalSize,
45
46    texture_format: wgpu::TextureFormat,
47    /// Non-sRGB variant of texture_format used for rendering.
48    /// Prevents automatic linear→sRGB conversion by the GPU, since our colors
49    /// (from CSS) are already in sRGB space.
50    render_format: wgpu::TextureFormat,
51    present_mode: wgpu::PresentMode,
52    texture_format_features: TextureFormatFeatures,
53}
54
55/// Returns the non-sRGB variant of a texture format.
56/// This prevents the GPU from applying automatic linear→sRGB gamma conversion,
57/// which would double-gamma colors that are already in sRGB space (e.g., CSS colors).
58fn strip_srgb(format: wgpu::TextureFormat) -> wgpu::TextureFormat {
59    match format {
60        wgpu::TextureFormat::Rgba8UnormSrgb => wgpu::TextureFormat::Rgba8Unorm,
61        wgpu::TextureFormat::Bgra8UnormSrgb => wgpu::TextureFormat::Bgra8Unorm,
62        other => other,
63    }
64}
65
66impl WindowHead {
67    pub fn resize_and_configure(&mut self, width: u32, height: u32, device: &wgpu::Device) {
68        self.size = PhysicalSize::new(width, height).unwrap();
69        self.configure(device);
70    }
71
72    pub fn configure(&self, device: &wgpu::Device) {
73        let mut view_formats = vec![self.texture_format];
74        if self.render_format != self.texture_format {
75            view_formats.push(self.render_format);
76        }
77
78        let surface_config = wgpu::SurfaceConfiguration {
79            alpha_mode: wgpu::CompositeAlphaMode::Auto,
80            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
81            format: self.texture_format,
82            width: self.size.width(),
83            height: self.size.height(),
84            present_mode: self.present_mode,
85            view_formats,
86            desired_maximum_frame_latency: 2,
87        };
88
89        self.surface.configure(device, &surface_config);
90    }
91
92    pub fn recreate_surface<MW>(
93        &mut self,
94        window: &MW,
95        instance: &wgpu::Instance,
96    ) -> Result<(), RenderError>
97    where
98        MW: MapWindow + HeadedMapWindow,
99    {
100        self.surface = unsafe {
101            instance
102                .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window.handle())?)?
103        };
104        Ok(())
105    }
106
107    pub fn surface(&self) -> &wgpu::Surface {
108        &self.surface
109    }
110}
111
112pub struct BufferedTextureHead {
113    texture: wgpu::Texture,
114    texture_format: wgpu::TextureFormat,
115    output_buffer: wgpu::Buffer,
116    buffer_dimensions: BufferDimensions,
117}
118
119#[cfg(feature = "headless")]
120#[derive(thiserror::Error, Debug)]
121pub enum WriteImageError {
122    #[error("error while rendering to image")]
123    WriteImage(#[from] png::EncodingError),
124    #[error("could not create file to save as an image")]
125    CreateImageFileFailed(#[from] std::io::Error),
126}
127
128#[cfg(feature = "headless")]
129impl BufferedTextureHead {
130    pub fn map_async(&self, device: &wgpu::Device) -> wgpu::BufferSlice {
131        // Note that we're not calling `.await` here.
132        let buffer_slice = self.output_buffer.slice(..);
133        buffer_slice.map_async(wgpu::MapMode::Read, |_| ());
134
135        // Poll the device in a blocking manner so that our future resolves.
136        // In an actual application, `device.poll(...)` should
137        // be called in an event loop or on another thread.
138        device.poll(wgpu::Maintain::Wait);
139        buffer_slice
140    }
141
142    pub fn unmap(&self) {
143        self.output_buffer.unmap();
144    }
145
146    pub fn write_png<'a>(
147        &self,
148        padded_buffer: &wgpu::BufferView<'a>,
149        png_output_path: &str,
150    ) -> Result<(), WriteImageError> {
151        use std::{fs::File, io::Write};
152        let mut png_encoder = png::Encoder::new(
153            File::create(png_output_path)?,
154            self.buffer_dimensions.width as u32,
155            self.buffer_dimensions.height as u32,
156        );
157        png_encoder.set_depth(png::BitDepth::Eight);
158        png_encoder.set_color(png::ColorType::Rgba);
159        let mut png_writer = png_encoder
160            .write_header()?
161            .into_stream_writer_with_size(self.buffer_dimensions.unpadded_bytes_per_row as usize)?;
162
163        // from the padded_buffer we write just the unpadded bytes into the image
164        for chunk in padded_buffer.chunks(self.buffer_dimensions.padded_bytes_per_row as usize) {
165            png_writer
166                .write_all(&chunk[..self.buffer_dimensions.unpadded_bytes_per_row as usize])?
167        }
168        png_writer.finish()?;
169        Ok(())
170    }
171
172    pub fn copy_texture(&self) -> wgpu::ImageCopyTexture<'_> {
173        self.texture.as_image_copy()
174    }
175
176    pub fn buffer(&self) -> &wgpu::Buffer {
177        &self.output_buffer
178    }
179
180    pub fn bytes_per_row(&self) -> u32 {
181        self.buffer_dimensions.padded_bytes_per_row
182    }
183}
184
185pub enum Head {
186    Headed(WindowHead),
187    Headless(Arc<BufferedTextureHead>),
188}
189
190pub struct Surface {
191    size: PhysicalSize,
192    head: Head,
193}
194
195impl Surface {
196    pub fn from_surface<MW>(
197        surface: wgpu::Surface<'static>,
198        adapter: &wgpu::Adapter,
199        window: &MW,
200        settings: &RendererSettings,
201    ) -> Self
202    where
203        MW: MapWindow + HeadedMapWindow,
204    {
205        let size = window.size();
206
207        let capabilities = surface.get_capabilities(adapter);
208        log::info!("adapter capabilities on surface: {capabilities:?}");
209
210        let texture_format = settings
211            .texture_format
212            .or_else(|| capabilities.formats.first().cloned())
213            .unwrap_or(wgpu::TextureFormat::Rgba8Unorm);
214        let render_format = strip_srgb(texture_format);
215        log::info!("surface format: {texture_format:?}, render format: {render_format:?}");
216
217        let texture_format_features = adapter.get_texture_format_features(texture_format);
218        log::info!("format features: {texture_format_features:?}");
219
220        Self {
221            size,
222            head: Head::Headed(WindowHead {
223                surface,
224                size,
225                texture_format,
226                render_format,
227                texture_format_features,
228                present_mode: settings.present_mode,
229            }),
230        }
231    }
232
233    // TODO: Give better name
234    pub fn from_image<MW>(device: &wgpu::Device, window: &MW, settings: &RendererSettings) -> Self
235    where
236        MW: MapWindow,
237    {
238        let size = window.size();
239
240        // It is a WebGPU requirement that ImageCopyBuffer.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
241        // So we calculate padded_bytes_per_row by rounding unpadded_bytes_per_row
242        // up to the next multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT.
243        // https://en.wikipedia.org/wiki/Data_structure_alignment#Computing_padding
244        let buffer_dimensions = BufferDimensions::new(size);
245
246        // The output buffer lets us retrieve the data as an array
247        let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
248            label: Some("BufferedTextureHead buffer"),
249            size: (buffer_dimensions.padded_bytes_per_row * buffer_dimensions.height) as u64,
250            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
251            mapped_at_creation: false,
252        });
253
254        // TODO: Is this a sane default?
255        let format = settings
256            .texture_format
257            .unwrap_or(wgpu::TextureFormat::Rgba8Unorm);
258
259        let texture_descriptor = wgpu::TextureDescriptor {
260            label: Some("Surface texture"),
261            size: wgpu::Extent3d {
262                width: size.width(),
263                height: size.height(),
264                depth_or_array_layers: 1,
265            },
266            mip_level_count: 1,
267            sample_count: 1,
268            dimension: wgpu::TextureDimension::D2,
269            format,
270            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
271            view_formats: &[format],
272        };
273        let texture = device.create_texture(&texture_descriptor);
274
275        Self {
276            size,
277            head: Head::Headless(Arc::new(BufferedTextureHead {
278                texture,
279                texture_format: format,
280                output_buffer,
281                buffer_dimensions,
282            })),
283        }
284    }
285
286    pub fn surface_format(&self) -> wgpu::TextureFormat {
287        match &self.head {
288            Head::Headed(headed) => headed.render_format,
289            Head::Headless(headless) => headless.texture_format,
290        }
291    }
292
293    #[tracing::instrument(name = "create_view", skip_all)]
294    pub fn create_view(&self, device: &wgpu::Device) -> TextureView {
295        match &self.head {
296            Head::Headed(window) => {
297                let WindowHead {
298                    surface,
299                    render_format,
300                    ..
301                } = window;
302                let frame = match surface.get_current_texture() {
303                    Ok(view) => view,
304                    Err(wgpu::SurfaceError::Outdated) => {
305                        log::warn!("surface outdated");
306                        window.configure(device);
307                        surface
308                            .get_current_texture()
309                            .expect("Error reconfiguring surface")
310                    }
311                    err => err.expect("Failed to acquire next swap chain texture!"),
312                };
313                // Create view with non-sRGB format to prevent double-gamma on CSS colors
314                let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
315                    format: Some(*render_format),
316                    ..Default::default()
317                });
318                TextureView::SurfaceTexture {
319                    view,
320                    texture: frame,
321                }
322            }
323            Head::Headless(arc) => arc
324                .texture
325                .create_view(&wgpu::TextureViewDescriptor::default())
326                .into(),
327        }
328    }
329
330    pub fn size(&self) -> PhysicalSize {
331        self.size
332    }
333
334    pub fn resize(&mut self, size: PhysicalSize) {
335        self.size = size;
336    }
337
338    pub fn reconfigure(&mut self, device: &wgpu::Device) {
339        match &mut self.head {
340            Head::Headed(window) => {
341                if window.has_changed(&(self.size.width(), self.size.height())) {
342                    window.resize_and_configure(self.size.width(), self.size.height(), device);
343                }
344            }
345            Head::Headless(_) => {}
346        }
347    }
348
349    pub fn recreate<MW>(
350        &mut self,
351        window: &MW,
352        instance: &wgpu::Instance,
353    ) -> Result<(), RenderError>
354    where
355        MW: MapWindow + HeadedMapWindow,
356    {
357        match &mut self.head {
358            Head::Headed(window_head) => {
359                if window_head.has_changed(&(self.size.width(), self.size.height())) {
360                    window_head.recreate_surface(window, instance)?;
361                }
362            }
363            Head::Headless(_) => {}
364        }
365        Ok(())
366    }
367
368    pub fn head(&self) -> &Head {
369        &self.head
370    }
371
372    pub fn head_mut(&mut self) -> &mut Head {
373        &mut self.head
374    }
375
376    pub fn is_multisampling_supported(&self, msaa: Msaa) -> bool {
377        match &self.head {
378            Head::Headed(headed) => {
379                let max_sample_count = {
380                    let flags = headed.texture_format_features.flags;
381                    if flags.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X8) {
382                        8
383                    } else if flags.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4) {
384                        4
385                    } else if flags.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X2) {
386                        2
387                    } else {
388                        1
389                    }
390                };
391                let is_supported = msaa.samples <= max_sample_count;
392                if !is_supported {
393                    log::debug!("Multisampling is not supported on surface");
394                }
395                is_supported
396            }
397            Head::Headless(_) => false, // TODO: support multisampling on headless
398        }
399    }
400}
401
402impl HasChanged for WindowHead {
403    /// Tuple of width and height
404    type Criteria = (u32, u32);
405
406    fn has_changed(&self, criteria: &Self::Criteria) -> bool {
407        self.size.width() != criteria.0 || self.size.height() != criteria.1
408    }
409}