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