astrelis_render/
blit.rs

1//! Texture blitting utilities for fullscreen quad rendering.
2//!
3//! This module provides a simple API for rendering textures to the screen,
4//! useful for video backgrounds, post-processing, and image display.
5
6use crate::context::GraphicsContext;
7use crate::Renderer;
8
9/// A renderer for blitting textures to the screen.
10///
11/// This provides an easy way to render a texture as a fullscreen quad,
12/// useful for video backgrounds, splash screens, or post-processing effects.
13///
14/// # Example
15///
16/// ```ignore
17/// let blit_renderer = BlitRenderer::new(context);
18///
19/// // In render loop:
20/// blit_renderer.blit(&mut render_pass, &texture_view);
21/// ```
22pub struct BlitRenderer {
23    pipeline: wgpu::RenderPipeline,
24    bind_group_layout: wgpu::BindGroupLayout,
25    sampler: wgpu::Sampler,
26    vertex_buffer: wgpu::Buffer,
27    context: &'static GraphicsContext,
28}
29
30impl BlitRenderer {
31    /// Create a new blit renderer.
32    ///
33    /// # Arguments
34    ///
35    /// * `context` - The graphics context
36    /// * `target_format` - The format of the render target (typically the surface format)
37    pub fn new(context: &'static GraphicsContext, target_format: wgpu::TextureFormat) -> Self {
38        Self::new_with_options(context, target_format, BlitOptions::default())
39    }
40
41    /// Create a new blit renderer with custom options.
42    pub fn new_with_options(
43        context: &'static GraphicsContext,
44        target_format: wgpu::TextureFormat,
45        options: BlitOptions,
46    ) -> Self {
47        let renderer = Renderer::new(context);
48
49        // Create shader
50        let shader = renderer.create_shader(
51            Some("Blit Shader"),
52            include_str!("shaders/blit.wgsl"),
53        );
54
55        // Create sampler
56        let sampler = context.device.create_sampler(&wgpu::SamplerDescriptor {
57            label: Some("Blit Sampler"),
58            address_mode_u: wgpu::AddressMode::ClampToEdge,
59            address_mode_v: wgpu::AddressMode::ClampToEdge,
60            address_mode_w: wgpu::AddressMode::ClampToEdge,
61            mag_filter: options.filter_mode,
62            min_filter: options.filter_mode,
63            mipmap_filter: wgpu::FilterMode::Nearest,
64            ..Default::default()
65        });
66
67        // Create bind group layout
68        let bind_group_layout =
69            context
70                .device
71                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
72                    label: Some("Blit Bind Group Layout"),
73                    entries: &[
74                        wgpu::BindGroupLayoutEntry {
75                            binding: 0,
76                            visibility: wgpu::ShaderStages::FRAGMENT,
77                            ty: wgpu::BindingType::Texture {
78                                multisampled: false,
79                                view_dimension: wgpu::TextureViewDimension::D2,
80                                sample_type: wgpu::TextureSampleType::Float { filterable: true },
81                            },
82                            count: None,
83                        },
84                        wgpu::BindGroupLayoutEntry {
85                            binding: 1,
86                            visibility: wgpu::ShaderStages::FRAGMENT,
87                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
88                            count: None,
89                        },
90                    ],
91                });
92
93        // Create pipeline layout
94        let pipeline_layout =
95            context
96                .device
97                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
98                    label: Some("Blit Pipeline Layout"),
99                    bind_group_layouts: &[&bind_group_layout],
100                    push_constant_ranges: &[],
101                });
102
103        // Create pipeline
104        let pipeline = context
105            .device
106            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
107                label: Some("Blit Pipeline"),
108                layout: Some(&pipeline_layout),
109                vertex: wgpu::VertexState {
110                    module: &shader,
111                    entry_point: Some("vs_main"),
112                    buffers: &[wgpu::VertexBufferLayout {
113                        array_stride: 16,
114                        step_mode: wgpu::VertexStepMode::Vertex,
115                        attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
116                    }],
117                    compilation_options: wgpu::PipelineCompilationOptions::default(),
118                },
119                fragment: Some(wgpu::FragmentState {
120                    module: &shader,
121                    entry_point: Some("fs_main"),
122                    targets: &[Some(wgpu::ColorTargetState {
123                        format: target_format,
124                        blend: options.blend_state,
125                        write_mask: wgpu::ColorWrites::ALL,
126                    })],
127                    compilation_options: wgpu::PipelineCompilationOptions::default(),
128                }),
129                primitive: wgpu::PrimitiveState {
130                    topology: wgpu::PrimitiveTopology::TriangleList,
131                    strip_index_format: None,
132                    front_face: wgpu::FrontFace::Ccw,
133                    cull_mode: None,
134                    polygon_mode: wgpu::PolygonMode::Fill,
135                    unclipped_depth: false,
136                    conservative: false,
137                },
138                depth_stencil: None,
139                multisample: wgpu::MultisampleState {
140                    count: 1,
141                    mask: !0,
142                    alpha_to_coverage_enabled: false,
143                },
144                multiview: None,
145                cache: None,
146            });
147
148        // Create fullscreen quad vertex buffer
149        #[rustfmt::skip]
150        let vertices: [f32; 24] = [
151            // Position (clip space)  UV
152            -1.0, -1.0,               0.0, 1.0,
153             1.0, -1.0,               1.0, 1.0,
154             1.0,  1.0,               1.0, 0.0,
155            -1.0, -1.0,               0.0, 1.0,
156             1.0,  1.0,               1.0, 0.0,
157            -1.0,  1.0,               0.0, 0.0,
158        ];
159
160        let vertex_buffer = renderer.create_vertex_buffer(Some("Blit Vertex Buffer"), &vertices);
161
162        Self {
163            pipeline,
164            bind_group_layout,
165            sampler,
166            vertex_buffer,
167            context,
168        }
169    }
170
171    /// Create a bind group for a texture.
172    ///
173    /// You can cache this bind group if you're blitting the same texture repeatedly.
174    pub fn create_bind_group(&self, texture_view: &wgpu::TextureView) -> wgpu::BindGroup {
175        self.context
176            .device
177            .create_bind_group(&wgpu::BindGroupDescriptor {
178                label: Some("Blit Bind Group"),
179                layout: &self.bind_group_layout,
180                entries: &[
181                    wgpu::BindGroupEntry {
182                        binding: 0,
183                        resource: wgpu::BindingResource::TextureView(texture_view),
184                    },
185                    wgpu::BindGroupEntry {
186                        binding: 1,
187                        resource: wgpu::BindingResource::Sampler(&self.sampler),
188                    },
189                ],
190            })
191    }
192
193    /// Blit a texture to the render target as a fullscreen quad.
194    ///
195    /// # Arguments
196    ///
197    /// * `render_pass` - The render pass to draw to
198    /// * `texture_view` - The texture to blit
199    ///
200    /// Note: This creates a new bind group each call. For better performance
201    /// with frequently-blitted textures, use `create_bind_group` and `blit_with_bind_group`.
202    pub fn blit(&self, render_pass: &mut wgpu::RenderPass, texture_view: &wgpu::TextureView) {
203        let bind_group = self.create_bind_group(texture_view);
204        self.blit_with_bind_group(render_pass, &bind_group);
205    }
206
207    /// Blit using a pre-created bind group.
208    ///
209    /// More efficient than `blit` when the same texture is blitted multiple times.
210    pub fn blit_with_bind_group(
211        &self,
212        render_pass: &mut wgpu::RenderPass,
213        bind_group: &wgpu::BindGroup,
214    ) {
215        render_pass.set_pipeline(&self.pipeline);
216        render_pass.set_bind_group(0, bind_group, &[]);
217        render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
218        render_pass.draw(0..6, 0..1);
219    }
220
221    /// Get the bind group layout for custom pipelines.
222    pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
223        &self.bind_group_layout
224    }
225}
226
227/// Options for configuring the blit renderer.
228#[derive(Debug, Clone)]
229pub struct BlitOptions {
230    /// Filter mode for texture sampling (Linear or Nearest)
231    pub filter_mode: wgpu::FilterMode,
232    /// Blend state for the blit operation
233    pub blend_state: Option<wgpu::BlendState>,
234}
235
236impl Default for BlitOptions {
237    fn default() -> Self {
238        Self {
239            filter_mode: wgpu::FilterMode::Linear,
240            blend_state: Some(wgpu::BlendState::REPLACE),
241        }
242    }
243}
244
245impl BlitOptions {
246    /// Create options for opaque blitting (no blending).
247    pub fn opaque() -> Self {
248        Self {
249            filter_mode: wgpu::FilterMode::Linear,
250            blend_state: Some(wgpu::BlendState::REPLACE),
251        }
252    }
253
254    /// Create options for alpha-blended blitting.
255    pub fn alpha_blend() -> Self {
256        Self {
257            filter_mode: wgpu::FilterMode::Linear,
258            blend_state: Some(wgpu::BlendState::ALPHA_BLENDING),
259        }
260    }
261
262    /// Create options for nearest-neighbor filtering (pixel art).
263    pub fn nearest() -> Self {
264        Self {
265            filter_mode: wgpu::FilterMode::Nearest,
266            blend_state: Some(wgpu::BlendState::REPLACE),
267        }
268    }
269
270    /// Set the filter mode.
271    pub fn with_filter(mut self, filter: wgpu::FilterMode) -> Self {
272        self.filter_mode = filter;
273        self
274    }
275
276    /// Set the blend state.
277    pub fn with_blend(mut self, blend: Option<wgpu::BlendState>) -> Self {
278        self.blend_state = blend;
279        self
280    }
281}
282
283/// Helper to upload texture data from CPU to GPU.
284///
285/// Useful for video frame upload or dynamic texture updates.
286pub struct TextureUploader {
287    texture: wgpu::Texture,
288    view: wgpu::TextureView,
289    width: u32,
290    height: u32,
291    format: wgpu::TextureFormat,
292}
293
294impl TextureUploader {
295    /// Create a new texture uploader with the specified dimensions.
296    ///
297    /// # Arguments
298    ///
299    /// * `context` - The graphics context
300    /// * `width` - Texture width in pixels
301    /// * `height` - Texture height in pixels
302    /// * `format` - Texture format (e.g., Rgba8UnormSrgb for standard images)
303    pub fn new(
304        context: &GraphicsContext,
305        width: u32,
306        height: u32,
307        format: wgpu::TextureFormat,
308    ) -> Self {
309        let texture = context.device.create_texture(&wgpu::TextureDescriptor {
310            label: Some("Uploadable Texture"),
311            size: wgpu::Extent3d {
312                width,
313                height,
314                depth_or_array_layers: 1,
315            },
316            mip_level_count: 1,
317            sample_count: 1,
318            dimension: wgpu::TextureDimension::D2,
319            format,
320            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
321            view_formats: &[],
322        });
323
324        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
325
326        Self {
327            texture,
328            view,
329            width,
330            height,
331            format,
332        }
333    }
334
335    /// Upload pixel data to the texture.
336    ///
337    /// # Arguments
338    ///
339    /// * `context` - The graphics context
340    /// * `data` - Raw pixel data (must match texture format and dimensions)
341    pub fn upload(&self, context: &GraphicsContext, data: &[u8]) {
342        let bytes_per_pixel = self.format.block_copy_size(None).unwrap_or(4);
343        let bytes_per_row = self.width * bytes_per_pixel;
344
345        context.queue.write_texture(
346            wgpu::TexelCopyTextureInfo {
347                texture: &self.texture,
348                mip_level: 0,
349                origin: wgpu::Origin3d::ZERO,
350                aspect: wgpu::TextureAspect::All,
351            },
352            data,
353            wgpu::TexelCopyBufferLayout {
354                offset: 0,
355                bytes_per_row: Some(bytes_per_row),
356                rows_per_image: Some(self.height),
357            },
358            wgpu::Extent3d {
359                width: self.width,
360                height: self.height,
361                depth_or_array_layers: 1,
362            },
363        );
364    }
365
366    /// Upload a subregion of the texture.
367    ///
368    /// # Arguments
369    ///
370    /// * `context` - The graphics context
371    /// * `data` - Raw pixel data for the region
372    /// * `x`, `y` - Top-left corner of the region
373    /// * `width`, `height` - Dimensions of the region
374    pub fn upload_region(
375        &self,
376        context: &GraphicsContext,
377        data: &[u8],
378        x: u32,
379        y: u32,
380        width: u32,
381        height: u32,
382    ) {
383        let bytes_per_pixel = self.format.block_copy_size(None).unwrap_or(4);
384        let bytes_per_row = width * bytes_per_pixel;
385
386        context.queue.write_texture(
387            wgpu::TexelCopyTextureInfo {
388                texture: &self.texture,
389                mip_level: 0,
390                origin: wgpu::Origin3d { x, y, z: 0 },
391                aspect: wgpu::TextureAspect::All,
392            },
393            data,
394            wgpu::TexelCopyBufferLayout {
395                offset: 0,
396                bytes_per_row: Some(bytes_per_row),
397                rows_per_image: Some(height),
398            },
399            wgpu::Extent3d {
400                width,
401                height,
402                depth_or_array_layers: 1,
403            },
404        );
405    }
406
407    /// Resize the texture (creates a new texture internally).
408    pub fn resize(&mut self, context: &GraphicsContext, width: u32, height: u32) {
409        if self.width == width && self.height == height {
410            return;
411        }
412
413        *self = Self::new(context, width, height, self.format);
414    }
415
416    /// Get the texture view for rendering.
417    pub fn view(&self) -> &wgpu::TextureView {
418        &self.view
419    }
420
421    /// Get the underlying texture.
422    pub fn texture(&self) -> &wgpu::Texture {
423        &self.texture
424    }
425
426    /// Get the texture dimensions.
427    pub fn size(&self) -> (u32, u32) {
428        (self.width, self.height)
429    }
430
431    /// Get the texture format.
432    pub fn format(&self) -> wgpu::TextureFormat {
433        self.format
434    }
435}