Skip to main content

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