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