1use 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
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, 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 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 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 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 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 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 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 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 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 let mut image_buffer = ImageBuffer::new(256, 256);
353 image_buffer.clear(30, 30, 40, 255);
354
355 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 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, min_filter: wgpu::FilterMode::Nearest,
384 mipmap_filter: wgpu::FilterMode::Nearest,
385 ..Default::default()
386 });
387
388 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 self.image_buffer.clear(30, 30, 40, 255);
435
436 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 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 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 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 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; };
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 }
523}
524
525fn 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}