1use 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
29const 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
79struct ImageBuffer {
81 width: u32,
82 height: u32,
83 pixels: Vec<u8>, }
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 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 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 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 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 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 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 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 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 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 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 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 let mut image_buffer = ImageBuffer::new(256, 256);
324 image_buffer.clear(30, 30, 40, 255);
325
326 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 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, min_filter: wgpu::FilterMode::Nearest,
351 mipmap_filter: wgpu::FilterMode::Nearest,
352 ..Default::default()
353 });
354
355 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 self.image_buffer.clear(30, 30, 40, 255);
396
397 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 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 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 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 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 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
483fn 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}