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