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