From 3344eb2d678f9c5973c8e38083760254b54c20fc Mon Sep 17 00:00:00 2001 From: metamuffin Date: Thu, 23 Jan 2025 22:45:35 +0100 Subject: split scene_prepare to many files --- client/src/interfaces/mod.rs | 2 +- client/src/main.rs | 6 +- client/src/meshops.rs | 81 --- client/src/render/mod.rs | 242 +++++++ client/src/render/scene/draw.rs | 104 +++ client/src/render/scene/meshops.rs | 81 +++ client/src/render/scene/mod.rs | 518 +++++++++++++++ client/src/render/scene/pipelines.rs | 196 ++++++ client/src/render/scene/textures.rs | 284 ++++++++ client/src/render/scene/vertex_buffers.rs | 138 ++++ client/src/renderer.rs | 239 ------- client/src/scene_prepare.rs | 1030 ----------------------------- client/src/scene_render.rs | 104 --- client/src/state.rs | 8 +- 14 files changed, 1567 insertions(+), 1466 deletions(-) delete mode 100644 client/src/meshops.rs create mode 100644 client/src/render/mod.rs create mode 100644 client/src/render/scene/draw.rs create mode 100644 client/src/render/scene/meshops.rs create mode 100644 client/src/render/scene/mod.rs create mode 100644 client/src/render/scene/pipelines.rs create mode 100644 client/src/render/scene/textures.rs create mode 100644 client/src/render/scene/vertex_buffers.rs delete mode 100644 client/src/renderer.rs delete mode 100644 client/src/scene_render.rs (limited to 'client') diff --git a/client/src/interfaces/mod.rs b/client/src/interfaces/mod.rs index 9a68da7..d7e8c7e 100644 --- a/client/src/interfaces/mod.rs +++ b/client/src/interfaces/mod.rs @@ -18,7 +18,7 @@ pub mod prefabindex; pub mod profiler; use crate::{ - download::Downloader, network::Network, scene_prepare::ScenePreparer, ui::UI_POSITION_OFFSET, + download::Downloader, network::Network, render::scene::ScenePreparer, ui::UI_POSITION_OFFSET, }; use egui::{Pos2, Widget}; use prefabindex::PrefabIndexInterface; diff --git a/client/src/main.rs b/client/src/main.rs index e72e3b0..673a71f 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -20,15 +20,13 @@ pub mod audio; pub mod camera; pub mod download; pub mod interfaces; -pub mod meshops; pub mod network; -pub mod renderer; +pub mod render; pub mod scene_prepare; -pub mod scene_render; +pub mod shaders; pub mod state; pub mod ui; pub mod window; -pub mod shaders; use anyhow::Result; use clap::Parser; diff --git a/client/src/meshops.rs b/client/src/meshops.rs deleted file mode 100644 index 0a3f963..0000000 --- a/client/src/meshops.rs +++ /dev/null @@ -1,81 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use glam::{Vec2, Vec3, vec2}; - -pub fn generate_normals(index: &[[u32; 3]], position: &[Vec3]) -> Vec { - let mut normal_denom = vec![0; position.len()]; - let mut normal = vec![Vec3::ZERO; position.len()]; - - for &[a, b, c] in index { - let pos_a = position[a as usize]; - let pos_b = position[b as usize]; - let pos_c = position[c as usize]; - - // TODO is this right? - let norm = (pos_b - pos_a).cross(pos_c - pos_a).normalize(); - - normal[a as usize] += norm; - normal[b as usize] += norm; - normal[c as usize] += norm; - normal_denom[a as usize] += 1; - normal_denom[b as usize] += 1; - normal_denom[c as usize] += 1; - } - for (denom, norm) in normal_denom.iter().zip(normal.iter_mut()) { - *norm /= *denom as f32; - } - normal -} - -pub fn generate_tangents(index: &[[u32; 3]], position: &[Vec3], texcoord: &[Vec2]) -> Vec { - let mut tangent_denom = vec![0; position.len()]; - let mut tangent = vec![Vec3::ZERO; position.len()]; - - for &[a, b, c] in index { - let (pos_a, uv_a) = (position[a as usize], texcoord[a as usize]); - let (pos_b, uv_b) = (position[b as usize], texcoord[b as usize]); - let (pos_c, uv_c) = (position[c as usize], texcoord[c as usize]); - - let pd_ba = pos_b - pos_a; - let pd_ca = pos_c - pos_a; - let td_ba = uv_b - uv_a; - let td_ca = uv_c - uv_a; - - let face_tangent = - (pd_ba * td_ca.y - pd_ca * td_ba.y) * (td_ba.x * td_ca.y - td_ba.y * td_ca.x); - - tangent[a as usize] += face_tangent; - tangent[b as usize] += face_tangent; - tangent[c as usize] += face_tangent; - tangent_denom[a as usize] += 1; - tangent_denom[b as usize] += 1; - tangent_denom[c as usize] += 1; - } - for (denom, tang) in tangent_denom.iter().zip(tangent.iter_mut()) { - *tang /= *denom as f32; - } - - tangent -} - -pub fn generate_texcoords(index: &[[u32; 3]], position: &[Vec3]) -> Vec { - let _ = (index, position); - // TODO implement equirectangular projection or something - (0..position.len()) - .map(|i| vec2(i as f32 * 0.01, 0.)) - .collect() -} diff --git a/client/src/render/mod.rs b/client/src/render/mod.rs new file mode 100644 index 0000000..eb9e089 --- /dev/null +++ b/client/src/render/mod.rs @@ -0,0 +1,242 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +pub mod scene; + +use crate::{ + camera::Camera, download::Downloader, interfaces::profiler::TimingProfiler, state::InputState, + ui::UiRenderer, +}; +use anyhow::{Result, anyhow}; +use log::{info, warn}; +use pollster::FutureExt; +use scene::{ScenePreparer, draw::ScenePipeline}; +use std::{ + mem::swap, + sync::{Arc, Mutex}, + thread::{sleep, spawn}, + time::Duration, +}; +use weareshared::tree::SceneTree; +use wgpu::{ + AdapterInfo, Backends, CommandEncoderDescriptor, Device, DeviceDescriptor, Extent3d, Features, + Instance, InstanceDescriptor, Limits, MaintainBase, PowerPreference, Queue, + RequestAdapterOptions, Surface, SurfaceConfiguration, TextureDescriptor, TextureDimension, + TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, +}; +use winit::window::Window; + +pub struct Renderer<'a> { + surface: Surface<'a>, + queue: Arc, + device: Arc, + surface_configuration: SurfaceConfiguration, + scene_pipeline: ScenePipeline, + pub ui_renderer: UiRenderer, + pub scene_prepare: Arc, + surface_needs_reconfigure: bool, + depth: TextureView, + pub timing: TimingProfiler, + pub timing_submit: Arc>, + pub adapter_info: Arc, +} +impl<'a> Renderer<'a> { + pub fn new(window: &'a Window, downloader: Arc) -> Result { + info!("wgpu init"); + let instance = Instance::new(InstanceDescriptor { + backends: Backends::all(), + ..Default::default() + }); + + let surface = instance.create_surface(window)?; + let adapter = instance + .request_adapter(&RequestAdapterOptions { + compatible_surface: Some(&surface), + power_preference: PowerPreference::HighPerformance, + ..Default::default() + }) + .block_on() + .ok_or(anyhow!("no adapter found"))?; + + let (device, queue) = adapter + .request_device( + &DeviceDescriptor { + required_features: Features::PUSH_CONSTANTS, + required_limits: Limits { + max_push_constant_size: 112, + max_vertex_buffers: 16, + ..Limits::default() + }, + ..Default::default() + }, + None, + ) + .block_on()?; + + let surface_configuration = surface + .get_default_config(&adapter, 256, 256) + .ok_or(anyhow!("no default config"))?; + + surface.configure(&device, &surface_configuration); + + let device = Arc::new(device); + let queue = Arc::new(queue); + + let scene_prepare = Arc::new(ScenePreparer::new( + device.clone(), + queue.clone(), + surface_configuration.format, + downloader, + )); + + let ui_renderer = + UiRenderer::new(device.clone(), queue.clone(), surface_configuration.format); + + let depth = device.create_texture(&TextureDescriptor { + label: None, + size: Extent3d { + height: 256, + width: 256, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Depth32Float, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let depth = depth.create_view(&TextureViewDescriptor::default()); + + // TODO multithreading introduces double-loading some resources. fix that before increasing thread count + for _ in 0..1 { + let scene_prepare = scene_prepare.clone(); + spawn(move || { + loop { + let ndone = scene_prepare.update().unwrap(); + if ndone == 0 { + sleep(Duration::from_millis(50)); + } + } + }); + } + + let adapter_info = Arc::new(adapter.get_info()); + + Ok(Self { + scene_pipeline: ScenePipeline, + scene_prepare, + surface, + adapter_info, + depth, + device, + queue, + surface_configuration, + ui_renderer, + surface_needs_reconfigure: false, + timing: Default::default(), + timing_submit: Default::default(), + }) + } + + pub fn resize(&mut self, width: u32, height: u32) { + self.surface_configuration.width = width; + self.surface_configuration.height = height; + self.surface + .configure(&self.device, &self.surface_configuration); + + self.depth = self + .device + .create_texture(&TextureDescriptor { + label: None, + size: Extent3d { + height, + width, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Depth32Float, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + .create_view(&TextureViewDescriptor::default()); + } + + pub fn draw( + &mut self, + scene: &SceneTree, + camera: &Camera, + input_state: &mut InputState, + ) -> Result<()> { + self.timing.begin("prepare"); + if self.surface_needs_reconfigure { + self.surface + .configure(&self.device, &self.surface_configuration); + self.surface_needs_reconfigure = false + } + let target = self.surface.get_current_texture()?; + if target.suboptimal { + warn!("suboptimal swapchain texture"); + self.surface_needs_reconfigure = true; + } + let target_view = target + .texture + .create_view(&TextureViewDescriptor::default()); + + let mut commands = self + .device + .create_command_encoder(&CommandEncoderDescriptor { label: None }); + + let projection = camera.to_matrix(); + + self.timing.checkpoint("draw scene"); + self.scene_pipeline.draw( + &mut commands, + &target_view, + &self.depth, + scene, + &self.scene_prepare.prefabs, + projection, + ); + + self.timing.checkpoint("draw ui"); + self.ui_renderer.draw( + &mut commands, + &target_view, + &self.depth, + projection, + input_state, + &self.surface_configuration, + ); + + self.timing.checkpoint("submit"); + let i = self.queue.submit(Some(commands.finish())); + self.timing.checkpoint("poll"); + self.device.poll(MaintainBase::WaitForSubmissionIndex(i)); + + self.timing.checkpoint("present"); + target.present(); + + self.timing.checkpoint(""); + + let mut ts = self.timing_submit.lock().unwrap(); + swap(&mut *ts, &mut self.timing); + + Ok(()) + } +} diff --git a/client/src/render/scene/draw.rs b/client/src/render/scene/draw.rs new file mode 100644 index 0000000..dabb9cd --- /dev/null +++ b/client/src/render/scene/draw.rs @@ -0,0 +1,104 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use super::{DemandMap, RPrefab}; +use glam::{EulerRot, Mat3, Mat4}; +use std::sync::Arc; +use weareshared::{packets::Resource, resources::Prefab, tree::SceneTree}; +use wgpu::{ + Color, CommandEncoder, IndexFormat, LoadOp, Operations, RenderPassColorAttachment, + RenderPassDepthStencilAttachment, RenderPassDescriptor, ShaderStages, StoreOp, TextureView, +}; + +pub struct ScenePipeline; + +impl ScenePipeline { + pub fn draw( + &mut self, + commands: &mut CommandEncoder, + target: &TextureView, + depth: &TextureView, + scene: &SceneTree, + prefabs: &DemandMap, Arc>, + projection: Mat4, + ) { + let mut rpass = commands.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: Operations { + store: StoreOp::Store, + load: LoadOp::Clear(Color { + r: 0.01, + g: 0.01, + b: 0.01, + a: 1., + }), + }, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth, + depth_ops: Some(Operations { + load: LoadOp::Clear(1.), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + ..Default::default() + }); + + for ob in scene.objects.values() { + let prefab_projection = projection + * Mat4::from_translation(ob.pos.into()) + * Mat4::from_mat3(Mat3::from_euler( + EulerRot::YXZ, + ob.rot.x, + ob.rot.y, + ob.rot.z, + )); + if let Some(prefab) = prefabs.try_get(ob.res.clone()) { + for (affine, part) in &prefab.0 { + let part_projection = prefab_projection + * Mat4::from_translation(affine.translation.into()) + * Mat4::from_mat3a(affine.matrix3); + let projection = part_projection.to_cols_array().map(|v| v.to_le_bytes()); + let mb = affine.matrix3.to_cols_array(); // TODO apply object rotation + // add padding for gpu mat3x3 repr + let model_basis = [ + mb[0], mb[1], mb[2], 0., // + mb[3], mb[4], mb[5], 0., // + mb[6], mb[7], mb[8], 0., // + ]; + let model_basis = bytemuck::cast_slice(&model_basis); + + rpass.set_pipeline(&part.pipeline); + rpass.set_bind_group(0, &*part.tex_albedo, &[]); + rpass.set_bind_group(1, &*part.tex_normal, &[]); + rpass.set_bind_group(2, &*part.material, &[]); + rpass.set_push_constants(ShaderStages::VERTEX, 0, projection.as_flattened()); + rpass.set_push_constants(ShaderStages::VERTEX, 64, model_basis); + rpass.set_index_buffer(part.index.slice(..), IndexFormat::Uint32); + rpass.set_vertex_buffer(0, part.va_position.slice(..)); + rpass.set_vertex_buffer(1, part.va_normal.slice(..)); + rpass.set_vertex_buffer(2, part.va_tangent.slice(..)); + rpass.set_vertex_buffer(3, part.va_texcoord.slice(..)); + rpass.draw_indexed(0..part.index_count, 0, 0..1); + } + } + } + } +} diff --git a/client/src/render/scene/meshops.rs b/client/src/render/scene/meshops.rs new file mode 100644 index 0000000..0a3f963 --- /dev/null +++ b/client/src/render/scene/meshops.rs @@ -0,0 +1,81 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use glam::{Vec2, Vec3, vec2}; + +pub fn generate_normals(index: &[[u32; 3]], position: &[Vec3]) -> Vec { + let mut normal_denom = vec![0; position.len()]; + let mut normal = vec![Vec3::ZERO; position.len()]; + + for &[a, b, c] in index { + let pos_a = position[a as usize]; + let pos_b = position[b as usize]; + let pos_c = position[c as usize]; + + // TODO is this right? + let norm = (pos_b - pos_a).cross(pos_c - pos_a).normalize(); + + normal[a as usize] += norm; + normal[b as usize] += norm; + normal[c as usize] += norm; + normal_denom[a as usize] += 1; + normal_denom[b as usize] += 1; + normal_denom[c as usize] += 1; + } + for (denom, norm) in normal_denom.iter().zip(normal.iter_mut()) { + *norm /= *denom as f32; + } + normal +} + +pub fn generate_tangents(index: &[[u32; 3]], position: &[Vec3], texcoord: &[Vec2]) -> Vec { + let mut tangent_denom = vec![0; position.len()]; + let mut tangent = vec![Vec3::ZERO; position.len()]; + + for &[a, b, c] in index { + let (pos_a, uv_a) = (position[a as usize], texcoord[a as usize]); + let (pos_b, uv_b) = (position[b as usize], texcoord[b as usize]); + let (pos_c, uv_c) = (position[c as usize], texcoord[c as usize]); + + let pd_ba = pos_b - pos_a; + let pd_ca = pos_c - pos_a; + let td_ba = uv_b - uv_a; + let td_ca = uv_c - uv_a; + + let face_tangent = + (pd_ba * td_ca.y - pd_ca * td_ba.y) * (td_ba.x * td_ca.y - td_ba.y * td_ca.x); + + tangent[a as usize] += face_tangent; + tangent[b as usize] += face_tangent; + tangent[c as usize] += face_tangent; + tangent_denom[a as usize] += 1; + tangent_denom[b as usize] += 1; + tangent_denom[c as usize] += 1; + } + for (denom, tang) in tangent_denom.iter().zip(tangent.iter_mut()) { + *tang /= *denom as f32; + } + + tangent +} + +pub fn generate_texcoords(index: &[[u32; 3]], position: &[Vec3]) -> Vec { + let _ = (index, position); + // TODO implement equirectangular projection or something + (0..position.len()) + .map(|i| vec2(i as f32 * 0.01, 0.)) + .collect() +} diff --git a/client/src/render/scene/mod.rs b/client/src/render/scene/mod.rs new file mode 100644 index 0000000..d83cb95 --- /dev/null +++ b/client/src/render/scene/mod.rs @@ -0,0 +1,518 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +pub mod draw; +pub mod meshops; +pub mod pipelines; +pub mod textures; +pub mod vertex_buffers; + +use crate::{armature::RArmature, download::Downloader, shaders::SceneShaders}; +use anyhow::Result; +use bytemuck::{Pod, Zeroable}; +use egui::{Grid, Widget}; +use glam::{UVec3, UVec4, Vec2, Vec3, Vec3A, uvec3, uvec4}; +use humansize::DECIMAL; +use log::{debug, trace}; +use pipelines::SceneBgLayouts; +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, + marker::PhantomData, + sync::{Arc, RwLock}, + time::Instant, +}; +use textures::MipGenerationPipeline; +use weareshared::{ + Affine3A, + packets::Resource, + resources::{Image, MeshPart, Prefab}, +}; +use wgpu::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, Buffer, BufferUsages, Device, Queue, + RenderPipeline, Texture, TextureFormat, + util::{BufferInitDescriptor, DeviceExt}, +}; + +pub struct DemandMap { + inner: RwLock>, +} +struct DemandMapState { + values: HashMap, + needed: HashSet, + size_metric: usize, +} +impl DemandMap { + pub fn new() -> Self { + Self { + inner: DemandMapState { + needed: HashSet::new(), + values: HashMap::new(), + size_metric: 0, + } + .into(), + } + } + pub fn needed(&self) -> Vec { + self.inner.read().unwrap().needed.iter().cloned().collect() + } + pub fn insert(&self, key: K, value: V, size: usize) { + let mut s = self.inner.write().unwrap(); + s.needed.remove(&key); + s.values.insert(key, value); + s.size_metric += size; + } + pub fn try_get(&self, key: K) -> Option { + let mut s = self.inner.write().unwrap(); + if let Some(k) = s.values.get(&key) { + Some(k.to_owned()) + } else { + s.needed.insert(key); + None + } + } +} + +struct GraphicsConfig { + max_anisotropy: u16, + max_mip_count: u32, +} + +pub struct ScenePreparer { + device: Arc, + queue: Arc, + layouts: SceneBgLayouts, + shaders: SceneShaders, + render_format: TextureFormat, + config: GraphicsConfig, + downloader: Arc, + + textures: DemandMap, Arc)>, + placeholder_textures: DemandMap, Arc)>, + index_buffers: DemandMap>, (Arc, u32)>, + vertex_buffers: DemandMap>, Arc>, + generated_tangent_buffers: DemandMap>, + generated_normal_buffers: DemandMap>, + generated_texcoord_buffers: DemandMap>, + mesh_parts: DemandMap, Arc>, + materials: DemandMap>, + pipelines: DemandMap>, + mip_generation_pipelines: DemandMap>, + pub prefabs: DemandMap, Arc>, +} + +pub struct RPrefab(pub Vec<(Affine3A, Arc)>); +pub struct RMeshPart { + pub pipeline: Arc, + pub index_count: u32, + pub index: Arc, + pub va_position: Arc, + pub va_normal: Arc, + pub va_tangent: Arc, + pub va_texcoord: Arc, + pub tex_albedo: Arc, + pub tex_normal: Arc, + pub material: Arc, + pub double_sided: bool, + + pub va_joint_index: Option>, + pub va_joint_weight: Option>, + pub joint_uniform: Option>, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct PipelineSpec { + format: TextureFormat, + skin: bool, + backface_culling: bool, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct TextureSpec { + data: Resource>, + linear: bool, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct TangentBufferSpec { + index: Resource>, + position: Resource>, + texcoord: Option>>, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct NormalBufferSpec { + index: Resource>, + position: Resource>, +} +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct TexcoordBufferSpec { + index: Resource>, + position: Resource>, +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +enum TextureIdentityKind { + Normal, + Multiply, +} + +#[derive(Debug, Clone, Copy, Pod, Zeroable, Hash, PartialEq, Eq)] +#[repr(C)] +struct Material { + roughness: u32, + metallic: u32, + _pad1: [u32; 2], + albedo_alpha: UVec4, + emission: UVec3, + _pad2: u32, +} + +impl ScenePreparer { + pub fn new( + device: Arc, + queue: Arc, + render_format: TextureFormat, + downloader: Arc, + ) -> Self { + Self { + // TODO normal mipmap requires linear texture, also demand map? + render_format, + config: GraphicsConfig { + max_anisotropy: 16, + max_mip_count: 16, + }, + layouts: SceneBgLayouts::load(&device), + shaders: SceneShaders::load(&device), + device, + queue, + downloader, + index_buffers: DemandMap::new(), + vertex_buffers: DemandMap::new(), + mesh_parts: DemandMap::new(), + prefabs: DemandMap::new(), + textures: DemandMap::new(), + placeholder_textures: DemandMap::new(), + generated_tangent_buffers: DemandMap::new(), + generated_normal_buffers: DemandMap::new(), + generated_texcoord_buffers: DemandMap::new(), + materials: DemandMap::new(), + pipelines: DemandMap::new(), + mip_generation_pipelines: DemandMap::new(), + } + } + pub fn update(&self) -> Result { + let mut num_done = 0; + + self.update_textures(&mut num_done)?; + self.update_vertex_buffers(&mut num_done)?; + + for pres in self.prefabs.needed() { + if let Some(prefab) = self.downloader.try_get(pres.clone())? { + let mut rprefab = RPrefab(Vec::new()); + for (aff, partres) in &prefab.mesh { + if let Some(part) = self.mesh_parts.try_get(partres.clone()) { + rprefab.0.push((*aff, part.clone())); + } + } + if rprefab.0.len() == prefab.mesh.len() { + self.prefabs.insert(pres.clone(), Arc::new(rprefab), 0); + debug!("prefab created ({pres})"); + num_done += 1; + } + } + } + + for spec in self.materials.needed() { + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("material props"), + usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + contents: bytemuck::cast_slice(&[spec]), + }); + let bind_group = self.device.create_bind_group(&BindGroupDescriptor { + label: Some("material"), + layout: &self.layouts.material, + entries: &[BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + self.materials.insert(spec, Arc::new(bind_group), 0); + } + for spec in self.pipelines.needed() { + self.pipelines.insert( + spec.clone(), + Arc::new(spec.create(&self.device, &self.layouts, &self.shaders)), + 0, + ); + } + for pres in self.mesh_parts.needed() { + let start = Instant::now(); + if let Some(part) = self.downloader.try_get(pres.clone())? { + if let (Some(indexres), Some(positionres)) = (part.index, part.va_position) { + let index = self.index_buffers.try_get(indexres.clone()); + let position = self + .vertex_buffers + .try_get(Resource(positionres.0, PhantomData)); + + let normal = if let Some(res) = part.va_normal.clone() { + self.vertex_buffers.try_get(Resource(res.0, PhantomData)) + } else { + self.generated_normal_buffers.try_get(NormalBufferSpec { + index: indexres.clone(), + position: Resource(positionres.0, PhantomData), + }) + }; + + let texcoord = if let Some(res) = part.va_texcoord.clone() { + self.vertex_buffers.try_get(Resource(res.0, PhantomData)) + } else { + self.generated_texcoord_buffers.try_get(TexcoordBufferSpec { + index: indexres.clone(), + position: Resource(positionres.0, PhantomData), + }) + }; + let tangent = if let Some(res) = part.va_tangent.clone() { + self.vertex_buffers.try_get(Resource(res.0, PhantomData)) + } else { + self.generated_tangent_buffers.try_get(TangentBufferSpec { + index: indexres, + position: Resource(positionres.0, PhantomData), + texcoord: part.va_texcoord, + }) + }; + + let joint_weight = if let Some(res) = part.va_joint_weight.clone() { + self.vertex_buffers + .try_get(Resource(res.0, PhantomData)) + .map(Some) + } else { + Some(None) + }; + let joint_index = if let Some(res) = part.va_joint_index.clone() { + self.vertex_buffers + .try_get(Resource(res.0, PhantomData)) + .map(Some) + } else { + Some(None) + }; + + let mut tex_albedo = None; + if let Some(albedores) = part.tex_albedo { + if let Some((_tex, bg)) = self.textures.try_get(TextureSpec { + data: albedores, + linear: false, + }) { + tex_albedo = Some(bg) + } + } else { + if let Some((_tex, bg)) = self + .placeholder_textures + .try_get(TextureIdentityKind::Multiply) + { + tex_albedo = Some(bg) + } + } + let mut tex_normal = None; + if let Some(normalres) = part.tex_normal { + if let Some((_tex, bg)) = self.textures.try_get(TextureSpec { + data: normalres, + linear: true, + }) { + tex_normal = Some(bg) + } + } else { + if let Some((_tex, bg)) = self + .placeholder_textures + .try_get(TextureIdentityKind::Normal) + { + tex_normal = Some(bg) + } + } + + let material = self.materials.try_get({ + let albedo = part.g_albedo.unwrap_or(Vec3A::ONE); + let emission = part.g_emission.unwrap_or(Vec3A::ONE); + Material { + roughness: part.g_roughness.unwrap_or(1.).to_bits(), + metallic: part.g_metallic.unwrap_or(0.).to_bits(), + _pad1: [0, 0], + albedo_alpha: uvec4( + albedo.x.to_bits(), + albedo.y.to_bits(), + albedo.z.to_bits(), + part.g_alpha.unwrap_or(1.).to_bits(), + ), + emission: uvec3( + emission.x.to_bits(), + emission.y.to_bits(), + emission.z.to_bits(), + ), + _pad2: 0, + } + }); + + let armature = if let Some(res) = part.armature.clone() { + Some(self.downloader.try_get(res)?) + } else { + Some(None) + }; + + let pipeline = self.pipelines.try_get(PipelineSpec { + format: self.render_format, + skin: false, + backface_culling: part.g_double_sided.is_none(), + }); + + if let ( + Some(pipeline), + Some((index, index_count)), + Some(va_normal), + Some(va_tangent), + Some(va_texcoord), + Some(va_position), + Some(va_joint_index), + Some(va_joint_weight), + Some(armature), + Some(tex_normal), + Some(tex_albedo), + Some(material), + ) = ( + pipeline, + index, + normal, + tangent, + texcoord, + position, + joint_index, + joint_weight, + armature, + tex_normal, + tex_albedo, + material, + ) { + let double_sided = part.g_double_sided.is_some(); + + let joint_uniform = if let Some(a) = armature { + let ra = RArmature::new(&self.device, a); + Some(ra.joint_mat_uniform_buffer.clone()) + } else { + None + }; + + debug!("part created (took {:?}) {pres}", start.elapsed()); + self.mesh_parts.insert( + pres, + Arc::new(RMeshPart { + pipeline, + index_count, + index, + va_normal, + va_tangent, + va_position, + va_texcoord, + va_joint_index, + va_joint_weight, + tex_albedo, + tex_normal, + material, + double_sided, + joint_uniform, + }), + 0, + ); + num_done += 1; + } + } + } + } + self.print_missing(); + Ok(num_done) + } +} + +impl Widget for &DemandMap { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + let state = self.inner.read().unwrap(); + ui.label(state.needed.len().to_string()); + ui.label(state.values.len().to_string()); + ui.label(humansize::format_size(state.size_metric, DECIMAL)); + ui.end_row(); + ui.response() + } +} +impl ScenePreparer { + pub fn print_missing(&self) { + fn visit(name: &str, m: &DemandMap) + where + K: Clone, + K: Hash, + K: std::cmp::Eq, + V: Clone, + { + let nl = m.needed().len(); + if nl > 0 { + trace!("{name}: need {nl}") + } + } + visit("prefabs", &self.prefabs); + visit("mesh_parts", &self.mesh_parts); + visit("vertex_buffers", &self.vertex_buffers); + visit("index_buffers", &self.index_buffers); + visit("placeholder_textures", &self.placeholder_textures); + visit("generated_tangent_buffers", &self.generated_tangent_buffers); + visit("generated_normal_buffers", &self.generated_normal_buffers); + visit( + "generated_texcoord_buffers", + &self.generated_texcoord_buffers, + ); + visit("textures", &self.textures); + visit("materials", &self.materials); + visit("pipelines", &self.pipelines); + } +} + +impl Widget for &ScenePreparer { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + Grid::new("sp") + .num_columns(4) + .show(ui, |ui| { + ui.label("prefabs"); + self.prefabs.ui(ui); + ui.label("mesh_parts"); + self.mesh_parts.ui(ui); + ui.label("vertex_buffers"); + self.vertex_buffers.ui(ui); + ui.label("index_buffers"); + self.index_buffers.ui(ui); + ui.label("placeholder_textures"); + self.placeholder_textures.ui(ui); + ui.label("generated_tangent_buffers"); + self.generated_tangent_buffers.ui(ui); + ui.label("generated_normal_buffers"); + self.generated_normal_buffers.ui(ui); + ui.label("generated_texcoord_buffers"); + self.generated_texcoord_buffers.ui(ui); + ui.label("textures"); + self.textures.ui(ui); + ui.label("materials"); + self.materials.ui(ui); + ui.label("pipelines"); + self.pipelines.ui(ui); + }) + .response + } +} diff --git a/client/src/render/scene/pipelines.rs b/client/src/render/scene/pipelines.rs new file mode 100644 index 0000000..3b6758e --- /dev/null +++ b/client/src/render/scene/pipelines.rs @@ -0,0 +1,196 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use wgpu::{ + BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendState, + BufferBindingType, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, + DepthStencilState, Device, Face, FragmentState, FrontFace, MultisampleState, + PipelineCompilationOptions, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, + PrimitiveTopology, PushConstantRange, RenderPipeline, RenderPipelineDescriptor, + SamplerBindingType, ShaderStages, StencilState, TextureFormat, TextureSampleType, + TextureViewDimension, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState, + VertexStepMode, +}; + +use crate::shaders::SceneShaders; + +use super::PipelineSpec; + +pub struct SceneBgLayouts { + pub texture: BindGroupLayout, + pub material: BindGroupLayout, + pub joints: BindGroupLayout, +} + +impl SceneBgLayouts { + pub fn load(device: &Device) -> Self { + Self { + texture: device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + count: None, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + }, + BindGroupLayoutEntry { + binding: 1, + count: None, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + }, + ], + label: None, + }), + material: device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + count: None, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }], + label: None, + }), + joints: device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + count: None, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }], + label: None, + }), + } + } +} + +impl PipelineSpec { + pub fn create( + &self, + device: &Device, + layouts: &SceneBgLayouts, + shaders: &SceneShaders, + ) -> RenderPipeline { + let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&layouts.texture, &layouts.texture, &layouts.material], + push_constant_ranges: &[PushConstantRange { + // 4x4 view projections + // 3x3(+1 pad) model basis + range: 0..((4 * 4 + 3 * 4) * size_of::() as u32), + stages: ShaderStages::VERTEX, + }], + }); + device.create_render_pipeline(&RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + fragment: Some(FragmentState { + module: &shaders.fragment_pbr, + entry_point: Some("main"), + targets: &[Some(ColorTargetState { + blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), + format: self.format, + write_mask: ColorWrites::all(), + })], + compilation_options: PipelineCompilationOptions::default(), + }), + vertex: VertexState { + module: if self.skin { + &shaders.vertex_world_skin + } else { + &shaders.vertex_world + }, + entry_point: Some("main"), + buffers: &[ + // position + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 3 * size_of::() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }], + }, + // normal + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 3 * size_of::() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 1, + }], + }, + // tangent + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 3 * size_of::() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 2, + }], + }, + // texcoord + VertexBufferLayout { + step_mode: VertexStepMode::Vertex, + array_stride: 2 * size_of::() as u64, + attributes: &[VertexAttribute { + format: VertexFormat::Float32x2, + offset: 0, + shader_location: 3, + }], + }, + ], + compilation_options: PipelineCompilationOptions::default(), + }, + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + front_face: FrontFace::Ccw, + cull_mode: if self.backface_culling { + Some(Face::Back) + } else { + None + }, + polygon_mode: PolygonMode::Fill, + ..Default::default() + }, + depth_stencil: Some(DepthStencilState { + depth_write_enabled: true, + depth_compare: CompareFunction::Less, + format: TextureFormat::Depth32Float, + bias: DepthBiasState::default(), + stencil: StencilState::default(), + }), + multisample: MultisampleState::default(), + multiview: None, + cache: None, + }) + } +} diff --git a/client/src/render/scene/textures.rs b/client/src/render/scene/textures.rs new file mode 100644 index 0000000..0a042bd --- /dev/null +++ b/client/src/render/scene/textures.rs @@ -0,0 +1,284 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use super::{GraphicsConfig, ScenePreparer, TextureIdentityKind}; +use anyhow::Result; +use image::ImageReader; +use log::debug; +use std::{io::Cursor, sync::Arc, time::Instant}; +use wgpu::{ + AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindingResource, + Color, ColorTargetState, ColorWrites, CommandEncoderDescriptor, Device, Extent3d, FilterMode, + ImageDataLayout, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipeline, SamplerDescriptor, StoreOp, Texture, TextureAspect, TextureDescriptor, + TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor, include_wgsl, +}; + +pub struct MipGenerationPipeline { + pipeline: RenderPipeline, +} +impl MipGenerationPipeline { + pub fn load(device: &Device, format: TextureFormat) -> Self { + let shader = device.create_shader_module(include_wgsl!("../../shaders/texture_copy.wgsl")); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("mip generator"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(ColorTargetState { + format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + Self { pipeline } + } +} + +impl ScenePreparer { + pub fn update_textures(&self, num_done: &mut usize) -> Result<()> { + for format in self.mip_generation_pipelines.needed() { + self.mip_generation_pipelines.insert( + format, + Arc::new(MipGenerationPipeline::load(&self.device, format)), + 0, + ); + *num_done += 1 + } + + for kind in self.placeholder_textures.needed() { + let (linear, color) = match kind { + TextureIdentityKind::Normal => (true, [128, 128, 255, 255]), + TextureIdentityKind::Multiply => (false, [255, 255, 255, 255]), + }; + let tex_bg = create_texture( + &self.device, + &self.queue, + &self.layouts.texture, + &color, + 1, + 1, + if linear { + TextureFormat::Rgba8Unorm + } else { + TextureFormat::Rgba8UnormSrgb + }, + None, + &self.config, + ); + self.placeholder_textures.insert(kind, tex_bg, 4); + *num_done += 1; + } + for spec in self.textures.needed() { + let start = Instant::now(); + let format = if spec.linear { + TextureFormat::Rgba8Unorm + } else { + TextureFormat::Rgba8UnormSrgb + }; + if let Some(mipgen) = self.mip_generation_pipelines.try_get(format) { + if let Some(buf) = self.downloader.try_get(spec.data.clone())? { + let image = ImageReader::new(Cursor::new(buf.0)).with_guessed_format()?; + let image = image.decode()?; + let dims = (image.width(), image.height()); + let image = image.into_rgba8(); + let image = image.into_vec(); + let tex_bg = create_texture( + &self.device, + &self.queue, + &self.layouts.texture, + &image, + dims.0, + dims.1, + format, + Some(&mipgen), + &self.config, + ); + self.textures.insert(spec, tex_bg, image.len()); + debug!( + "texture created (res={}x{}, took {:?})", + dims.0, + dims.1, + start.elapsed() + ); + *num_done += 1; + } + } + } + Ok(()) + } +} + +fn create_texture( + device: &Device, + queue: &Queue, + bgl: &BindGroupLayout, + data: &[u8], + width: u32, + height: u32, + format: TextureFormat, + mipgen: Option<&MipGenerationPipeline>, + config: &GraphicsConfig, +) -> (Arc, Arc) { + let mip_level_count = (width.ilog2().max(4) - 3).min(config.max_mip_count); + + let extent = Extent3d { + depth_or_array_layers: 1, + width, + height, + }; + let texture = device.create_texture(&TextureDescriptor { + label: None, + size: extent, + mip_level_count, + sample_count: 1, + dimension: TextureDimension::D2, + format, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let textureview = texture.create_view(&TextureViewDescriptor::default()); + let sampler = device.create_sampler(&SamplerDescriptor { + address_mode_u: AddressMode::Repeat, + address_mode_v: AddressMode::Repeat, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Linear, + anisotropy_clamp: config.max_anisotropy, + ..Default::default() + }); + let bind_group = device.create_bind_group(&BindGroupDescriptor { + label: None, + layout: &bgl, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&textureview), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&sampler), + }, + ], + }); + + let level_views = (0..mip_level_count) + .map(|mip| { + texture.create_view(&TextureViewDescriptor { + label: Some("mip generation level view"), + format: None, + dimension: None, + aspect: TextureAspect::All, + base_mip_level: mip, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: None, + }) + }) + .collect::>(); + + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + + // TODO why does copy_buffer_to_texture have more restrictive alignment requirements?! + // let upload_buffer = device.create_buffer_init(&BufferInitDescriptor { + // label: Some("texture upload"), + // contents: data, + // usage: BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + // }); + // encoder.copy_buffer_to_texture( + // ImageCopyBuffer { + // buffer: &upload_buffer, + // layout: ImageDataLayout { + // offset: 0, + // bytes_per_row: Some(width * 4), + // rows_per_image: None, + // }, + // }, + // texture.as_image_copy(), + // extent, + // ); + queue.write_texture( + texture.as_image_copy(), + data, + ImageDataLayout { + bytes_per_row: Some(width * 4), + rows_per_image: None, + offset: 0, + }, + extent, + ); + + for level in 1..mip_level_count { + let mip_pipeline = &mipgen.unwrap().pipeline; + let source_view = &level_views[level as usize - 1]; + let target_view = &level_views[level as usize]; + let mip_bind_group = device.create_bind_group(&BindGroupDescriptor { + layout: &mip_pipeline.get_bind_group_layout(0), + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(source_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&sampler), + }, + ], + label: None, + }); + let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: target_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color::WHITE), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + rpass.set_pipeline(&mip_pipeline); + rpass.set_bind_group(0, &mip_bind_group, &[]); + rpass.draw(0..3, 0..1); + } + + queue.submit(Some(encoder.finish())); + + (Arc::new(texture), Arc::new(bind_group)) +} diff --git a/client/src/render/scene/vertex_buffers.rs b/client/src/render/scene/vertex_buffers.rs new file mode 100644 index 0000000..0c34746 --- /dev/null +++ b/client/src/render/scene/vertex_buffers.rs @@ -0,0 +1,138 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use super::{meshops::{generate_normals, generate_tangents, generate_texcoords}, ScenePreparer}; +use anyhow::Result; +use log::debug; +use std::{sync::Arc, time::Instant}; +use wgpu::{ + BufferUsages, + util::{BufferInitDescriptor, DeviceExt}, +}; + +impl ScenePreparer { + pub fn update_vertex_buffers(&self, num_done: &mut usize) -> Result<()> { + for pres in self.index_buffers.needed() { + let start = Instant::now(); + if let Some(buf) = self.downloader.try_get(pres.clone())? { + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("index"), + contents: bytemuck::cast_slice(buf.as_slice()), + usage: BufferUsages::INDEX | BufferUsages::COPY_DST, + }); + self.index_buffers.insert( + pres.clone(), + (Arc::new(buffer), (buf.len() * 3) as u32), + buf.len() * size_of::() * 3, + ); + debug!( + "index buffer created (len={}, took {:?}) {pres}", + buf.len() / size_of::(), + start.elapsed(), + ); + *num_done += 1; + } + } + for pres in self.vertex_buffers.needed() { + let start = Instant::now(); + if let Some(buf) = self.downloader.try_get(pres.clone())? { + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + contents: bytemuck::cast_slice(buf.as_slice()), + label: Some("vertex attribute"), + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + }); + self.vertex_buffers.insert( + pres.clone(), + Arc::new(buffer), + buf.len() * size_of::(), + ); + debug!( + "vertex attribute buffer created (len={}, took {:?}) {pres}", + buf.len() / size_of::(), + start.elapsed() + ); + *num_done += 1; + } + } + + for spec in self.generated_tangent_buffers.needed() { + if let (Some(index), Some(position), texcoord) = ( + self.downloader.try_get(spec.index.clone())?, + self.downloader.try_get(spec.position.clone())?, + spec.texcoord + .clone() + .map(|r| self.downloader.try_get(r)) + .transpose()?, + ) { + let texcoord = match texcoord { + Some(Some(x)) => Some(x), + Some(None) => continue, // tangents provided but still loading + None => None, + }; + let texcoord = texcoord.unwrap_or_else(|| generate_texcoords(&index, &position)); + let tangents = generate_tangents(&index, &position, &texcoord); + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("generated tangent"), + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + contents: bytemuck::cast_slice(tangents.as_slice()), + }); + self.generated_tangent_buffers.insert( + spec, + Arc::new(buffer), + size_of::() * tangents.len() * 3, + ); + } + } + for spec in self.generated_normal_buffers.needed() { + if let (Some(index), Some(position)) = ( + self.downloader.try_get(spec.index.clone())?, + self.downloader.try_get(spec.position.clone())?, + ) { + let normals = generate_normals(&index, &position); + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("generated normal"), + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + contents: bytemuck::cast_slice(normals.as_slice()), + }); + self.generated_normal_buffers.insert( + spec, + Arc::new(buffer), + size_of::() * normals.len() * 3, + ); + } + } + for spec in self.generated_texcoord_buffers.needed() { + if let (Some(index), Some(position)) = ( + self.downloader.try_get(spec.index.clone())?, + self.downloader.try_get(spec.position.clone())?, + ) { + let texcoords = generate_texcoords(&index, &position); + let buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("generated texcoord"), + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + contents: bytemuck::cast_slice(texcoords.as_slice()), + }); + self.generated_texcoord_buffers.insert( + spec, + Arc::new(buffer), + size_of::() * texcoords.len() * 3, + ); + } + } + + Ok(()) + } +} diff --git a/client/src/renderer.rs b/client/src/renderer.rs deleted file mode 100644 index c69f4bf..0000000 --- a/client/src/renderer.rs +++ /dev/null @@ -1,239 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use crate::{ - camera::Camera, download::Downloader, interfaces::profiler::TimingProfiler, - scene_prepare::ScenePreparer, scene_render::ScenePipeline, state::InputState, ui::UiRenderer, -}; -use anyhow::{Result, anyhow}; -use log::{info, warn}; -use pollster::FutureExt; -use std::{ - mem::swap, - sync::{Arc, Mutex}, - thread::{sleep, spawn}, - time::Duration, -}; -use weareshared::tree::SceneTree; -use wgpu::{ - AdapterInfo, Backends, CommandEncoderDescriptor, Device, DeviceDescriptor, Extent3d, Features, - Instance, InstanceDescriptor, Limits, MaintainBase, PowerPreference, Queue, - RequestAdapterOptions, Surface, SurfaceConfiguration, TextureDescriptor, TextureDimension, - TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, -}; -use winit::window::Window; - -pub struct Renderer<'a> { - surface: Surface<'a>, - queue: Arc, - device: Arc, - surface_configuration: SurfaceConfiguration, - scene_pipeline: ScenePipeline, - pub ui_renderer: UiRenderer, - pub scene_prepare: Arc, - surface_needs_reconfigure: bool, - depth: TextureView, - pub timing: TimingProfiler, - pub timing_submit: Arc>, - pub adapter_info: Arc, -} -impl<'a> Renderer<'a> { - pub fn new(window: &'a Window, downloader: Arc) -> Result { - info!("wgpu init"); - let instance = Instance::new(InstanceDescriptor { - backends: Backends::all(), - ..Default::default() - }); - - let surface = instance.create_surface(window)?; - let adapter = instance - .request_adapter(&RequestAdapterOptions { - compatible_surface: Some(&surface), - power_preference: PowerPreference::HighPerformance, - ..Default::default() - }) - .block_on() - .ok_or(anyhow!("no adapter found"))?; - - let (device, queue) = adapter - .request_device( - &DeviceDescriptor { - required_features: Features::PUSH_CONSTANTS, - required_limits: Limits { - max_push_constant_size: 112, - max_vertex_buffers: 16, - ..Limits::default() - }, - ..Default::default() - }, - None, - ) - .block_on()?; - - let surface_configuration = surface - .get_default_config(&adapter, 256, 256) - .ok_or(anyhow!("no default config"))?; - - surface.configure(&device, &surface_configuration); - - let device = Arc::new(device); - let queue = Arc::new(queue); - - let scene_prepare = Arc::new(ScenePreparer::new( - device.clone(), - queue.clone(), - surface_configuration.format, - )); - - let ui_renderer = - UiRenderer::new(device.clone(), queue.clone(), surface_configuration.format); - - let depth = device.create_texture(&TextureDescriptor { - label: None, - size: Extent3d { - height: 256, - width: 256, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Depth32Float, - usage: TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - let depth = depth.create_view(&TextureViewDescriptor::default()); - - // TODO multithreading introduces double-loading some resources. fix that before increasing thread count - for _ in 0..1 { - let scene_prepare = scene_prepare.clone(); - let downloader = downloader.clone(); - spawn(move || { - loop { - let ndone = scene_prepare.update(&downloader).unwrap(); - if ndone == 0 { - sleep(Duration::from_millis(50)); - } - } - }); - } - - let adapter_info = Arc::new(adapter.get_info()); - - Ok(Self { - scene_pipeline: ScenePipeline, - scene_prepare, - surface, - adapter_info, - depth, - device, - queue, - surface_configuration, - ui_renderer, - surface_needs_reconfigure: false, - timing: Default::default(), - timing_submit: Default::default(), - }) - } - - pub fn resize(&mut self, width: u32, height: u32) { - self.surface_configuration.width = width; - self.surface_configuration.height = height; - self.surface - .configure(&self.device, &self.surface_configuration); - - self.depth = self - .device - .create_texture(&TextureDescriptor { - label: None, - size: Extent3d { - height, - width, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Depth32Float, - usage: TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }) - .create_view(&TextureViewDescriptor::default()); - } - - pub fn draw( - &mut self, - scene: &SceneTree, - camera: &Camera, - input_state: &mut InputState, - ) -> Result<()> { - self.timing.begin("prepare"); - if self.surface_needs_reconfigure { - self.surface - .configure(&self.device, &self.surface_configuration); - self.surface_needs_reconfigure = false - } - let target = self.surface.get_current_texture()?; - if target.suboptimal { - warn!("suboptimal swapchain texture"); - self.surface_needs_reconfigure = true; - } - let target_view = target - .texture - .create_view(&TextureViewDescriptor::default()); - - let mut commands = self - .device - .create_command_encoder(&CommandEncoderDescriptor { label: None }); - - let projection = camera.to_matrix(); - - self.timing.checkpoint("draw scene"); - self.scene_pipeline.draw( - &mut commands, - &target_view, - &self.depth, - scene, - &self.scene_prepare.prefabs, - projection, - ); - - self.timing.checkpoint("draw ui"); - self.ui_renderer.draw( - &mut commands, - &target_view, - &self.depth, - projection, - input_state, - &self.surface_configuration, - ); - - self.timing.checkpoint("submit"); - let i = self.queue.submit(Some(commands.finish())); - self.timing.checkpoint("poll"); - self.device.poll(MaintainBase::WaitForSubmissionIndex(i)); - - self.timing.checkpoint("present"); - target.present(); - - self.timing.checkpoint(""); - - let mut ts = self.timing_submit.lock().unwrap(); - swap(&mut *ts, &mut self.timing); - - Ok(()) - } -} diff --git a/client/src/scene_prepare.rs b/client/src/scene_prepare.rs index d4535a1..e69de29 100644 --- a/client/src/scene_prepare.rs +++ b/client/src/scene_prepare.rs @@ -1,1030 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use crate::{ - armature::RArmature, - download::Downloader, - meshops::{generate_normals, generate_tangents, generate_texcoords}, - shaders::SceneShaders, -}; -use anyhow::Result; -use bytemuck::{Pod, Zeroable}; -use egui::{Grid, Widget}; -use glam::{UVec3, UVec4, Vec2, Vec3, Vec3A, uvec3, uvec4}; -use humansize::DECIMAL; -use image::ImageReader; -use log::{debug, trace}; -use std::{ - collections::{HashMap, HashSet}, - hash::Hash, - io::Cursor, - marker::PhantomData, - sync::{Arc, RwLock}, - time::Instant, -}; -use weareshared::{ - Affine3A, - packets::Resource, - resources::{Image, MeshPart, Prefab}, -}; -use wgpu::{ - AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, - Buffer, BufferBindingType, BufferUsages, Color, ColorTargetState, ColorWrites, - CommandEncoderDescriptor, CompareFunction, DepthBiasState, DepthStencilState, Device, Extent3d, - Face, FilterMode, FragmentState, FrontFace, ImageDataLayout, LoadOp, MultisampleState, - Operations, PipelineCompilationOptions, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, - PrimitiveTopology, PushConstantRange, Queue, RenderPassColorAttachment, RenderPassDescriptor, - RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, ShaderStages, - StencilState, StoreOp, Texture, TextureAspect, TextureDescriptor, TextureDimension, - TextureFormat, TextureSampleType, TextureUsages, TextureViewDescriptor, TextureViewDimension, - VertexAttribute, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, include_wgsl, - util::{BufferInitDescriptor, DeviceExt}, -}; - -pub struct DemandMap { - inner: RwLock>, -} -struct DemandMapState { - values: HashMap, - needed: HashSet, - size_metric: usize, -} -impl DemandMap { - pub fn new() -> Self { - Self { - inner: DemandMapState { - needed: HashSet::new(), - values: HashMap::new(), - size_metric: 0, - } - .into(), - } - } - pub fn needed(&self) -> Vec { - self.inner.read().unwrap().needed.iter().cloned().collect() - } - pub fn insert(&self, key: K, value: V, size: usize) { - let mut s = self.inner.write().unwrap(); - s.needed.remove(&key); - s.values.insert(key, value); - s.size_metric += size; - } - pub fn try_get(&self, key: K) -> Option { - let mut s = self.inner.write().unwrap(); - if let Some(k) = s.values.get(&key) { - Some(k.to_owned()) - } else { - s.needed.insert(key); - None - } - } -} - -struct GraphicsConfig { - max_anisotropy: u16, - max_mip_count: u32, -} - -pub struct ScenePreparer { - device: Arc, - queue: Arc, - layouts: SceneBgLayouts, - shaders: SceneShaders, - render_format: TextureFormat, - config: GraphicsConfig, - - textures: DemandMap, Arc)>, - placeholder_textures: DemandMap, Arc)>, - index_buffers: DemandMap>, (Arc, u32)>, - vertex_buffers: DemandMap>, Arc>, - generated_tangent_buffers: DemandMap>, - generated_normal_buffers: DemandMap>, - generated_texcoord_buffers: DemandMap>, - mesh_parts: DemandMap, Arc>, - materials: DemandMap>, - pipelines: DemandMap>, - mip_generation_pipelines: DemandMap>, - pub prefabs: DemandMap, Arc>, -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct PipelineSpec { - format: TextureFormat, - skin: bool, - backface_culling: bool, -} - -pub struct RPrefab(pub Vec<(Affine3A, Arc)>); -pub struct RMeshPart { - pub pipeline: Arc, - pub index_count: u32, - pub index: Arc, - pub va_position: Arc, - pub va_normal: Arc, - pub va_tangent: Arc, - pub va_texcoord: Arc, - pub tex_albedo: Arc, - pub tex_normal: Arc, - pub material: Arc, - pub double_sided: bool, - - pub va_joint_index: Option>, - pub va_joint_weight: Option>, - pub joint_uniform: Option>, -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -struct TextureSpec { - data: Resource>, - linear: bool, -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -struct TangentBufferSpec { - index: Resource>, - position: Resource>, - texcoord: Option>>, -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -struct NormalBufferSpec { - index: Resource>, - position: Resource>, -} -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -struct TexcoordBufferSpec { - index: Resource>, - position: Resource>, -} - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -enum TextureIdentityKind { - Normal, - Multiply, -} - -#[derive(Debug, Clone, Copy, Pod, Zeroable, Hash, PartialEq, Eq)] -#[repr(C)] -struct Material { - roughness: u32, - metallic: u32, - _pad1: [u32; 2], - albedo_alpha: UVec4, - emission: UVec3, - _pad2: u32, -} - -impl ScenePreparer { - pub fn new(device: Arc, queue: Arc, render_format: TextureFormat) -> Self { - Self { - // TODO normal mipmap requires linear texture, also demand map? - render_format, - config: GraphicsConfig { - max_anisotropy: 16, - max_mip_count: 16, - }, - layouts: SceneBgLayouts::load(&device), - shaders: SceneShaders::load(&device), - device, - queue, - index_buffers: DemandMap::new(), - vertex_buffers: DemandMap::new(), - mesh_parts: DemandMap::new(), - prefabs: DemandMap::new(), - textures: DemandMap::new(), - placeholder_textures: DemandMap::new(), - generated_tangent_buffers: DemandMap::new(), - generated_normal_buffers: DemandMap::new(), - generated_texcoord_buffers: DemandMap::new(), - materials: DemandMap::new(), - pipelines: DemandMap::new(), - mip_generation_pipelines: DemandMap::new(), - } - } - pub fn update(&self, dls: &Downloader) -> Result { - let mut num_done = 0; - for pres in self.prefabs.needed() { - if let Some(prefab) = dls.try_get(pres.clone())? { - let mut rprefab = RPrefab(Vec::new()); - for (aff, partres) in &prefab.mesh { - if let Some(part) = self.mesh_parts.try_get(partres.clone()) { - rprefab.0.push((*aff, part.clone())); - } - } - if rprefab.0.len() == prefab.mesh.len() { - self.prefabs.insert(pres.clone(), Arc::new(rprefab), 0); - debug!("prefab created ({pres})"); - num_done += 1; - } - } - } - for pres in self.index_buffers.needed() { - let start = Instant::now(); - if let Some(buf) = dls.try_get(pres.clone())? { - let buffer = self.device.create_buffer_init(&BufferInitDescriptor { - label: Some("index"), - contents: bytemuck::cast_slice(buf.as_slice()), - usage: BufferUsages::INDEX | BufferUsages::COPY_DST, - }); - self.index_buffers.insert( - pres.clone(), - (Arc::new(buffer), (buf.len() * 3) as u32), - buf.len() * size_of::() * 3, - ); - debug!( - "index buffer created (len={}, took {:?}) {pres}", - buf.len() / size_of::(), - start.elapsed(), - ); - num_done += 1; - } - } - for pres in self.vertex_buffers.needed() { - let start = Instant::now(); - if let Some(buf) = dls.try_get(pres.clone())? { - let buffer = self.device.create_buffer_init(&BufferInitDescriptor { - contents: bytemuck::cast_slice(buf.as_slice()), - label: Some("vertex attribute"), - usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, - }); - self.vertex_buffers.insert( - pres.clone(), - Arc::new(buffer), - buf.len() * size_of::(), - ); - debug!( - "vertex attribute buffer created (len={}, took {:?}) {pres}", - buf.len() / size_of::(), - start.elapsed() - ); - num_done += 1; - } - } - for format in self.mip_generation_pipelines.needed() { - self.mip_generation_pipelines.insert( - format, - Arc::new(MipGenerationPipeline::load(&self.device, format)), - 0, - ); - } - for spec in self.textures.needed() { - let start = Instant::now(); - let format = if spec.linear { - TextureFormat::Rgba8Unorm - } else { - TextureFormat::Rgba8UnormSrgb - }; - if let Some(mipgen) = self.mip_generation_pipelines.try_get(format) { - if let Some(buf) = dls.try_get(spec.data.clone())? { - let image = ImageReader::new(Cursor::new(buf.0)).with_guessed_format()?; - let image = image.decode()?; - let dims = (image.width(), image.height()); - let image = image.into_rgba8(); - let image = image.into_vec(); - let tex_bg = create_texture( - &self.device, - &self.queue, - &self.layouts.texture, - &image, - dims.0, - dims.1, - format, - Some(&mipgen), - &self.config, - ); - self.textures.insert(spec, tex_bg, image.len()); - debug!( - "texture created (res={}x{}, took {:?})", - dims.0, - dims.1, - start.elapsed() - ); - num_done += 1; - } - } - } - for kind in self.placeholder_textures.needed() { - let (linear, color) = match kind { - TextureIdentityKind::Normal => (true, [128, 128, 255, 255]), - TextureIdentityKind::Multiply => (false, [255, 255, 255, 255]), - }; - let tex_bg = create_texture( - &self.device, - &self.queue, - &self.layouts.texture, - &color, - 1, - 1, - if linear { - TextureFormat::Rgba8Unorm - } else { - TextureFormat::Rgba8UnormSrgb - }, - None, - &self.config, - ); - self.placeholder_textures.insert(kind, tex_bg, 4); - num_done += 1; - } - for spec in self.generated_tangent_buffers.needed() { - if let (Some(index), Some(position), texcoord) = ( - dls.try_get(spec.index.clone())?, - dls.try_get(spec.position.clone())?, - spec.texcoord.clone().map(|r| dls.try_get(r)).transpose()?, - ) { - let texcoord = match texcoord { - Some(Some(x)) => Some(x), - Some(None) => continue, // tangents provided but still loading - None => None, - }; - let texcoord = texcoord.unwrap_or_else(|| generate_texcoords(&index, &position)); - let tangents = generate_tangents(&index, &position, &texcoord); - let buffer = self.device.create_buffer_init(&BufferInitDescriptor { - label: Some("generated tangent"), - usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, - contents: bytemuck::cast_slice(tangents.as_slice()), - }); - self.generated_tangent_buffers.insert( - spec, - Arc::new(buffer), - size_of::() * tangents.len() * 3, - ); - } - } - for spec in self.generated_normal_buffers.needed() { - if let (Some(index), Some(position)) = ( - dls.try_get(spec.index.clone())?, - dls.try_get(spec.position.clone())?, - ) { - let normals = generate_normals(&index, &position); - let buffer = self.device.create_buffer_init(&BufferInitDescriptor { - label: Some("generated normal"), - usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, - contents: bytemuck::cast_slice(normals.as_slice()), - }); - self.generated_normal_buffers.insert( - spec, - Arc::new(buffer), - size_of::() * normals.len() * 3, - ); - } - } - for spec in self.generated_texcoord_buffers.needed() { - if let (Some(index), Some(position)) = ( - dls.try_get(spec.index.clone())?, - dls.try_get(spec.position.clone())?, - ) { - let texcoords = generate_texcoords(&index, &position); - let buffer = self.device.create_buffer_init(&BufferInitDescriptor { - label: Some("generated texcoord"), - usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, - contents: bytemuck::cast_slice(texcoords.as_slice()), - }); - self.generated_texcoord_buffers.insert( - spec, - Arc::new(buffer), - size_of::() * texcoords.len() * 3, - ); - } - } - for spec in self.materials.needed() { - let buffer = self.device.create_buffer_init(&BufferInitDescriptor { - label: Some("material props"), - usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, - contents: bytemuck::cast_slice(&[spec]), - }); - let bind_group = self.device.create_bind_group(&BindGroupDescriptor { - label: Some("material"), - layout: &self.layouts.material, - entries: &[BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); - self.materials.insert(spec, Arc::new(bind_group), 0); - } - for spec in self.pipelines.needed() { - self.pipelines.insert( - spec.clone(), - Arc::new(spec.create(&self.device, &self.layouts, &self.shaders)), - 0, - ); - } - for pres in self.mesh_parts.needed() { - let start = Instant::now(); - if let Some(part) = dls.try_get(pres.clone())? { - if let (Some(indexres), Some(positionres)) = (part.index, part.va_position) { - let index = self.index_buffers.try_get(indexres.clone()); - let position = self - .vertex_buffers - .try_get(Resource(positionres.0, PhantomData)); - - let normal = if let Some(res) = part.va_normal.clone() { - self.vertex_buffers.try_get(Resource(res.0, PhantomData)) - } else { - self.generated_normal_buffers.try_get(NormalBufferSpec { - index: indexres.clone(), - position: Resource(positionres.0, PhantomData), - }) - }; - - let texcoord = if let Some(res) = part.va_texcoord.clone() { - self.vertex_buffers.try_get(Resource(res.0, PhantomData)) - } else { - self.generated_texcoord_buffers.try_get(TexcoordBufferSpec { - index: indexres.clone(), - position: Resource(positionres.0, PhantomData), - }) - }; - let tangent = if let Some(res) = part.va_tangent.clone() { - self.vertex_buffers.try_get(Resource(res.0, PhantomData)) - } else { - self.generated_tangent_buffers.try_get(TangentBufferSpec { - index: indexres, - position: Resource(positionres.0, PhantomData), - texcoord: part.va_texcoord, - }) - }; - - let joint_weight = if let Some(res) = part.va_joint_weight.clone() { - self.vertex_buffers - .try_get(Resource(res.0, PhantomData)) - .map(Some) - } else { - Some(None) - }; - let joint_index = if let Some(res) = part.va_joint_index.clone() { - self.vertex_buffers - .try_get(Resource(res.0, PhantomData)) - .map(Some) - } else { - Some(None) - }; - - let mut tex_albedo = None; - if let Some(albedores) = part.tex_albedo { - if let Some((_tex, bg)) = self.textures.try_get(TextureSpec { - data: albedores, - linear: false, - }) { - tex_albedo = Some(bg) - } - } else { - if let Some((_tex, bg)) = self - .placeholder_textures - .try_get(TextureIdentityKind::Multiply) - { - tex_albedo = Some(bg) - } - } - let mut tex_normal = None; - if let Some(normalres) = part.tex_normal { - if let Some((_tex, bg)) = self.textures.try_get(TextureSpec { - data: normalres, - linear: true, - }) { - tex_normal = Some(bg) - } - } else { - if let Some((_tex, bg)) = self - .placeholder_textures - .try_get(TextureIdentityKind::Normal) - { - tex_normal = Some(bg) - } - } - - let material = self.materials.try_get({ - let albedo = part.g_albedo.unwrap_or(Vec3A::ONE); - let emission = part.g_emission.unwrap_or(Vec3A::ONE); - Material { - roughness: part.g_roughness.unwrap_or(1.).to_bits(), - metallic: part.g_metallic.unwrap_or(0.).to_bits(), - _pad1: [0, 0], - albedo_alpha: uvec4( - albedo.x.to_bits(), - albedo.y.to_bits(), - albedo.z.to_bits(), - part.g_alpha.unwrap_or(1.).to_bits(), - ), - emission: uvec3( - emission.x.to_bits(), - emission.y.to_bits(), - emission.z.to_bits(), - ), - _pad2: 0, - } - }); - - let armature = if let Some(res) = part.armature.clone() { - Some(dls.try_get(res)?) - } else { - Some(None) - }; - - let pipeline = self.pipelines.try_get(PipelineSpec { - format: self.render_format, - skin: false, - backface_culling: part.g_double_sided.is_none(), - }); - - if let ( - Some(pipeline), - Some((index, index_count)), - Some(va_normal), - Some(va_tangent), - Some(va_texcoord), - Some(va_position), - Some(va_joint_index), - Some(va_joint_weight), - Some(armature), - Some(tex_normal), - Some(tex_albedo), - Some(material), - ) = ( - pipeline, - index, - normal, - tangent, - texcoord, - position, - joint_index, - joint_weight, - armature, - tex_normal, - tex_albedo, - material, - ) { - let double_sided = part.g_double_sided.is_some(); - - let joint_uniform = if let Some(a) = armature { - let ra = RArmature::new(&self.device, a); - Some(ra.joint_mat_uniform_buffer.clone()) - } else { - None - }; - - debug!("part created (took {:?}) {pres}", start.elapsed()); - self.mesh_parts.insert( - pres, - Arc::new(RMeshPart { - pipeline, - index_count, - index, - va_normal, - va_tangent, - va_position, - va_texcoord, - va_joint_index, - va_joint_weight, - tex_albedo, - tex_normal, - material, - double_sided, - joint_uniform, - }), - 0, - ); - num_done += 1; - } - } - } - } - self.print_missing(); - Ok(num_done) - } -} - -struct MipGenerationPipeline { - pipeline: RenderPipeline, -} -impl MipGenerationPipeline { - pub fn load(device: &Device, format: TextureFormat) -> Self { - let shader = device.create_shader_module(include_wgsl!("shaders/texture_copy.wgsl")); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("mip generator"), - layout: None, - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - compilation_options: Default::default(), - buffers: &[], - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - compilation_options: Default::default(), - targets: &[Some(ColorTargetState { - format, - blend: None, - write_mask: ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - cache: None, - }); - Self { pipeline } - } -} - -fn create_texture( - device: &Device, - queue: &Queue, - bgl: &BindGroupLayout, - data: &[u8], - width: u32, - height: u32, - format: TextureFormat, - mipgen: Option<&MipGenerationPipeline>, - config: &GraphicsConfig, -) -> (Arc, Arc) { - let mip_level_count = (width.ilog2().max(4) - 3).min(config.max_mip_count); - - let extent = Extent3d { - depth_or_array_layers: 1, - width, - height, - }; - let texture = device.create_texture(&TextureDescriptor { - label: None, - size: extent, - mip_level_count, - sample_count: 1, - dimension: TextureDimension::D2, - format, - usage: TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_DST - | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - let textureview = texture.create_view(&TextureViewDescriptor::default()); - let sampler = device.create_sampler(&SamplerDescriptor { - address_mode_u: AddressMode::Repeat, - address_mode_v: AddressMode::Repeat, - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - mipmap_filter: FilterMode::Linear, - anisotropy_clamp: config.max_anisotropy, - ..Default::default() - }); - let bind_group = device.create_bind_group(&BindGroupDescriptor { - label: None, - layout: &bgl, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&textureview), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&sampler), - }, - ], - }); - - let level_views = (0..mip_level_count) - .map(|mip| { - texture.create_view(&TextureViewDescriptor { - label: Some("mip generation level view"), - format: None, - dimension: None, - aspect: TextureAspect::All, - base_mip_level: mip, - mip_level_count: Some(1), - base_array_layer: 0, - array_layer_count: None, - }) - }) - .collect::>(); - - let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None }); - - // TODO why does copy_buffer_to_texture have more restrictive alignment requirements?! - // let upload_buffer = device.create_buffer_init(&BufferInitDescriptor { - // label: Some("texture upload"), - // contents: data, - // usage: BufferUsages::COPY_DST | BufferUsages::COPY_SRC, - // }); - // encoder.copy_buffer_to_texture( - // ImageCopyBuffer { - // buffer: &upload_buffer, - // layout: ImageDataLayout { - // offset: 0, - // bytes_per_row: Some(width * 4), - // rows_per_image: None, - // }, - // }, - // texture.as_image_copy(), - // extent, - // ); - queue.write_texture( - texture.as_image_copy(), - data, - ImageDataLayout { - bytes_per_row: Some(width * 4), - rows_per_image: None, - offset: 0, - }, - extent, - ); - - for level in 1..mip_level_count { - let mip_pipeline = &mipgen.unwrap().pipeline; - let source_view = &level_views[level as usize - 1]; - let target_view = &level_views[level as usize]; - let mip_bind_group = device.create_bind_group(&BindGroupDescriptor { - layout: &mip_pipeline.get_bind_group_layout(0), - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(source_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&sampler), - }, - ], - label: None, - }); - let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor { - label: None, - color_attachments: &[Some(RenderPassColorAttachment { - view: target_view, - resolve_target: None, - ops: Operations { - load: LoadOp::Clear(Color::WHITE), - store: StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - rpass.set_pipeline(&mip_pipeline); - rpass.set_bind_group(0, &mip_bind_group, &[]); - rpass.draw(0..3, 0..1); - } - - queue.submit(Some(encoder.finish())); - - (Arc::new(texture), Arc::new(bind_group)) -} - -pub struct SceneBgLayouts { - pub texture: BindGroupLayout, - pub material: BindGroupLayout, - pub joints: BindGroupLayout, -} -impl SceneBgLayouts { - pub fn load(device: &Device) -> Self { - Self { - texture: device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ - BindGroupLayoutEntry { - binding: 0, - count: None, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - multisampled: false, - }, - }, - BindGroupLayoutEntry { - binding: 1, - count: None, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - }, - ], - label: None, - }), - material: device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - count: None, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - }], - label: None, - }), - joints: device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - count: None, - visibility: ShaderStages::VERTEX, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - }], - label: None, - }), - } - } -} - -impl PipelineSpec { - pub fn create( - &self, - device: &Device, - layouts: &SceneBgLayouts, - shaders: &SceneShaders, - ) -> RenderPipeline { - let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[&layouts.texture, &layouts.texture, &layouts.material], - push_constant_ranges: &[PushConstantRange { - // 4x4 view projections - // 3x3(+1 pad) model basis - range: 0..((4 * 4 + 3 * 4) * size_of::() as u32), - stages: ShaderStages::VERTEX, - }], - }); - device.create_render_pipeline(&RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - fragment: Some(FragmentState { - module: &shaders.fragment_pbr, - entry_point: Some("main"), - targets: &[Some(ColorTargetState { - blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), - format: self.format, - write_mask: ColorWrites::all(), - })], - compilation_options: PipelineCompilationOptions::default(), - }), - vertex: VertexState { - module: if self.skin { - &shaders.vertex_world_skin - } else { - &shaders.vertex_world - }, - entry_point: Some("main"), - buffers: &[ - // position - VertexBufferLayout { - step_mode: VertexStepMode::Vertex, - array_stride: 3 * size_of::() as u64, - attributes: &[VertexAttribute { - format: VertexFormat::Float32x3, - offset: 0, - shader_location: 0, - }], - }, - // normal - VertexBufferLayout { - step_mode: VertexStepMode::Vertex, - array_stride: 3 * size_of::() as u64, - attributes: &[VertexAttribute { - format: VertexFormat::Float32x3, - offset: 0, - shader_location: 1, - }], - }, - // tangent - VertexBufferLayout { - step_mode: VertexStepMode::Vertex, - array_stride: 3 * size_of::() as u64, - attributes: &[VertexAttribute { - format: VertexFormat::Float32x3, - offset: 0, - shader_location: 2, - }], - }, - // texcoord - VertexBufferLayout { - step_mode: VertexStepMode::Vertex, - array_stride: 2 * size_of::() as u64, - attributes: &[VertexAttribute { - format: VertexFormat::Float32x2, - offset: 0, - shader_location: 3, - }], - }, - ], - compilation_options: PipelineCompilationOptions::default(), - }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - front_face: FrontFace::Ccw, - cull_mode: if self.backface_culling { - Some(Face::Back) - } else { - None - }, - polygon_mode: PolygonMode::Fill, - ..Default::default() - }, - depth_stencil: Some(DepthStencilState { - depth_write_enabled: true, - depth_compare: CompareFunction::Less, - format: TextureFormat::Depth32Float, - bias: DepthBiasState::default(), - stencil: StencilState::default(), - }), - multisample: MultisampleState::default(), - multiview: None, - cache: None, - }) - } -} - -impl Widget for &DemandMap { - fn ui(self, ui: &mut egui::Ui) -> egui::Response { - let state = self.inner.read().unwrap(); - ui.label(state.needed.len().to_string()); - ui.label(state.values.len().to_string()); - ui.label(humansize::format_size(state.size_metric, DECIMAL)); - ui.end_row(); - ui.response() - } -} -impl ScenePreparer { - pub fn print_missing(&self) { - fn visit(name: &str, m: &DemandMap) - where - K: Clone, - K: Hash, - K: std::cmp::Eq, - V: Clone, - { - let nl = m.needed().len(); - if nl > 0 { - trace!("{name}: need {nl}") - } - } - visit("prefabs", &self.prefabs); - visit("mesh_parts", &self.mesh_parts); - visit("vertex_buffers", &self.vertex_buffers); - visit("index_buffers", &self.index_buffers); - visit("placeholder_textures", &self.placeholder_textures); - visit("generated_tangent_buffers", &self.generated_tangent_buffers); - visit("generated_normal_buffers", &self.generated_normal_buffers); - visit( - "generated_texcoord_buffers", - &self.generated_texcoord_buffers, - ); - visit("textures", &self.textures); - visit("materials", &self.materials); - visit("pipelines", &self.pipelines); - } -} - -impl Widget for &ScenePreparer { - fn ui(self, ui: &mut egui::Ui) -> egui::Response { - Grid::new("sp") - .num_columns(4) - .show(ui, |ui| { - ui.label("prefabs"); - self.prefabs.ui(ui); - ui.label("mesh_parts"); - self.mesh_parts.ui(ui); - ui.label("vertex_buffers"); - self.vertex_buffers.ui(ui); - ui.label("index_buffers"); - self.index_buffers.ui(ui); - ui.label("placeholder_textures"); - self.placeholder_textures.ui(ui); - ui.label("generated_tangent_buffers"); - self.generated_tangent_buffers.ui(ui); - ui.label("generated_normal_buffers"); - self.generated_normal_buffers.ui(ui); - ui.label("generated_texcoord_buffers"); - self.generated_texcoord_buffers.ui(ui); - ui.label("textures"); - self.textures.ui(ui); - ui.label("materials"); - self.materials.ui(ui); - ui.label("pipelines"); - self.pipelines.ui(ui); - }) - .response - } -} diff --git a/client/src/scene_render.rs b/client/src/scene_render.rs deleted file mode 100644 index 2720e33..0000000 --- a/client/src/scene_render.rs +++ /dev/null @@ -1,104 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use crate::scene_prepare::{DemandMap, RPrefab}; -use glam::{EulerRot, Mat3, Mat4}; -use std::sync::Arc; -use weareshared::{packets::Resource, resources::Prefab, tree::SceneTree}; -use wgpu::{ - Color, CommandEncoder, IndexFormat, LoadOp, Operations, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, ShaderStages, StoreOp, TextureView, -}; - -pub struct ScenePipeline; - -impl ScenePipeline { - pub fn draw( - &mut self, - commands: &mut CommandEncoder, - target: &TextureView, - depth: &TextureView, - scene: &SceneTree, - prefabs: &DemandMap, Arc>, - projection: Mat4, - ) { - let mut rpass = commands.begin_render_pass(&RenderPassDescriptor { - label: None, - color_attachments: &[Some(RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: Operations { - store: StoreOp::Store, - load: LoadOp::Clear(Color { - r: 0.01, - g: 0.01, - b: 0.01, - a: 1., - }), - }, - })], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth, - depth_ops: Some(Operations { - load: LoadOp::Clear(1.), - store: StoreOp::Store, - }), - stencil_ops: None, - }), - ..Default::default() - }); - - for ob in scene.objects.values() { - let prefab_projection = projection - * Mat4::from_translation(ob.pos.into()) - * Mat4::from_mat3(Mat3::from_euler( - EulerRot::YXZ, - ob.rot.x, - ob.rot.y, - ob.rot.z, - )); - if let Some(prefab) = prefabs.try_get(ob.res.clone()) { - for (affine, part) in &prefab.0 { - let part_projection = prefab_projection - * Mat4::from_translation(affine.translation.into()) - * Mat4::from_mat3a(affine.matrix3); - let projection = part_projection.to_cols_array().map(|v| v.to_le_bytes()); - let mb = affine.matrix3.to_cols_array(); // TODO apply object rotation - // add padding for gpu mat3x3 repr - let model_basis = [ - mb[0], mb[1], mb[2], 0., // - mb[3], mb[4], mb[5], 0., // - mb[6], mb[7], mb[8], 0., // - ]; - let model_basis = bytemuck::cast_slice(&model_basis); - - rpass.set_pipeline(&part.pipeline); - rpass.set_bind_group(0, &*part.tex_albedo, &[]); - rpass.set_bind_group(1, &*part.tex_normal, &[]); - rpass.set_bind_group(2, &*part.material, &[]); - rpass.set_push_constants(ShaderStages::VERTEX, 0, projection.as_flattened()); - rpass.set_push_constants(ShaderStages::VERTEX, 64, model_basis); - rpass.set_index_buffer(part.index.slice(..), IndexFormat::Uint32); - rpass.set_vertex_buffer(0, part.va_position.slice(..)); - rpass.set_vertex_buffer(1, part.va_normal.slice(..)); - rpass.set_vertex_buffer(2, part.va_tangent.slice(..)); - rpass.set_vertex_buffer(3, part.va_texcoord.slice(..)); - rpass.draw_indexed(0..part.index_count, 0, 0..1); - } - } - } - } -} diff --git a/client/src/state.rs b/client/src/state.rs index c8b4921..d132e3e 100644 --- a/client/src/state.rs +++ b/client/src/state.rs @@ -15,13 +15,7 @@ along with this program. If not, see . */ use crate::{ - audio::Audio, - camera::Camera, - download::Downloader, - interfaces::{InterfaceData, ui_selector}, - network::Network, - renderer::Renderer, - ui::UiEvent, + audio::Audio, camera::Camera, download::Downloader, interfaces::{ui_selector, InterfaceData}, network::Network, render::Renderer, ui::UiEvent }; use anyhow::{Context, Result}; use glam::{Vec2, Vec3}; -- cgit v1.2.3-70-g09d2