use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::sync::Arc; use bevy_ecs::schedule::Schedule; use bevy_ecs::world::World; use egui::ViewportId; use tracing::info; use web_time::Instant; use wgpu::{Adapter, Backends, BindGroupDescriptor, BindGroupEntry, BindingResource, Buffer, BufferDescriptor, BufferUsages, Color, ColorTargetState, CommandEncoderDescriptor, Device, DeviceDescriptor, Features, FragmentState, include_wgsl, Instance, InstanceDescriptor, Limits, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, RequestAdapterOptions, ShaderModule, StoreOp, Surface, TextureViewDescriptor, VertexState}; use wgpu::SurfaceConfiguration; use winit::dpi::{LogicalSize, PhysicalSize}; use winit::window::Window; use crate::ecs::{Camera, Position, Scale, SpriteTexture}; use crate::rendering::mipmap::MipGenerator; use crate::rendering::renderer::RenderInitRes::{Initialized, NotReadyYet}; use crate::rendering::texture; use crate::rendering::ui::UiRenderable; pub struct Renderer { pub last_frame_time: Instant, pub surface: Surface<'static>, pub surface_configuration: SurfaceConfiguration, pub device: Device, pub queue: Queue, pub adapter: Adapter, pub sprite_shader_module: ShaderModule, pub sprite_pipeline: RenderPipeline, pub world: World, pub update_schedule: Schedule, pub gui_ctx: egui::Context, pub gui_winit: egui_winit::State, pub gui_renderer: egui_wgpu::Renderer, pub gui_renderable: T, pub textures: HashMap, pub mip_generator: MipGenerator, pub uniform_buffer: Buffer, pub scale_factor: f64, pub window: Arc, pub size: PhysicalSize, pub logical_size: LogicalSize } pub enum RenderInitRes { Initialized(Renderer), NotReadyYet(World, Schedule, T) } impl Debug for RenderInitRes { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Initialized(..) => write!(f, "[initialized renderer]"), NotReadyYet(..) => write!(f, "[pending initialization]") } } } impl Renderer { pub async fn new(window: Arc, world: World, update_schedule: Schedule, gui_renderable: T) -> RenderInitRes { let size = window.inner_size(); if size.width == 0 || size.height == 0 { return NotReadyYet(world, update_schedule, gui_renderable); } // First, create an instance. This is our handle to wgpu, and is the equivalent of navigator.gpu in WebGPU let instance = Instance::new(InstanceDescriptor { backends: Backends::all(), // Select the appropriate backend for the HAL to use. Defined in the platform module, as it's platform-specific ..Default::default() // Other fields aren't relevant here }); // Next, get our render surface let surface = instance.create_surface(window.clone()).unwrap(); // Next, request out adapter let adapter = instance.request_adapter(&RequestAdapterOptions { power_preference: Default::default(), // Don't care force_fallback_adapter: false, // We want a real GPU compatible_surface: Some(&surface), // Find an adapter that is able to render to our window }).await.unwrap(); let (device, queue) = adapter.request_device(&DeviceDescriptor { label: Some("Basic render device"), required_features: Features::default(), required_limits: Limits::downlevel_webgl2_defaults(), memory_hints: Default::default(), }, None).await.unwrap(); let format = surface.get_default_config(&adapter, size.width, size.height).unwrap(); surface.configure(&device, &format); let sprite_shader_module = device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl")); let sprite_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { label: Some("Sprite pipeline"), layout: None, vertex: VertexState { module: &sprite_shader_module, entry_point: Some("vs"), compilation_options: Default::default(), buffers: &[], }, primitive: Default::default(), depth_stencil: None, multisample: Default::default(), fragment: Some(FragmentState { module: &sprite_shader_module, entry_point: Some("fs"), compilation_options: Default::default(), targets: &[Some(ColorTargetState { format: format.format, blend: None, write_mask: Default::default(), })], }), multiview: None, cache: None, }); let gui_context = egui::Context::default(); let gui_winit = egui_winit::State::new( gui_context.clone(), ViewportId::ROOT, &window, Some(window.scale_factor() as f32), None, Some(device.limits().max_texture_dimension_2d as usize) ); let gui_renderer = egui_wgpu::Renderer::new( &device, format.format, None, 1, false ); let uniform_buffer = device.create_buffer(&BufferDescriptor { label: Some("quad uniforms"), size: 16, usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, mapped_at_creation: false, }); Initialized(Self { mip_generator: MipGenerator::new(&device), textures: HashMap::new(), last_frame_time: Instant::now(), surface, device, queue, adapter, sprite_shader_module, sprite_pipeline, world, update_schedule, gui_ctx: gui_context, gui_winit, gui_renderer, gui_renderable, logical_size: LogicalSize::from_physical(size, window.scale_factor()), scale_factor: window.scale_factor(), window, uniform_buffer, size, surface_configuration: format }) } pub fn render(&mut self) { // update the world self.update_schedule.run(&mut self.world); // update the UI let egui_output = self.gui_ctx.run( self.gui_winit.take_egui_input(&self.window), |ctx| { self.gui_renderable.render(ctx, &mut self.world) } ); self.gui_winit.handle_platform_output(&self.window, egui_output.platform_output); let output = self.surface.get_current_texture().unwrap(); let view = output.texture.create_view(&TextureViewDescriptor::default()); let render_pass_descriptor = RenderPassDescriptor { label: Some("basic render pass"), color_attachments: &[Some(RenderPassColorAttachment { view: &view, resolve_target: None, ops: Operations { load: LoadOp::Clear(Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0, }), store: StoreOp::Store, }, })], ..Default::default() }; let mut encoder = self .device .create_command_encoder(&CommandEncoderDescriptor { label: Some("command encoder"), }); let mut sprites_to_render: Vec<(Position, Scale, SpriteTexture)> = vec![]; let mut things_to_render = self.world.query::<(&Position, &Scale, &SpriteTexture)>(); for thing in things_to_render.iter_mut(&mut self.world) { sprites_to_render.push((*thing.0, *thing.1, thing.2.clone())); } let cam = self.world.resource::(); for (pos, scale, tex) in sprites_to_render { let tex = self.textures.entry(tex.texture.clone()).or_insert_with(|| { info!("loading texture {}", &tex.texture); let b: &[u8] = match tex.texture.as_str() { "f" => include_bytes!("../textures/f.png"), "happy-tree" => include_bytes!("../textures/happy-tree.png"), "uv" => include_bytes!("../textures/uv.png"), u => panic!("unknown texture {u}, has it been added in rendering::renderer::::render()?") }; texture::Texture::new( b, &tex.texture, &self.device, &self.queue, &mut self.mip_generator, ) }); // need to calculate width, height, x, and y, using logical size & aspect // calculate "viewport position" w/ the camera let viewport_x = pos.x - cam.x; let viewport_y = pos.y - cam.y; let scaled_w = scale.width * cam.zoom; let scaled_h = scale.height * cam.zoom; let x_screen = viewport_x / self.logical_size.width as f32; let y_screen = viewport_y / self.logical_size.height as f32; let w_screen = scaled_w / self.logical_size.width as f32; let h_screen = scaled_h / self.logical_size.height as f32; let mut uniform_buffer = vec![]; let uniform_buffer_data = [w_screen, h_screen, x_screen, y_screen]; for i in uniform_buffer_data { let mut bytes = i.to_ne_bytes().to_vec(); uniform_buffer.append(&mut bytes); } self.queue .write_buffer(&self.uniform_buffer, 0, &uniform_buffer); let bind_group = self.device.create_bind_group(&BindGroupDescriptor { label: Some("test_bind_group"), layout: &self.sprite_pipeline.get_bind_group_layout(0), entries: &[ BindGroupEntry { binding: 0, resource: BindingResource::Sampler(&tex.sampler), }, BindGroupEntry { binding: 1, resource: BindingResource::TextureView( &tex.texture.create_view(&TextureViewDescriptor::default()), ), }, BindGroupEntry { binding: 2, resource: BindingResource::Buffer( self.uniform_buffer.as_entire_buffer_binding(), ), }, ], }); let mut pass = encoder.begin_render_pass(&render_pass_descriptor); pass.set_pipeline(&self.sprite_pipeline); pass.set_bind_group(0, Some(&bind_group), &[]); pass.draw(0..6, 0..1); } // main game rendering done // next up: egui UI rendering for (id, image_delta) in &egui_output.textures_delta.set { self.gui_renderer.update_texture(&self.device, &self.queue, *id, image_delta); } { let paint_jobs = self.gui_ctx.tessellate(egui_output.shapes, self.scale_factor as f32); let screen_descriptor = egui_wgpu::ScreenDescriptor { size_in_pixels: [self.size.width, self.size.height], pixels_per_point: self.scale_factor as f32 }; self.gui_renderer.update_buffers(&self.device, &self.queue, &mut encoder, &paint_jobs, &screen_descriptor); let render_pass = encoder.begin_render_pass(&RenderPassDescriptor { label: Some("ui render pass"), color_attachments: &[Some(RenderPassColorAttachment { view: &view, resolve_target: None, ops: Operations { load: LoadOp::Load, store: StoreOp::Store } })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); let mut forgotten_render_pass = render_pass.forget_lifetime(); self.gui_renderer.render(&mut forgotten_render_pass, &paint_jobs, &screen_descriptor); for id in egui_output.textures_delta.free { self.gui_renderer.free_texture(&id); } } let buffer = encoder.finish(); self.queue.submit(std::iter::once(buffer)); output.present(); } }