Skip to main content

image_blitting/
image_blitting.rs

1//! Example demonstrating image blitting (CPU-side pixel manipulation and GPU upload).
2//!
3//! This example shows how to:
4//! - Create textures in memory
5//! - Manipulate pixels on the CPU
6//! - Upload texture data to the GPU
7//! - Render textured quads
8//!
9//! This pattern is useful for:
10//! - Procedural texture generation
11//! - Dynamic image manipulation
12//! - Software rendering to texture
13//! - Animated sprites/textures
14
15use astrelis_core::logging;
16use astrelis_render::{Color, GraphicsContext, RenderWindow, RenderWindowBuilder};
17use astrelis_winit::{
18    WindowId,
19    app::run_app,
20    window::{Window, WindowDescriptor, WinitPhysicalSize},
21};
22use std::collections::HashMap;
23use std::sync::Arc;
24use std::time::Instant;
25use wgpu::util::DeviceExt;
26
27/// WGSL shader for rendering textured quads
28const SHADER: &str = r#"
29struct Uniforms {
30    mvp: mat4x4<f32>,
31    tint: vec4<f32>,
32}
33
34@group(0) @binding(0) var<uniform> uniforms: Uniforms;
35@group(0) @binding(1) var tex: texture_2d<f32>;
36@group(0) @binding(2) var tex_sampler: sampler;
37
38struct VertexInput {
39    @location(0) position: vec2<f32>,
40    @location(1) uv: vec2<f32>,
41}
42
43struct VertexOutput {
44    @builtin(position) clip_position: vec4<f32>,
45    @location(0) uv: vec2<f32>,
46}
47
48@vertex
49fn vs_main(in: VertexInput) -> VertexOutput {
50    var out: VertexOutput;
51    out.clip_position = uniforms.mvp * vec4<f32>(in.position, 0.0, 1.0);
52    out.uv = in.uv;
53    return out;
54}
55
56@fragment
57fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
58    let tex_color = textureSample(tex, tex_sampler, in.uv);
59    return tex_color * uniforms.tint;
60}
61"#;
62
63#[repr(C)]
64#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
65struct Vertex {
66    position: [f32; 2],
67    uv: [f32; 2],
68}
69
70#[repr(C)]
71#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
72struct Uniforms {
73    mvp: [[f32; 4]; 4],
74    tint: [f32; 4],
75}
76
77/// A CPU-side image buffer that can be blitted to GPU.
78struct ImageBuffer {
79    width: u32,
80    height: u32,
81    pixels: Vec<u8>, // RGBA8
82}
83
84impl ImageBuffer {
85    fn new(width: u32, height: u32) -> Self {
86        Self {
87            width,
88            height,
89            pixels: vec![0; (width * height * 4) as usize],
90        }
91    }
92
93    /// Clear to a solid color.
94    fn clear(&mut self, r: u8, g: u8, b: u8, a: u8) {
95        for chunk in self.pixels.chunks_exact_mut(4) {
96            chunk[0] = r;
97            chunk[1] = g;
98            chunk[2] = b;
99            chunk[3] = a;
100        }
101    }
102
103    /// Set a pixel at (x, y).
104    fn set_pixel(&mut self, x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) {
105        if x < self.width && y < self.height {
106            let idx = ((y * self.width + x) * 4) as usize;
107            self.pixels[idx] = r;
108            self.pixels[idx + 1] = g;
109            self.pixels[idx + 2] = b;
110            self.pixels[idx + 3] = a;
111        }
112    }
113
114    /// Draw a filled rectangle.
115    fn fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: [u8; 4]) {
116        for dy in 0..h {
117            for dx in 0..w {
118                self.set_pixel(x + dx, y + dy, color[0], color[1], color[2], color[3]);
119            }
120        }
121    }
122
123    /// Draw a circle using midpoint algorithm.
124    fn fill_circle(&mut self, cx: i32, cy: i32, radius: i32, color: [u8; 4]) {
125        for y in (cy - radius)..=(cy + radius) {
126            for x in (cx - radius)..=(cx + radius) {
127                let dx = x - cx;
128                let dy = y - cy;
129                if dx * dx + dy * dy <= radius * radius && x >= 0 && y >= 0 {
130                    self.set_pixel(x as u32, y as u32, color[0], color[1], color[2], color[3]);
131                }
132            }
133        }
134    }
135
136    /// Draw a horizontal gradient.
137    fn gradient_h(&mut self, y: u32, h: u32, from: [u8; 3], to: [u8; 3]) {
138        for dy in 0..h {
139            for x in 0..self.width {
140                let t = x as f32 / self.width as f32;
141                let r = (from[0] as f32 * (1.0 - t) + to[0] as f32 * t) as u8;
142                let g = (from[1] as f32 * (1.0 - t) + to[1] as f32 * t) as u8;
143                let b = (from[2] as f32 * (1.0 - t) + to[2] as f32 * t) as u8;
144                self.set_pixel(x, y + dy, r, g, b, 255);
145            }
146        }
147    }
148}
149
150struct App {
151    context: Arc<GraphicsContext>,
152    windows: HashMap<WindowId, RenderWindow>,
153    pipeline: wgpu::RenderPipeline,
154    _bind_group_layout: wgpu::BindGroupLayout,
155    vertex_buffer: wgpu::Buffer,
156    texture: wgpu::Texture,
157    bind_group: wgpu::BindGroup,
158    _uniform_buffer: wgpu::Buffer,
159    image_buffer: ImageBuffer,
160    start_time: Instant,
161}
162
163fn main() {
164    logging::init();
165
166    run_app(|ctx| {
167        let graphics_ctx =
168            GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
169        let mut windows = HashMap::new();
170
171        let scale = Window::platform_dpi() as f32;
172        let window = ctx
173            .create_window(WindowDescriptor {
174                title: "Image Blitting Example".to_string(),
175                size: Some(WinitPhysicalSize::new(800.0 * scale, 600.0 * scale)),
176                ..Default::default()
177            })
178            .expect("Failed to create window");
179
180        let renderable_window = RenderWindowBuilder::new()
181            .color_format(wgpu::TextureFormat::Bgra8UnormSrgb)
182            .with_depth_default()
183            .build(window, graphics_ctx.clone())
184            .expect("Failed to create render window");
185
186        let window_id = renderable_window.id();
187        windows.insert(window_id, renderable_window);
188
189        // Create shader module
190        let shader = graphics_ctx
191            .device()
192            .create_shader_module(wgpu::ShaderModuleDescriptor {
193                label: Some("Blit Shader"),
194                source: wgpu::ShaderSource::Wgsl(SHADER.into()),
195            });
196
197        // Create bind group layout
198        let bind_group_layout =
199            graphics_ctx
200                .device()
201                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
202                    label: Some("Blit Bind Group Layout"),
203                    entries: &[
204                        wgpu::BindGroupLayoutEntry {
205                            binding: 0,
206                            visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
207                            ty: wgpu::BindingType::Buffer {
208                                ty: wgpu::BufferBindingType::Uniform,
209                                has_dynamic_offset: false,
210                                min_binding_size: None,
211                            },
212                            count: None,
213                        },
214                        wgpu::BindGroupLayoutEntry {
215                            binding: 1,
216                            visibility: wgpu::ShaderStages::FRAGMENT,
217                            ty: wgpu::BindingType::Texture {
218                                sample_type: wgpu::TextureSampleType::Float { filterable: true },
219                                view_dimension: wgpu::TextureViewDimension::D2,
220                                multisampled: false,
221                            },
222                            count: None,
223                        },
224                        wgpu::BindGroupLayoutEntry {
225                            binding: 2,
226                            visibility: wgpu::ShaderStages::FRAGMENT,
227                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
228                            count: None,
229                        },
230                    ],
231                });
232
233        // Create pipeline layout
234        let pipeline_layout =
235            graphics_ctx
236                .device()
237                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
238                    label: Some("Blit Pipeline Layout"),
239                    bind_group_layouts: &[&bind_group_layout],
240                    push_constant_ranges: &[],
241                });
242
243        // Create render pipeline
244        let pipeline =
245            graphics_ctx
246                .device()
247                .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
248                    label: Some("Blit Pipeline"),
249                    layout: Some(&pipeline_layout),
250                    vertex: wgpu::VertexState {
251                        module: &shader,
252                        entry_point: Some("vs_main"),
253                        buffers: &[wgpu::VertexBufferLayout {
254                            array_stride: std::mem::size_of::<Vertex>() as u64,
255                            step_mode: wgpu::VertexStepMode::Vertex,
256                            attributes: &[
257                                wgpu::VertexAttribute {
258                                    offset: 0,
259                                    shader_location: 0,
260                                    format: wgpu::VertexFormat::Float32x2,
261                                },
262                                wgpu::VertexAttribute {
263                                    offset: 8,
264                                    shader_location: 1,
265                                    format: wgpu::VertexFormat::Float32x2,
266                                },
267                            ],
268                        }],
269                        compilation_options: wgpu::PipelineCompilationOptions::default(),
270                    },
271                    fragment: Some(wgpu::FragmentState {
272                        module: &shader,
273                        entry_point: Some("fs_main"),
274                        targets: &[Some(wgpu::ColorTargetState {
275                            format: wgpu::TextureFormat::Bgra8UnormSrgb,
276                            blend: Some(wgpu::BlendState::ALPHA_BLENDING),
277                            write_mask: wgpu::ColorWrites::ALL,
278                        })],
279                        compilation_options: wgpu::PipelineCompilationOptions::default(),
280                    }),
281                    primitive: wgpu::PrimitiveState {
282                        topology: wgpu::PrimitiveTopology::TriangleList,
283                        strip_index_format: None,
284                        front_face: wgpu::FrontFace::Ccw,
285                        cull_mode: None,
286                        polygon_mode: wgpu::PolygonMode::Fill,
287                        unclipped_depth: false,
288                        conservative: false,
289                    },
290                    depth_stencil: None,
291                    multisample: wgpu::MultisampleState::default(),
292                    multiview: None,
293                    cache: None,
294                });
295
296        // Create vertex buffer for a fullscreen quad
297        let vertices = [
298            Vertex {
299                position: [-0.8, -0.8],
300                uv: [0.0, 1.0],
301            },
302            Vertex {
303                position: [0.8, -0.8],
304                uv: [1.0, 1.0],
305            },
306            Vertex {
307                position: [0.8, 0.8],
308                uv: [1.0, 0.0],
309            },
310            Vertex {
311                position: [-0.8, -0.8],
312                uv: [0.0, 1.0],
313            },
314            Vertex {
315                position: [0.8, 0.8],
316                uv: [1.0, 0.0],
317            },
318            Vertex {
319                position: [-0.8, 0.8],
320                uv: [0.0, 0.0],
321            },
322        ];
323        let vertex_buffer =
324            graphics_ctx
325                .device()
326                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
327                    label: Some("Vertex Buffer"),
328                    contents: bytemuck::cast_slice(&vertices),
329                    usage: wgpu::BufferUsages::VERTEX,
330                });
331
332        // Create uniform buffer
333        let uniforms = Uniforms {
334            mvp: [
335                [1.0, 0.0, 0.0, 0.0],
336                [0.0, 1.0, 0.0, 0.0],
337                [0.0, 0.0, 1.0, 0.0],
338                [0.0, 0.0, 0.0, 1.0],
339            ],
340            tint: [1.0, 1.0, 1.0, 1.0],
341        };
342        let uniform_buffer =
343            graphics_ctx
344                .device()
345                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
346                    label: Some("Uniform Buffer"),
347                    contents: bytemuck::cast_slice(&[uniforms]),
348                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
349                });
350
351        // Create CPU-side image buffer
352        let mut image_buffer = ImageBuffer::new(256, 256);
353        image_buffer.clear(30, 30, 40, 255);
354
355        // Create GPU texture
356        let texture = graphics_ctx
357            .device()
358            .create_texture(&wgpu::TextureDescriptor {
359                label: Some("Blit Texture"),
360                size: wgpu::Extent3d {
361                    width: 256,
362                    height: 256,
363                    depth_or_array_layers: 1,
364                },
365                mip_level_count: 1,
366                sample_count: 1,
367                dimension: wgpu::TextureDimension::D2,
368                format: wgpu::TextureFormat::Rgba8UnormSrgb,
369                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
370                view_formats: &[],
371            });
372
373        // Create texture view and sampler
374        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
375        let sampler = graphics_ctx
376            .device()
377            .create_sampler(&wgpu::SamplerDescriptor {
378                label: Some("Blit Sampler"),
379                address_mode_u: wgpu::AddressMode::ClampToEdge,
380                address_mode_v: wgpu::AddressMode::ClampToEdge,
381                address_mode_w: wgpu::AddressMode::ClampToEdge,
382                mag_filter: wgpu::FilterMode::Nearest, // Pixel-perfect rendering
383                min_filter: wgpu::FilterMode::Nearest,
384                mipmap_filter: wgpu::FilterMode::Nearest,
385                ..Default::default()
386            });
387
388        // Create bind group
389        let bind_group = graphics_ctx
390            .device()
391            .create_bind_group(&wgpu::BindGroupDescriptor {
392                label: Some("Blit Bind Group"),
393                layout: &bind_group_layout,
394                entries: &[
395                    wgpu::BindGroupEntry {
396                        binding: 0,
397                        resource: uniform_buffer.as_entire_binding(),
398                    },
399                    wgpu::BindGroupEntry {
400                        binding: 1,
401                        resource: wgpu::BindingResource::TextureView(&texture_view),
402                    },
403                    wgpu::BindGroupEntry {
404                        binding: 2,
405                        resource: wgpu::BindingResource::Sampler(&sampler),
406                    },
407                ],
408            });
409
410        Box::new(App {
411            context: graphics_ctx,
412            windows,
413            pipeline,
414            _bind_group_layout: bind_group_layout,
415            vertex_buffer,
416            texture,
417            bind_group,
418            _uniform_buffer: uniform_buffer,
419            image_buffer,
420            start_time: Instant::now(),
421        })
422    });
423}
424
425impl astrelis_winit::app::App for App {
426    fn update(
427        &mut self,
428        _ctx: &mut astrelis_winit::app::AppCtx,
429        _time: &astrelis_winit::FrameTime,
430    ) {
431        let time = self.start_time.elapsed().as_secs_f32();
432
433        // Animate the CPU-side image buffer
434        self.image_buffer.clear(30, 30, 40, 255);
435
436        // Draw animated gradient background
437        let phase = (time * 0.5).sin() * 0.5 + 0.5;
438        let r1 = (50.0 + phase * 50.0) as u8;
439        let b1 = (80.0 + (1.0 - phase) * 50.0) as u8;
440        self.image_buffer
441            .gradient_h(0, 256, [r1, 40, b1], [40, r1, b1]);
442
443        // Draw bouncing circles
444        for i in 0..5 {
445            let offset = i as f32 * 0.4;
446            let x = 128.0 + (time * 2.0 + offset).sin() * 80.0;
447            let y = 128.0 + (time * 3.0 + offset).cos() * 80.0;
448            let hue = (time * 0.5 + offset) % 1.0;
449            let (r, g, b) = hsv_to_rgb(hue, 0.8, 1.0);
450            self.image_buffer
451                .fill_circle(x as i32, y as i32, 20, [r, g, b, 255]);
452        }
453
454        // Draw animated rectangles
455        for i in 0..3 {
456            let x = ((time * (1.0 + i as f32 * 0.3)).sin() * 100.0 + 128.0) as u32;
457            let y = 20 + i * 80;
458            let w = 30 + (time.sin() * 10.0) as u32;
459            let h = 20;
460            self.image_buffer
461                .fill_rect(x.saturating_sub(w / 2), y, w, h, [255, 255, 255, 200]);
462        }
463
464        // Upload to GPU (this is the "blit" operation)
465        self.context.queue().write_texture(
466            wgpu::TexelCopyTextureInfo {
467                texture: &self.texture,
468                mip_level: 0,
469                origin: wgpu::Origin3d::ZERO,
470                aspect: wgpu::TextureAspect::All,
471            },
472            &self.image_buffer.pixels,
473            wgpu::TexelCopyBufferLayout {
474                offset: 0,
475                bytes_per_row: Some(self.image_buffer.width * 4),
476                rows_per_image: Some(self.image_buffer.height),
477            },
478            wgpu::Extent3d {
479                width: self.image_buffer.width,
480                height: self.image_buffer.height,
481                depth_or_array_layers: 1,
482            },
483        );
484    }
485
486    fn render(
487        &mut self,
488        _ctx: &mut astrelis_winit::app::AppCtx,
489        window_id: WindowId,
490        events: &mut astrelis_winit::event::EventBatch,
491    ) {
492        let Some(window) = self.windows.get_mut(&window_id) else {
493            return;
494        };
495
496        // Handle resize
497        events.dispatch(|event| {
498            if let astrelis_winit::event::Event::WindowResized(size) = event {
499                window.resized(*size);
500                astrelis_winit::event::HandleStatus::consumed()
501            } else {
502                astrelis_winit::event::HandleStatus::ignored()
503            }
504        });
505
506        let Some(frame) = window.begin_frame() else {
507            return; // Surface not available
508        };
509
510        {
511            let mut pass = frame
512                .render_pass()
513                .clear_color(Color::rgb(0.05, 0.05, 0.08))
514                .label("image_blitting_pass")
515                .build();
516            pass.set_pipeline(&self.pipeline);
517            pass.set_bind_group(0, &self.bind_group, &[]);
518            pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
519            pass.draw(0..6, 0..1);
520        }
521        // Frame auto-submits on drop
522    }
523}
524
525/// Convert HSV to RGB (h in [0,1], s in [0,1], v in [0,1])
526fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
527    let c = v * s;
528    let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
529    let m = v - c;
530
531    let (r, g, b) = match (h * 6.0) as i32 {
532        0 => (c, x, 0.0),
533        1 => (x, c, 0.0),
534        2 => (0.0, c, x),
535        3 => (0.0, x, c),
536        4 => (x, 0.0, c),
537        _ => (c, 0.0, x),
538    };
539
540    (
541        ((r + m) * 255.0) as u8,
542        ((g + m) * 255.0) as u8,
543        ((b + m) * 255.0) as u8,
544    )
545}