use bytemuck::bytes_of; use std::collections::BTreeMap; use std::io::BufReader; use std::sync::Arc; use std::time::Instant; use tracing::info; use wgpu::util::DeviceExt; use wgpu::{ Backends, FragmentState, IndexFormat, Instance, InstanceDescriptor, PipelineCompilationOptions, }; use winit::application::ApplicationHandler; use winit::event::{ElementState, MouseButton}; use winit::event_loop::{ActiveEventLoop, EventLoop}; #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; use winit::window::{Window, WindowAttributes, WindowId}; use zlog::LogLevel; use zlog::config::LoggerConfig; pub mod camera; pub mod model; pub mod texture; struct WindowContext<'window> { window: Arc, renderer: WgpuRenderer<'window>, } impl std::ops::Deref for WindowContext<'_> { type Target = winit::window::Window; fn deref(&self) -> &Self::Target { self.window.as_ref() } } struct WgpuRenderer<'surface> { device: wgpu::Device, queue: wgpu::Queue, surface: wgpu::Surface<'surface>, surface_config: wgpu::SurfaceConfiguration, render_pipeline: wgpu::RenderPipeline, depth_texture: wgpu::Texture, depth_texture_view: wgpu::TextureView, camera: camera::Camera, camera_uniform: camera::CameraUniform, camera_buffer: wgpu::Buffer, camera_bind_group: wgpu::BindGroup, pumpkin: model::Model, delta: f32, last_frame_time: Instant, default_texture: wgpu::BindGroup, } impl WgpuRenderer<'_> { pub fn draw(&mut self) { let surface_texture = self.surface.get_current_texture().unwrap(); let view = surface_texture.texture.create_view(&Default::default()); let mut encoder = self.device .create_command_encoder(&wgpu::wgt::CommandEncoderDescriptor { label: Some("Render encoder"), }); { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { view: &self.depth_texture_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store, }), stencil_ops: None, }), occlusion_query_set: None, timestamp_writes: None, }); rpass.set_pipeline(&self.render_pipeline); rpass.set_bind_group(0, &self.camera_bind_group, &[]); for mesh in &self.pumpkin.meshes { let bind_group = mesh .material .and_then(|i| self.pumpkin.materials.get(i)) .map(|m| &m.bind_group) .unwrap_or(&self.default_texture); rpass.set_bind_group(1, bind_group, &[]); rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); rpass.set_index_buffer(mesh.index_buffer.slice(..), IndexFormat::Uint32); rpass.draw_indexed(0..mesh.num_elements, 0, 0..1); } } self.queue.submit(Some(encoder.finish())); let delta_time = std::time::Instant::now() - self.last_frame_time; self.delta = delta_time.as_secs_f32(); info!("{}", self.delta); surface_texture.present(); self.last_frame_time = std::time::Instant::now(); } } struct App<'window> { state: WgpuState, windows: BTreeMap>, } impl App<'_> { fn new() -> Self { Self { state: WgpuState::new(), windows: BTreeMap::new(), } } pub fn spawn_window(&mut self, event_loop: &ActiveEventLoop) { let attr = WindowAttributes::default() .with_title("Zenyx - SubWindow") .with_min_inner_size(winit::dpi::LogicalSize::new(1, 1)); let window = event_loop.create_window(attr).unwrap(); let window = Arc::new(window); let renderer = self.state.create_renderer(window.clone()); let window_ctx = WindowContext { renderer: smol::block_on(renderer), window: window.clone(), }; let window_id = window.id(); self.windows.insert(window_id, window_ctx); } } struct WgpuState { instance: wgpu::Instance, } static _ICON: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Badge.png")); static _PUMPKIN: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/Pumpkin.obj")); impl WgpuState { fn new() -> Self { let backends = Backends::PRIMARY; let instance_descriptor = InstanceDescriptor { backends, ..Default::default() }; let instance = Instance::new(&instance_descriptor); Self { instance } } async fn create_renderer<'surface>(&self, window: Arc) -> WgpuRenderer<'surface> { let surface = self.instance.create_surface(window.clone()).unwrap(); let adapter = self .instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::default(), compatible_surface: Some(&surface), ..Default::default() }) .await .unwrap(); let device_descriptor = wgpu::DeviceDescriptor::default(); let (device, queue) = adapter.request_device(&device_descriptor).await.unwrap(); let size = window.inner_size(); let width = size.width.max(1); let height = size.height.max(1); let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, // This should match the filterable field of the // corresponding Texture entry above. ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], label: Some("texture_bind_group_layout"), }); let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }], label: Some("Camera Bind group layout"), }); #[repr(align(4))] struct ShaderCode([u8; N]); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Pipeline Layout"), bind_group_layouts: &[&camera_bind_group_layout, &texture_bind_group_layout], push_constant_ranges: &[], }); let vert_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Vertex Shader"), source: unsafe { static SHADER_CODE: &[u8] = &ShaderCode(*include_bytes!(concat!(env!("OUT_DIR"), "/vert.spv"))).0; // assert!(bytes.len() % 4 == 0); let shader = SHADER_CODE.align_to::().1; wgpu::ShaderSource::SpirV(std::borrow::Cow::Borrowed(shader)) }, }); let frag_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Fragment Shader"), source: unsafe { static SHADER_CODE: &[u8] = &ShaderCode(*include_bytes!(concat!(env!("OUT_DIR"), "/frag.spv"))).0; debug_assert!(SHADER_CODE.len() % 4 == 0); let shader = SHADER_CODE.align_to::().1; wgpu::ShaderSource::SpirV(std::borrow::Cow::Borrowed(shader)) }, }); let var_name = [Some(wgpu::ColorTargetState { format: surface.get_capabilities(&adapter).formats[0], blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })]; let pipeline_descriptor = wgpu::RenderPipelineDescriptor { label: Some("Main pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &vert_shader, entry_point: Some("main"), buffers: &[model::Vertex::desc()], compilation_options: Default::default(), }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, cull_mode: Some(wgpu::Face::Back), polygon_mode: wgpu::PolygonMode::Fill, unclipped_depth: false, conservative: false, }, depth_stencil: Some(wgpu::DepthStencilState { format: wgpu::TextureFormat::Depth32Float, depth_write_enabled: true, depth_compare: wgpu::CompareFunction::Less, stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false, }, fragment: Some(FragmentState { module: &frag_shader, entry_point: Some("main"), compilation_options: PipelineCompilationOptions::default(), targets: &var_name, }), multiview: None, cache: None, }; // todo!(); let surface_caps = surface.get_capabilities(&adapter); let present_mode = if surface_caps .present_modes .contains(&wgpu::PresentMode::Mailbox) { wgpu::PresentMode::Mailbox } else { wgpu::PresentMode::Fifo }; let surface_config = wgpu::SurfaceConfiguration { width, height, format: surface_caps.formats[0], present_mode, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], usage: wgpu::TextureUsages::RENDER_ATTACHMENT, desired_maximum_frame_latency: 3, }; surface.configure(&device, &surface_config); let pumpkin = model::Model::load_obj( &mut BufReader::new(std::fs::File::open("Pumpkin.obj").unwrap()), &device, &queue, &texture_bind_group_layout, ); let checkerboard_img = crate::texture::create_checkerboard(); let checkerboard_texture = texture::Texture::from_image(&device, &queue, &checkerboard_img, Some("checkerboard")) .unwrap(); let checkerboard_sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::Repeat, address_mode_v: wgpu::AddressMode::Repeat, address_mode_w: wgpu::AddressMode::Repeat, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); let default_texture = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("checkerboard-bind-group"), layout: &texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&checkerboard_texture.view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&checkerboard_sampler), }, ], }); let (depth_texture, depth_texture_view) = create_depth_texture(&device, surface_config.width, surface_config.height); let camera = camera::Camera { // position the camera 1 unit up and 2 units back // +z is out of the screen eye: (0.0, 0.0, 2.0).into(), // have it look at the origin target: (0.0, 0.0, 0.0).into(), // which way is "up" up: cgmath::Vector3::unit_y(), aspect: surface_config.width as f32 / surface_config.height as f32, fovy: 45.0, znear: 0.1, zfar: 1000.0, }; let mut camera_uniform = camera::CameraUniform::default(); camera_uniform.update_view_proj(&camera); let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Camera Buffer"), contents: bytemuck::cast_slice(&[camera_uniform]), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &camera_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: camera_buffer.as_entire_binding(), }], label: Some("Camera Bind Group"), }); let render_pipeline = device.create_render_pipeline(&pipeline_descriptor); WgpuRenderer { surface, surface_config, depth_texture, render_pipeline, device, camera, queue, camera_uniform, camera_buffer, camera_bind_group, depth_texture_view, pumpkin, default_texture, delta: 0f32, last_frame_time: Instant::now(), } } } fn create_depth_texture( device: &wgpu::Device, width: u32, height: u32, ) -> (wgpu::Texture, wgpu::TextureView) { let size = wgpu::Extent3d { width, height, depth_or_array_layers: 1, }; let desc = wgpu::TextureDescriptor { label: Some("Depth Texture"), size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Depth32Float, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }; let texture = device.create_texture(&desc); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); (texture, view) } impl ApplicationHandler for App<'_> { fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, window_id: WindowId, event: winit::event::WindowEvent, ) { match event { winit::event::WindowEvent::RedrawRequested => { let window_ctx = self.windows.get_mut(&window_id).unwrap(); window_ctx.renderer.draw() } winit::event::WindowEvent::CloseRequested => { let _ = self.windows.remove(&window_id); if self.windows.is_empty() { event_loop.exit(); } } winit::event::WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left && state == ElementState::Pressed { self.spawn_window(event_loop); } } winit::event::WindowEvent::Resized(size) => { if let Some(window_ctx) = self.windows.get_mut(&window_id) { if size.width == 0 || size.height == 0 { return; } window_ctx .renderer .camera .update_aspect(size.width as f32 / size.height as f32); window_ctx .renderer .camera_uniform .update_view_proj(&window_ctx.renderer.camera); window_ctx.renderer.queue.write_buffer( &window_ctx.renderer.camera_buffer, 0, bytes_of(&window_ctx.renderer.camera_uniform), ); let mut new_config = window_ctx.renderer.surface_config.clone(); new_config.width = size.width; new_config.height = size.height; let (depth_texture, depth_view) = create_depth_texture(&window_ctx.renderer.device, size.width, size.height); window_ctx.renderer.depth_texture = depth_texture; window_ctx.renderer.depth_texture_view = depth_view; window_ctx .renderer .surface .configure(&window_ctx.renderer.device, &new_config) } } _ => (), } } fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { let attr = WindowAttributes::default() .with_title("Zenyx") .with_min_inner_size(winit::dpi::LogicalSize::new(1, 1)); if self.windows.is_empty() { let window = event_loop.create_window(attr).unwrap(); let window = Arc::new(window); let renderer = self.state.create_renderer(window.clone()); let window_ctx = WindowContext { renderer: smol::block_on(renderer), window: window.clone(), }; let window_id = window.id(); self.windows.insert(window_id, window_ctx); } } fn suspended(&mut self, _event_loop: &ActiveEventLoop) { self.windows.clear(); } } pub fn main() -> Result<(), terminator::Terminator> { let config = LoggerConfig::default() .colored_stdout(true) .log_to_stdout(true) .file_include_time(true) .log_to_file(true) .level(LogLevel::Info) .log_path("zenyx.log"); let _logger = zlog::Logger::new(config); #[cfg(not(target_os = "android"))] { _main() } #[cfg(target_os = "android")] { Ok(()) } } pub fn run_app(event_loop: winit::event_loop::EventLoop<()>) -> Result<(), terminator::Terminator> { let mut app = App::new(); event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); event_loop.run_app(&mut app)?; Ok(()) } #[unsafe(no_mangle)] #[cfg(target_os = "android")] extern "C" fn android_main(app: AndroidApp) { use winit::event_loop::EventLoopBuilder; use winit::platform::android::EventLoopBuilderExtAndroid; let event_loop = EventLoopBuilder::default() .with_android_app(app) .build() .unwrap(); run_app(event_loop).unwrap() } fn _main() -> Result<(), terminator::Terminator> { let event_loop = EventLoop::new()?; run_app(event_loop)?; info!("Exiting..."); Ok(()) }