diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/Cargo.toml | 10 | ||||
-rw-r--r-- | client/src/main.rs | 3 | ||||
-rw-r--r-- | client/src/renderer.rs | 184 | ||||
-rw-r--r-- | client/src/skin_manager.rs | 146 | ||||
-rw-r--r-- | client/src/window.rs | 20 |
5 files changed, 340 insertions, 23 deletions
diff --git a/client/Cargo.toml b/client/Cargo.toml index 8101167..93c1431 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -13,16 +13,26 @@ xdg = "2.5.2" wgpu = "23.0.1" pollster = "0.4.0" winit = "0.30.8" +vek = { version = "0.16.1", default-features = false, features = [ + "bytemuck", + "az", + "rgba", + "std", + "uv", +] } +image = "0.24.9" twgame = { path = "../../twgame/twgame" } twgpu = { path = "../../twgpu/twgpu" } twsnap = { path = "../../twsnap" } +twmap = { path = "../../twmap/twmap" } libtw2-gamenet-ddnet = { path = "../../libtw2/gamenet/ddnet" } libtw2-gamenet-common = { path = "../../libtw2/gamenet/common" } libtw2-event-loop = { path = "../../libtw2/event-loop" } libtw2-packer = { path = "../../libtw2/packer" } libtw2-demo = { path = "../../libtw2/demo" } libtw2-snapshot = { path = "../../libtw2/snapshot" } +twstorage = "0.1.1" # pre-rfc3243-libtw2-gamenet-ddnet = "*" # pre-rfc3243-libtw2-gamenet-common = "*" diff --git a/client/src/main.rs b/client/src/main.rs index 8373db9..5a52620 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -6,11 +6,12 @@ use winit::event_loop::EventLoop; pub mod client; pub mod renderer; pub mod window; +pub mod skin_manager; fn main() -> Result<()> { env_logger::init_from_env("LOG"); - Client::new()?; + // Client::new()?; let evloop = EventLoop::new()?; evloop.run_app(&mut WindowState::new())?; diff --git a/client/src/renderer.rs b/client/src/renderer.rs index c8f7f0b..b213307 100644 --- a/client/src/renderer.rs +++ b/client/src/renderer.rs @@ -1,22 +1,44 @@ -use std::sync::Arc; - +use crate::skin_manager::{SkinManager, init_sprite_textures}; use anyhow::{Result, anyhow}; +use log::warn; use pollster::FutureExt; +use std::{sync::Arc, time::Instant}; use twgpu::{ - Camera, GpuCamera, - map::{GpuMapData, GpuMapStatic}, - shared::Rng, - sprites::{ParticleData, SpriteTextures, SpritesData, SpritesStatic}, + Camera, GpuCamera, TwRenderPass, + blit::Blit, + buffer::GpuBuffer, + map::{GpuMapData, GpuMapRender, GpuMapStatic}, + sprites::{ParticleData, ParticleGroup, SpriteTextures, SpritesData, SpritesStatic}, textures::Samplers, }; +use twmap::TwMap; +use vek::Vec2; use wgpu::{ - Backends, DeviceDescriptor, Features, Instance, InstanceDescriptor, Limits, MemoryHints, - PowerPreference, RequestAdapterOptions, TextureFormat, + Backends, Color, CommandEncoderDescriptor, Device, DeviceDescriptor, Features, Instance, + InstanceDescriptor, Limits, LoadOp, MaintainBase, MemoryHints, Operations, PowerPreference, + Queue, RenderPassColorAttachment, RenderPassDescriptor, RequestAdapterOptions, StoreOp, + Surface, SurfaceConfiguration, TextureFormat, TextureViewDescriptor, }; -use winit::window::Window; +use winit::{dpi::PhysicalSize, window::Window}; pub struct Renderer<'a> { + device: Arc<Device>, + queue: Arc<Queue>, window: &'a Window, + surface: Surface<'a>, + surface_configuration: SurfaceConfiguration, + twmap: TwMap, + map_render: GpuMapRender, + map_data: GpuMapData, + camera: Camera, + sprites_data: SpritesData, + particle_data: ParticleData, + skin_manager: SkinManager, + sprites_static: SpritesStatic, + sprite_textures: SpriteTextures, + gpu_camera: Arc<GpuBuffer<Camera>>, + start: Instant, + need_reconfigure: bool, } impl<'a> Renderer<'a> { @@ -51,9 +73,17 @@ impl<'a> Renderer<'a> { None, ) .block_on()?; + let device = Arc::new(device); + let queue = Arc::new(queue); + + let mut twmap = TwMap::parse(include_bytes!( + "/home/muffin/etc/ddnet-maps/types/novice/maps/Mint.map" + ))?; + + twmap.load()?; let samplers = Arc::new(Samplers::new(&device)); - let mut camera = Camera::new(1.); + let camera = Camera::new(1.); let gpu_camera = Arc::new(GpuCamera::upload(&camera, &device)); let map_static = GpuMapStatic::new(texture_format, &device); @@ -62,15 +92,133 @@ impl<'a> Renderer<'a> { map_static.prepare_render(&twmap, &map_data, &gpu_camera, &samplers, &device); let sprites_static = SpritesStatic::new(texture_format, &device); - let mut sprites_data = SpritesData::new(&device); - let mut particle_data = ParticleData::new(demo.current_time(), &device); - let mut textures = SpriteTextures::new(&device, &queue, &gpu_camera, &samplers); - let mut rng = Rng::new(0); - init_sprite_textures(&mut textures, twmap.version, &device, &queue)?; + let sprites_data = SpritesData::new(&device); + let particle_data = ParticleData::new(0.0, &device); + let mut sprite_textures = SpriteTextures::new(&device, &queue, &gpu_camera, &samplers); + init_sprite_textures(&mut sprite_textures, twmap.version, &device, &queue)?; let blit = Arc::new(Blit::new(&device)); - let mut skin_manager = SkinManager::new(blit, &mut textures, device.clone(), queue.clone()); + let skin_manager = + SkinManager::new(blit, &mut sprite_textures, device.clone(), queue.clone()); + + let surface_configuration = surface + .get_default_config(&adapter, 256, 256) + .ok_or(anyhow!("no surface config"))?; + + surface.configure(&device, &surface_configuration); + + Ok(Self { + start: Instant::now(), + map_data, + sprites_static, + window, + device, + queue, + twmap, + surface, + surface_configuration, + map_render, + camera, + gpu_camera, + particle_data, + skin_manager, + sprites_data, + sprite_textures, + need_reconfigure: false, + }) + } + pub fn resize(&mut self, size: PhysicalSize<u32>) { + self.surface_configuration.width = size.width; + self.surface_configuration.height = size.height; + self.camera + .switch_aspect_ratio(size.width as f32 / size.height as f32); + self.reconfigure(); + } + pub fn reconfigure(&mut self) { + self.surface + .configure(&self.device, &self.surface_configuration); + self.need_reconfigure = false; + } + + pub fn redraw(&mut self) -> Result<()> { + if self.need_reconfigure { + self.reconfigure(); + } + let target = self.surface.get_current_texture()?; + if target.suboptimal { + warn!("suboptimal surface, need reconfigure"); + self.need_reconfigure = true; + } + + let time = self.start.elapsed().as_micros() as i64; + let size = Vec2::new( + self.surface_configuration.width, + self.surface_configuration.height, + ); + self.map_data + .update(&self.twmap, &self.camera, size, time, time, &self.queue); + self.gpu_camera.update(&self.camera, &self.queue); + + let target_view = target + .texture + .create_view(&TextureViewDescriptor::default()); + + let mut commands = self + .device + .create_command_encoder(&CommandEncoderDescriptor { label: None }); + + { + let rpass = commands.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &target_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color::BLACK), + store: StoreOp::Store, + }, + })], + ..Default::default() + }); + let mut rpass = TwRenderPass::new(rpass, size, &self.camera); + self.map_render.render_background(&mut rpass); + self.sprites_static.render_particles( + &self.particle_data, + ParticleGroup::Trails, + &self.sprite_textures, + &mut rpass.render_pass, + ); + self.sprites_static.render( + &self.sprites_data, + &self.sprite_textures, + &mut rpass.render_pass, + ); + self.map_render.render_foreground(&mut rpass); + self.sprites_static.render_particles( + &self.particle_data, + ParticleGroup::Explosions, + &self.sprite_textures, + &mut rpass.render_pass, + ); + self.sprites_static.render_particles( + &self.particle_data, + ParticleGroup::Extra, + &self.sprite_textures, + &mut rpass.render_pass, + ); + self.sprites_static.render_particles( + &self.particle_data, + ParticleGroup::General, + &self.sprite_textures, + &mut rpass.render_pass, + ); + } + + let submission = self.queue.submit(Some(commands.finish())); + self.device + .poll(MaintainBase::WaitForSubmissionIndex(submission)); + + target.present(); - Self { window } + Ok(()) } - pub fn redraw() {} } diff --git a/client/src/skin_manager.rs b/client/src/skin_manager.rs new file mode 100644 index 0000000..1f6624d --- /dev/null +++ b/client/src/skin_manager.rs @@ -0,0 +1,146 @@ +// Adapted from twgpu-tools +// AGPL-3.0-only, Copyright 2025 Patiga + +use anyhow::Result; +use image::{ImageFormat, RgbaImage}; +use std::{collections::HashSet, io::BufReader, sync::Arc}; +use twgpu::{ + blit::Blit, + sprites::{AtlasToken, SpriteTextures, TeeSprite}, +}; +use wgpu::{Device, Queue, Texture}; + +pub fn load_png(path: &str, version: twmap::Version) -> Result<RgbaImage> { + let file = twstorage::read_file(path, version.into())?; + let reader = image::io::Reader::with_format(BufReader::new(file), ImageFormat::Png); + Ok(reader.decode()?.into_rgba8()) +} + +pub fn init_sprite_textures( + textures: &mut SpriteTextures, + version: twmap::Version, + device: &Device, + queue: &Queue, +) -> Result<()> { + println!("Loading sprite textures from filesystem"); + let game = load_png("game.png", version)?; + let particles = load_png("particles.png", version)?; + let emoticons = load_png("emoticons.png", version)?; + let extras = load_png("extras.png", version)?; + textures.game_skin = textures.register_atlas_image(&game, device, queue); + textures.particles = textures.register_atlas_image(&particles, device, queue); + textures.emoticons = textures.register_atlas_image(&emoticons, device, queue); + textures.extras = textures.register_atlas_image(&extras, device, queue); + Ok(()) +} + +type SkinResult = Result<(String, Vec<Texture>), (String, String)>; + +pub struct SkinManager { + known: HashSet<String>, + handles: Vec<std::thread::JoinHandle<SkinResult>>, + blit: Arc<Blit>, + device: Arc<Device>, + queue: Arc<Queue>, +} + +fn load_skin(name: &str, blit: &Blit, device: &Device, queue: &Queue) -> Result<Vec<Texture>> { + // TODO check filename + let path = format!("skins/{name}.png"); + let path2 = format!("downloadedskins/{name}.png"); + let image = match load_png(&path, twmap::Version::DDNet06) { + Err(_) => load_png(&path2, twmap::Version::DDNet06)?, + Ok(img) => img, + }; + + let texture = blit.upload_mipmapped_atlas::<TeeSprite>(&image, device, queue); + Ok(texture) +} + +impl SkinManager { + pub fn new( + blit: Arc<Blit>, + textures: &mut SpriteTextures, + device: Arc<Device>, + queue: Arc<Queue>, + ) -> Self { + let mut manager = Self { + known: HashSet::new(), + handles: Vec::new(), + blit, + device, + queue, + }; + if let Some(default) = manager.load_skin("default", textures) { + textures.default_skin = default; + } + if let Some(ninja) = manager.load_skin("x_ninja", textures) { + textures.ninja_skin = ninja; + } + manager + } + + pub fn load_skin( + &mut self, + name: &str, + textures: &mut SpriteTextures, + ) -> Option<AtlasToken<TeeSprite>> { + let texture = match load_skin(name, &self.blit, &self.device, &self.queue) { + Ok(texture) => texture, + Err(err) => { + println!("Error loading skin '{name}': {err}"); + return None; + } + }; + self.known.insert(name.to_string()); + let token = textures.register_skin_texture(texture, name.to_string(), &self.device); + Some(token) + } + + pub fn queue_load_skin(&mut self, name: String) { + let device = self.device.clone(); + let queue = self.queue.clone(); + let blit = self.blit.clone(); + let handle = std::thread::spawn(move || match load_skin(&name, &blit, &device, &queue) { + Err(err) => Err((name, err.to_string())), + Ok(texture) => Ok((name, texture)), + }); + self.handles.push(handle); + } + + pub fn wait_for_queued(&mut self, textures: &mut SpriteTextures) { + while let Some(handle) = self.handles.pop() { + match handle.join().unwrap() { + Ok((name, texture)) => { + textures.register_skin_texture(texture, name, &self.device); + } + Err((name, err)) => println!("Error with skin '{name}': {err}"), + } + } + } + + pub fn poll_queued(&mut self, textures: &mut SpriteTextures) { + while !self.handles.is_empty() { + if self.handles[0].is_finished() { + match self.handles.remove(0).join().unwrap() { + Ok((name, texture)) => { + textures.register_skin_texture(texture, name, &self.device); + } + Err((name, err)) => println!("Error with skin '{name}': {err}"), + } + } else { + return; + } + } + } + + pub fn queue_snap_skins(&mut self, snap: &twsnap::Snap) { + for player in snap.players.values() { + if !self.known.contains(player.skin.as_str()) { + println!("New skin: '{}'", player.skin); + self.known.insert(player.skin.to_string()); + self.queue_load_skin(player.skin.to_string()); + } + } + } +} diff --git a/client/src/window.rs b/client/src/window.rs index d0296e7..0bcac6a 100644 --- a/client/src/window.rs +++ b/client/src/window.rs @@ -1,4 +1,5 @@ use crate::renderer::Renderer; +use log::warn; use winit::{ application::ApplicationHandler, event::WindowEvent, @@ -20,19 +21,30 @@ impl ApplicationHandler for WindowState { let window = event_loop .create_window(WindowAttributes::default().with_maximized(true)) .unwrap(); - let renderer = Renderer::new(unsafe { std::mem::transmute(&window) }); + let renderer = Renderer::new(unsafe { std::mem::transmute(&window) }).unwrap(); self.window = Some((window, renderer)) } fn window_event( &mut self, event_loop: &ActiveEventLoop, - window_id: WindowId, + _window_id: WindowId, event: WindowEvent, ) { - if let Some((win, ren)) = &self.window { + if let Some((win, ren)) = &mut self.window { match event { - WindowEvent::RedrawRequested => {} + WindowEvent::CloseRequested => { + event_loop.exit(); + } + WindowEvent::Resized(size) => { + ren.resize(size); + } + WindowEvent::RedrawRequested => { + if let Err(e) = ren.redraw() { + warn!("{e:?}") + } + win.request_redraw(); + } _ => (), } } |