aboutsummaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/Cargo.toml10
-rw-r--r--client/src/main.rs3
-rw-r--r--client/src/renderer.rs184
-rw-r--r--client/src/skin_manager.rs146
-rw-r--r--client/src/window.rs20
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();
+ }
_ => (),
}
}