Skip to main content

wde_wgpu/resources/
texture.rs

1//! Texture creation and copy helpers built on `wgpu::Texture`.
2
3use wde_logger::prelude::*;
4
5use crate::RenderInstanceData;
6
7/// Surface texture.
8pub type SurfaceTexture = wgpu::SurfaceTexture;
9
10/// Texture view
11pub type TextureView = wgpu::TextureView;
12
13/// Texture usages.
14pub type TextureUsages = wgpu::TextureUsages;
15
16/// Texture format.
17pub type TextureFormat = wgpu::TextureFormat;
18
19/// Texture filter mode.
20pub type FilterMode = wgpu::FilterMode;
21
22/// The swapchain texture format.
23pub const SWAPCHAIN_FORMAT: TextureFormat = TextureFormat::Bgra8UnormSrgb;
24/// The depth texture format.
25pub const DEPTH_FORMAT: TextureFormat = TextureFormat::Depth24PlusStencil8;
26
27/// Texture wrapper with a ready-to-use view and sampler.
28///
29/// # Examples
30/// Create a render target and clear it:
31/// ```rust,no_run
32/// use wde_wgpu::{texture::{Texture, TextureFormat, TextureUsages}, instance::RenderInstanceData};
33///
34/// let color = Texture::new(
35///     instance,
36///     "color-target",
37///     (1280, 720),
38///     TextureFormat::Rgba8Unorm,
39///     TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
40/// );
41/// ```
42///
43/// Upload raw pixel data (RGBA8):
44/// ```rust,no_run
45/// use wde_wgpu::{texture::{Texture, TextureFormat, TextureUsages}, instance::RenderInstanceData};
46///
47/// let texture = Texture::new(
48///     instance,
49///     "albedo",
50///     (512, 512),
51///     TextureFormat::Rgba8Unorm,
52///     TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
53/// );
54/// texture.copy_from_buffer(instance, TextureFormat::Rgba8Unorm, pixels);
55/// // Or
56/// texture.copy_from_buffer_layered(instance, TextureFormat::Rgba8Unorm, 0, pixels); // For texture arrays
57/// ```
58///
59/// Copy one GPU texture into another:
60/// ```rust,no_run
61/// # use wde_wgpu::{texture::{Texture, TextureFormat, TextureUsages}, instance::RenderInstanceData};
62///
63/// let dst = Texture::new(
64///     instance,
65///     "blit-target",
66///     (1024, 1024),
67///     TextureFormat::Rgba8Unorm,
68///     TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
69/// );
70/// dst.copy_from_texture(instance, src, (1024, 1024));
71/// ```
72pub struct Texture {
73    pub label: String,
74    pub texture: wgpu::Texture,
75    pub format: TextureFormat,
76    pub view: TextureView,
77    pub sampler: wgpu::Sampler,
78    pub size: (u32, u32),
79    pub sample_count: u32,
80    pub layer_count: u32,
81    pub mip_level_count: u32,
82    pub filterable: bool
83}
84
85impl std::fmt::Debug for Texture {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("Texture")
88            .field("label", &self.label)
89            .field("sampler", &self.sampler)
90            .field("size", &self.size)
91            .field("sample_count", &self.sample_count)
92            .field("layer_count", &self.layer_count)
93            .field("mip_level_count", &self.mip_level_count)
94            .field("filterable", &self.filterable)
95            .finish()
96    }
97}
98
99impl Texture {
100    /// Create a new texture.
101    ///
102    /// # Arguments
103    ///
104    /// * `instance` - Game instance.
105    /// * `label` - Label of the texture. This is only for debugging purposes.
106    /// * `size` - Size of the texture (width, height).
107    /// * `format` - Format of the texture (e.g. Rgba8Unorm, Depth32Float, etc.).
108    /// * `usage` - Usage of the texture (e.g. RENDER_ATTACHMENT, COPY_SRC, COPY_DST, etc.).
109    /// * `sample_count` - Sample count of the texture (e.g. 1 for no MSAA, 4 for 4x MSAA, etc.).
110    /// * `layer_count` - Number of layers in the texture array. Default is 1 (for non-array textures).
111    /// * `mip_level_count` - Number of mip levels. Default is 1 (no mipmaps). If set to 0, it will be auto-calculated based on the texture size.
112    #[allow(clippy::too_many_arguments)]
113    pub fn new(
114        instance: &RenderInstanceData<'_>,
115        label: &str,
116        size: (u32, u32),
117        format: TextureFormat,
118        usage: TextureUsages,
119        sample_count: u32,
120        layer_count: u32,
121        mip_level_count: u32,
122        filterable: bool,
123        tiling: bool
124    ) -> Self {
125        event!(LogLevel::TRACE, "Creating wgpu texture {}.", label);
126
127        // Calculate max mip levels if requested
128        let mip_level_count = if mip_level_count == 0 {
129            (size.0.max(size.1) as f32).log2().floor() as u32 + 1
130        } else {
131            mip_level_count
132        };
133
134        // Create texture
135        let texture = instance.device.create_texture(&wgpu::TextureDescriptor {
136            label: Some(format!("{}-texture", label).as_str()),
137            size: wgpu::Extent3d {
138                width: size.0,
139                height: size.1,
140                depth_or_array_layers: layer_count
141            },
142            mip_level_count,
143            sample_count,
144            dimension: wgpu::TextureDimension::D2,
145            format,
146            usage: usage | wgpu::TextureUsages::COPY_DST,
147            view_formats: &[]
148        });
149
150        // Create texture view
151        let view = texture.create_view(&wgpu::TextureViewDescriptor {
152            label: Some(format!("{}-texture-view", label).as_str()),
153            format: if format == DEPTH_FORMAT {
154                None
155            } else {
156                Some(format)
157            },
158            dimension: if format == DEPTH_FORMAT {
159                None
160            } else if layer_count > 1 {
161                Some(wgpu::TextureViewDimension::D2Array)
162            } else {
163                Some(wgpu::TextureViewDimension::D2)
164            },
165            aspect: wgpu::TextureAspect::All,
166            base_mip_level: 0,
167            base_array_layer: 0,
168            mip_level_count: None,
169            array_layer_count: if layer_count > 1 {
170                Some(layer_count)
171            } else {
172                None
173            },
174            usage: None
175        });
176
177        // Create sampler: tiling textures repeat, and are filtered anisotropically when mipmapped
178        let address_mode = match tiling {
179            true => wgpu::AddressMode::Repeat,
180            false => wgpu::AddressMode::ClampToEdge
181        };
182        let sampler = instance.device.create_sampler(&wgpu::SamplerDescriptor {
183            label: Some(format!("{}-texture-sampler", label).as_str()),
184            address_mode_u: address_mode,
185            address_mode_v: address_mode,
186            address_mode_w: wgpu::AddressMode::ClampToEdge,
187            mag_filter: wgpu::FilterMode::Linear,
188            min_filter: wgpu::FilterMode::Linear,
189            mipmap_filter: if mip_level_count > 1 {
190                wgpu::FilterMode::Linear
191            } else {
192                wgpu::FilterMode::Nearest
193            },
194            lod_min_clamp: 0.0,
195            lod_max_clamp: mip_level_count as f32,
196            compare: None,
197            anisotropy_clamp: if tiling && mip_level_count > 1 { 16 } else { 1 },
198            border_color: None
199        });
200
201        // Return texture
202        Self {
203            label: label.to_string(),
204            texture,
205            format,
206            view,
207            sampler,
208            size,
209            sample_count,
210            layer_count,
211            mip_level_count,
212            filterable
213        }
214    }
215
216    /// Copy buffer to texture.
217    /// It is assumed that the buffer is the same size as the texture.
218    /// It will be copied on the next queue submit.
219    /// Note that the buffer must have the COPY_DST usage.
220    ///
221    /// # Arguments
222    ///
223    /// * `instance` - Game instance.
224    /// * `texture_format` - The wgpu texture format.
225    /// * `buffer` - Image buffer.
226    pub fn copy_from_buffer(
227        &self,
228        instance: &RenderInstanceData,
229        texture_format: TextureFormat,
230        buffer: &[u8]
231    ) {
232        // Retrieve size corresponding to the texture format
233        let format_size = match texture_format.block_dimensions() {
234            (1, 1) => texture_format.block_copy_size(None).unwrap() as usize,
235            _ => panic!("Using pixel_size for compressed textures is invalid")
236        };
237
238        // Copy buffer to texture
239        instance.queue.write_texture(
240            wgpu::TexelCopyTextureInfo {
241                texture: &self.texture,
242                mip_level: 0,
243                origin: wgpu::Origin3d::ZERO,
244                aspect: wgpu::TextureAspect::All
245            },
246            buffer,
247            wgpu::TexelCopyBufferLayout {
248                offset: 0,
249                bytes_per_row: Some(self.size.0 * format_size as u32),
250                rows_per_image: None
251            },
252            wgpu::Extent3d {
253                width: self.size.0,
254                height: self.size.1,
255                depth_or_array_layers: 1
256            }
257        );
258    }
259
260    /// Copy texture to buffer at a given array layer.
261    /// It is assumed that the buffer is the same size as the texture.
262    /// It will be copied on the next queue submit.
263    /// Note that the buffer must have the COPY_DST usage.
264    ///
265    /// # Arguments
266    ///
267    /// * `instance` - Game instance.
268    /// * `texture_format` - The wgpu texture format.
269    /// * `array_layer` - The array layer to copy from (for texture arrays). Default is 0 for non-array textures.
270    /// * `buffer` - Image buffer.
271    pub fn copy_from_buffer_layered(
272        &self,
273        instance: &RenderInstanceData,
274        texture_format: TextureFormat,
275        array_layer: u32,
276        buffer: &[u8]
277    ) {
278        // Retrieve size corresponding to the texture format
279        let format_size = match texture_format.block_dimensions() {
280            (1, 1) => texture_format.block_copy_size(None).unwrap() as usize,
281            _ => panic!("Using pixel_size for compressed textures is invalid")
282        };
283
284        // Copy buffer to texture
285        instance.queue.write_texture(
286            wgpu::TexelCopyTextureInfo {
287                texture: &self.texture,
288                mip_level: 0,
289                origin: wgpu::Origin3d {
290                    x: 0,
291                    y: 0,
292                    z: array_layer
293                },
294                aspect: wgpu::TextureAspect::All
295            },
296            buffer,
297            wgpu::TexelCopyBufferLayout {
298                offset: 0,
299                bytes_per_row: Some(self.size.0 * format_size as u32),
300                rows_per_image: None
301            },
302            wgpu::Extent3d {
303                width: self.size.0,
304                height: self.size.1,
305                depth_or_array_layers: 1
306            }
307        );
308    }
309
310    /// Copy texture to texture.
311    /// It is assumed that the texture is the same size as the source texture.
312    /// Note that the input texture must have the COPY_SRC usage, and the output texture must have the COPY_DST usage.
313    ///
314    /// # Arguments
315    ///
316    /// * `instance` - Game instance.
317    /// * `texture` - Texture to copy from.
318    /// * `size` - Size of the texture.
319    pub fn copy_from_texture(
320        &self,
321        instance: &RenderInstanceData<'_>,
322        texture: &wgpu::Texture,
323        size: (u32, u32)
324    ) {
325        // Create command buffer
326        let mut command = crate::command_buffer::CommandBuffer::new(instance, "Copy Texture");
327
328        // Copy texture to texture
329        command.encoder().copy_texture_to_texture(
330            wgpu::TexelCopyTextureInfo {
331                texture,
332                mip_level: 0,
333                origin: wgpu::Origin3d::ZERO,
334                aspect: wgpu::TextureAspect::All
335            },
336            wgpu::TexelCopyTextureInfo {
337                texture: &self.texture,
338                mip_level: 0,
339                origin: wgpu::Origin3d::ZERO,
340                aspect: wgpu::TextureAspect::All
341            },
342            wgpu::Extent3d {
343                width: size.0,
344                height: size.1,
345                depth_or_array_layers: 1
346            }
347        );
348
349        // Submit the commands
350        command.submit(instance);
351    }
352
353    /// Copy texture from a surface texture (e.g. swapchain frame).
354    /// It is assumed that the texture is the same size as the source texture.
355    /// Note that the input texture must have the COPY_SRC usage, and the output texture must have the COPY_DST usage.
356    ///
357    /// # Arguments
358    ///
359    /// * `instance` - Game instance.
360    /// * `surface_texture` - Surface texture to copy from.
361    /// * `size` - Size of the texture.
362    pub fn copy_from_surface_texture(
363        &self,
364        instance: &RenderInstanceData<'_>,
365        surface_texture: &SurfaceTexture,
366        size: (u32, u32)
367    ) {
368        self.copy_from_texture(instance, &surface_texture.texture, size);
369    }
370
371    /// Copy texture to texture at a given array layer.
372    /// It is assumed that the texture is the same size as the source texture.
373    /// Note that the input texture must have the COPY_SRC usage, and the output texture must have the COPY_DST usage.
374    ///
375    /// # Arguments
376    ///
377    /// * `instance` - Game instance.
378    /// * `texture` - Texture to copy from.
379    /// * `array_layer` - The array layer to copy to (for texture arrays). Default is 0 for non-array textures.
380    /// * `size` - Size of the texture.
381    pub fn copy_from_texture_layered(
382        &self,
383        instance: &RenderInstanceData<'_>,
384        texture: &wgpu::Texture,
385        array_layer: usize,
386        size: (u32, u32)
387    ) {
388        // Create command buffer
389        let mut command = crate::command_buffer::CommandBuffer::new(instance, "Copy Texture");
390
391        // Copy texture to texture
392        command.encoder().copy_texture_to_texture(
393            wgpu::TexelCopyTextureInfo {
394                texture,
395                mip_level: 0,
396                origin: wgpu::Origin3d::ZERO,
397                aspect: wgpu::TextureAspect::All
398            },
399            wgpu::TexelCopyTextureInfo {
400                texture: &self.texture,
401                mip_level: 0,
402                origin: wgpu::Origin3d {
403                    x: 0,
404                    y: 0,
405                    z: array_layer as u32
406                },
407                aspect: wgpu::TextureAspect::All
408            },
409            wgpu::Extent3d {
410                width: size.0,
411                height: size.1,
412                depth_or_array_layers: 1
413            }
414        );
415
416        // Submit the commands
417        command.submit(instance);
418    }
419
420    /// Generate mipmaps for this texture.
421    /// The texture must have been created with TEXTURE_BINDING | RENDER_ATTACHMENT | COPY_SRC usage.
422    /// This method uses a simple blit approach to downsample each mip level from the previous one.
423    ///
424    /// # Arguments
425    ///
426    /// * `instance` - Game instance.
427    pub fn generate_mipmaps(&self, instance: &RenderInstanceData<'_>) {
428        if self.mip_level_count <= 1 {
429            trace!(
430                "Texture {} does not have multiple mip levels ({}), skipping mipmap generation.",
431                self.label, self.mip_level_count
432            );
433            return;
434        }
435
436        trace!(
437            "Generating mipmaps for texture {} with {} mip levels and {} layers.",
438            self.label, self.mip_level_count, self.layer_count
439        );
440
441        // Create the blit shader module
442        let shader_source = r#"
443struct VertexOutput {
444    @builtin(position) position: vec4<f32>,
445    @location(0) tex_coord: vec2<f32>,
446}
447
448@vertex
449fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
450    var out: VertexOutput;
451    let x = f32((vertex_index << 1u) & 2u);
452    let y = f32(vertex_index & 2u);
453    out.position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
454    out.tex_coord = vec2<f32>(x, 1.0 - y);
455    return out;
456}
457
458@group(0) @binding(0) var src_texture: texture_2d<f32>;
459@group(0) @binding(1) var src_sampler: sampler;
460
461@fragment
462fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
463    return textureSample(src_texture, src_sampler, in.tex_coord);
464}
465"#;
466
467        let shader = instance
468            .device
469            .create_shader_module(wgpu::ShaderModuleDescriptor {
470                label: Some("mipmap_blit_shader"),
471                source: wgpu::ShaderSource::Wgsl(shader_source.into())
472            });
473
474        // Create sampler for mipmap generation (linear filtering for downsampling)
475        let blit_sampler = instance.device.create_sampler(&wgpu::SamplerDescriptor {
476            label: Some("mipmap_blit_sampler"),
477            address_mode_u: wgpu::AddressMode::ClampToEdge,
478            address_mode_v: wgpu::AddressMode::ClampToEdge,
479            address_mode_w: wgpu::AddressMode::ClampToEdge,
480            mag_filter: wgpu::FilterMode::Linear,
481            min_filter: wgpu::FilterMode::Linear,
482            mipmap_filter: wgpu::FilterMode::Nearest,
483            ..Default::default()
484        });
485
486        // Create bind group layout
487        let bind_group_layout =
488            instance
489                .device
490                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
491                    label: Some("mipmap_blit_bind_group_layout"),
492                    entries: &[
493                        wgpu::BindGroupLayoutEntry {
494                            binding: 0,
495                            visibility: wgpu::ShaderStages::FRAGMENT,
496                            ty: wgpu::BindingType::Texture {
497                                sample_type: wgpu::TextureSampleType::Float { filterable: true },
498                                view_dimension: wgpu::TextureViewDimension::D2,
499                                multisampled: false
500                            },
501                            count: None
502                        },
503                        wgpu::BindGroupLayoutEntry {
504                            binding: 1,
505                            visibility: wgpu::ShaderStages::FRAGMENT,
506                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
507                            count: None
508                        }
509                    ]
510                });
511
512        // Create pipeline layout
513        let pipeline_layout =
514            instance
515                .device
516                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
517                    label: Some("mipmap_blit_pipeline_layout"),
518                    bind_group_layouts: &[&bind_group_layout],
519                    push_constant_ranges: &[]
520                });
521
522        // Create render pipeline
523        let pipeline = instance
524            .device
525            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
526                label: Some("mipmap_blit_pipeline"),
527                layout: Some(&pipeline_layout),
528                vertex: wgpu::VertexState {
529                    module: &shader,
530                    entry_point: Some("vs_main"),
531                    buffers: &[],
532                    compilation_options: Default::default()
533                },
534                fragment: Some(wgpu::FragmentState {
535                    module: &shader,
536                    entry_point: Some("fs_main"),
537                    targets: &[Some(wgpu::ColorTargetState {
538                        format: self.format,
539                        blend: None,
540                        write_mask: wgpu::ColorWrites::ALL
541                    })],
542                    compilation_options: Default::default()
543                }),
544                primitive: wgpu::PrimitiveState {
545                    topology: wgpu::PrimitiveTopology::TriangleList,
546                    ..Default::default()
547                },
548                depth_stencil: None,
549                multisample: wgpu::MultisampleState::default(),
550                multiview: None,
551                cache: None
552            });
553
554        // Generate each mip level
555        let mut command = crate::command_buffer::CommandBuffer::new(instance, "Generate Mipmaps");
556
557        for layer in 0..self.layer_count {
558            for mip_level in 1..self.mip_level_count {
559                let src_view = self.texture.create_view(&wgpu::TextureViewDescriptor {
560                    label: Some(&format!("{}_mip_{}_src", self.label, mip_level)),
561                    format: Some(self.format),
562                    dimension: Some(wgpu::TextureViewDimension::D2),
563                    aspect: wgpu::TextureAspect::All,
564                    base_mip_level: mip_level - 1,
565                    mip_level_count: Some(1),
566                    base_array_layer: layer,
567                    array_layer_count: Some(1),
568                    usage: None
569                });
570
571                let dst_view = self.texture.create_view(&wgpu::TextureViewDescriptor {
572                    label: Some(&format!("{}_mip_{}_dst", self.label, mip_level)),
573                    format: Some(self.format),
574                    dimension: Some(wgpu::TextureViewDimension::D2),
575                    aspect: wgpu::TextureAspect::All,
576                    base_mip_level: mip_level,
577                    mip_level_count: Some(1),
578                    base_array_layer: layer,
579                    array_layer_count: Some(1),
580                    usage: None
581                });
582
583                let bind_group = instance
584                    .device
585                    .create_bind_group(&wgpu::BindGroupDescriptor {
586                        label: Some(&format!("{}_mip_{}_bind_group", self.label, mip_level)),
587                        layout: &bind_group_layout,
588                        entries: &[
589                            wgpu::BindGroupEntry {
590                                binding: 0,
591                                resource: wgpu::BindingResource::TextureView(&src_view)
592                            },
593                            wgpu::BindGroupEntry {
594                                binding: 1,
595                                resource: wgpu::BindingResource::Sampler(&blit_sampler)
596                            }
597                        ]
598                    });
599
600                {
601                    let mut render_pass =
602                        command
603                            .encoder()
604                            .begin_render_pass(&wgpu::RenderPassDescriptor {
605                                label: Some(&format!(
606                                    "{}_mip_{}_render_pass",
607                                    self.label, mip_level
608                                )),
609                                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
610                                    view: &dst_view,
611                                    resolve_target: None,
612                                    ops: wgpu::Operations {
613                                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
614                                        store: wgpu::StoreOp::Store
615                                    },
616                                    depth_slice: None
617                                })],
618                                depth_stencil_attachment: None,
619                                timestamp_writes: None,
620                                occlusion_query_set: None
621                            });
622
623                    render_pass.set_pipeline(&pipeline);
624                    render_pass.set_bind_group(0, &bind_group, &[]);
625                    render_pass.draw(0..3, 0..1);
626                }
627            }
628        }
629
630        command.submit(instance);
631        event!(
632            LogLevel::TRACE,
633            "Finished generating mipmaps for texture {}.",
634            self.label
635        );
636    }
637}