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<T: UiRenderable> {
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<String, texture::Texture>,
pub mip_generator: MipGenerator,
pub uniform_buffer: Buffer,
pub scale_factor: f64,
pub window: Arc<Window>,
pub size: PhysicalSize<u32>,
pub logical_size: LogicalSize<u32>
}
pub enum RenderInitRes<T: UiRenderable> {
Initialized(Renderer<T>),
NotReadyYet(World, Schedule, T)
}
impl<T: UiRenderable> Debug for RenderInitRes<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Initialized(..) => write!(f, "[initialized renderer]"),
NotReadyYet(..) => write!(f, "[pending initialization]")
}
}
}
impl<T: UiRenderable> Renderer<T> {
pub async fn new(window: Arc<Window>, world: World, update_schedule: Schedule, gui_renderable: T) -> RenderInitRes<T> {
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::<Camera>();
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::<impl 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();
}
}