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