1use 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
27const 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
77struct ImageBuffer {
79 width: u32,
80 height: u32,
81 pixels: Vec<u8>, }
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 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 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 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 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 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 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 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 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 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 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 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 let mut image_buffer = ImageBuffer::new(256, 256);
355 image_buffer.clear(30, 30, 40, 255);
356
357 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 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, min_filter: wgpu::FilterMode::Nearest,
386 mipmap_filter: wgpu::FilterMode::Nearest,
387 ..Default::default()
388 });
389
390 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 self.image_buffer.clear(30, 30, 40, 255);
437
438 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 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 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 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 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; };
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 }
524}
525
526fn 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}