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