diff --git a/Cargo.lock b/Cargo.lock index e9a1b583..68ae0ba1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1136,6 +1136,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1463,6 +1469,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "intel_tex_2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd699c0e6adcac28c06db24a220c834c8ec811e0d2d80f0261bb14e01737b4dc" +dependencies = [ + "ispc_rt", +] + [[package]] name = "interface_procedural" version = "0.1.0" @@ -1489,6 +1504,16 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "ispc_rt" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d30e08ddfd6fe26c3ee2e856dff022420cd0514ed16f5ccf2dc3e1d5cae578" +dependencies = [ + "libc", + "num_cpus", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1603,6 +1628,7 @@ dependencies = [ "glidesort", "hashbrown", "image", + "intel_tex_2", "korangar_audio", "korangar_debug", "korangar_interface", @@ -2133,6 +2159,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -2581,7 +2617,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 38e6b211..53ca2010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ flate2 = { version = "1", default-features = false } glidesort = "0.1" hashbrown = "0.15" image = { version = "0.25", default-features = false } +intel_tex_2 = "0.4" kira = { version = "0.9", default-features = false } korangar_audio = { path = "korangar_audio" } korangar_debug = { path = "korangar_debug" } diff --git a/korangar/Cargo.toml b/korangar/Cargo.toml index e200621b..3f7cb3b0 100644 --- a/korangar/Cargo.toml +++ b/korangar/Cargo.toml @@ -16,6 +16,7 @@ flate2 = { workspace = true, features = ["zlib-rs"] } hashbrown = { workspace = true } glidesort = { workspace = true } image = { workspace = true, features = ["bmp", "jpeg", "png", "tga", "rayon"] } +intel_tex_2 = { workspace = true } korangar_audio = { workspace = true } korangar_debug = { workspace = true, optional = true } korangar_interface = { workspace = true, features = ["serde", "cgmath"] } diff --git a/korangar/src/graphics/capabilities.rs b/korangar/src/graphics/capabilities.rs index a45dc38a..afebf1a0 100644 --- a/korangar/src/graphics/capabilities.rs +++ b/korangar/src/graphics/capabilities.rs @@ -17,6 +17,7 @@ pub struct Capabilities { bindless: bool, multidraw_indirect: bool, clamp_to_border: bool, + texture_compression: bool, #[cfg(feature = "debug")] polygon_mode_line: bool, required_features: Features, @@ -37,6 +38,7 @@ impl Capabilities { bindless: false, multidraw_indirect: false, clamp_to_border: false, + texture_compression: false, #[cfg(feature = "debug")] polygon_mode_line: false, required_features: Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, @@ -58,6 +60,7 @@ impl Capabilities { adapter_features, Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, ); + Self::check_feature(adapter_features, Features::TEXTURE_COMPRESSION_BC); Self::check_feature(adapter_features, Features::TEXTURE_BINDING_ARRAY); Self::check_feature(adapter_features, Features::POLYGON_MODE_LINE); } @@ -85,6 +88,11 @@ impl Capabilities { capabilities.required_features |= Features::ADDRESS_MODE_CLAMP_TO_BORDER | Features::ADDRESS_MODE_CLAMP_TO_ZERO; } + if adapter_features.contains(Features::TEXTURE_COMPRESSION_BC) { + capabilities.texture_compression = true; + capabilities.required_features |= Features::TEXTURE_COMPRESSION_BC; + } + #[cfg(feature = "debug")] if adapter_features.contains(Features::POLYGON_MODE_LINE) { capabilities.polygon_mode_line = true; @@ -135,6 +143,11 @@ impl Capabilities { self.clamp_to_border } + /// Returns `true` if the backend supports BC texture compression. + pub fn supports_texture_compression(&self) -> bool { + self.texture_compression + } + /// Returns `true` if the backend allows drawing triangles as lines /// (wireframe) instead of filled. #[cfg(feature = "debug")] diff --git a/korangar/src/graphics/engine.rs b/korangar/src/graphics/engine.rs index 5533d1ca..e2187459 100644 --- a/korangar/src/graphics/engine.rs +++ b/korangar/src/graphics/engine.rs @@ -17,7 +17,8 @@ use winit::window::Window; use super::{ AntiAliasingResource, Capabilities, EntityInstruction, FramePacer, FrameStage, GlobalContext, LimitFramerate, ModelInstruction, Msaa, - Prepare, PresentModeInfo, ScreenSpaceAntiAliasing, ShadowDetail, Ssaa, Surface, TextureSamplerType, RENDER_TO_TEXTURE_FORMAT, + Prepare, PresentModeInfo, ScreenSpaceAntiAliasing, ShadowDetail, Ssaa, Surface, TextureCompression, TextureSamplerType, + RENDER_TO_TEXTURE_FORMAT, }; use crate::graphics::instruction::RenderInstruction; use crate::graphics::passes::*; @@ -428,6 +429,14 @@ impl GraphicsEngine { ssaa } + pub fn check_texture_compression_requirements(&self, texture_compression: TextureCompression) -> TextureCompression { + if self.capabilities.supports_texture_compression() { + texture_compression + } else { + TextureCompression::Off + } + } + pub fn on_suspended(&mut self) { // Android devices are expected to drop their surface view. if cfg!(target_os = "android") { diff --git a/korangar/src/graphics/passes/directional_shadow/shader/model.wgsl b/korangar/src/graphics/passes/directional_shadow/shader/model.wgsl index 6d6e8242..6ed31ecd 100644 --- a/korangar/src/graphics/passes/directional_shadow/shader/model.wgsl +++ b/korangar/src/graphics/passes/directional_shadow/shader/model.wgsl @@ -42,7 +42,7 @@ fn vs_main( fn fs_main(input: VertexOutput) -> @location(0) vec4 { var diffuse_color = textureSampleLevel(texture, texture_sampler, input.texture_coordinates, 0.0); - if (diffuse_color.a < 1.0) { + if (diffuse_color.a == 0.0) { discard; } diff --git a/korangar/src/graphics/passes/point_shadow/shader/model.wgsl b/korangar/src/graphics/passes/point_shadow/shader/model.wgsl index 0c4abf0d..f5c15b38 100644 --- a/korangar/src/graphics/passes/point_shadow/shader/model.wgsl +++ b/korangar/src/graphics/passes/point_shadow/shader/model.wgsl @@ -47,7 +47,7 @@ fn fs_main(input: VertexOutput) -> @builtin(frag_depth) f32 { let light_distance = length(input.world_position.xyz - pass_uniforms.light_position.xyz); - if (diffuse_color.a != 1.0) { + if (diffuse_color.a == 0.0) { discard; } diff --git a/korangar/src/graphics/settings.rs b/korangar/src/graphics/settings.rs index 46e18f68..44a4219f 100644 --- a/korangar/src/graphics/settings.rs +++ b/korangar/src/graphics/settings.rs @@ -5,6 +5,7 @@ use std::num::NonZeroU32; #[cfg(feature = "debug")] use derive_new::new; use serde::{Deserialize, Serialize}; +use wgpu::TextureFormat; use crate::interface::layout::ScreenSize; @@ -160,6 +161,23 @@ impl Display for ScreenSpaceAntiAliasing { } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum TextureCompression { + Off, + BC3, + BC7, +} + +impl From for TextureFormat { + fn from(value: TextureCompression) -> Self { + match value { + TextureCompression::Off => TextureFormat::Rgba8UnormSrgb, + TextureCompression::BC3 => TextureFormat::Bc3RgbaUnormSrgb, + TextureCompression::BC7 => TextureFormat::Bc7RgbaUnormSrgb, + } + } +} + #[cfg(feature = "debug")] #[derive(Copy, Clone, Default, new)] pub struct RenderSettings { diff --git a/korangar/src/graphics/texture.rs b/korangar/src/graphics/texture.rs index 8a15e37c..187112d3 100644 --- a/korangar/src/graphics/texture.rs +++ b/korangar/src/graphics/texture.rs @@ -7,8 +7,8 @@ use hashbrown::HashMap; use korangar_util::container::Cacheable; use wgpu::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, - BindingType, Device, Extent3d, ImageDataLayout, Queue, ShaderStages, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, - TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension, + BindingType, Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, ShaderStages, TextureAspect, TextureDescriptor, + TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension, }; use crate::interface::layout::ScreenSize; @@ -73,24 +73,54 @@ impl Texture { } } - /// This function doesn't upload mip-map data. Mip maps should be written - /// using the `MipMapRenderPassContext` & `Lanczos3Drawer`. pub fn new_with_data(device: &Device, queue: &Queue, descriptor: &TextureDescriptor, image_data: &[u8], transparent: bool) -> Self { let id = TEXTURE_ID.fetch_add(1, Ordering::Relaxed); let label = descriptor.label.map(|label| label.to_string()); let texture = device.create_texture(descriptor); - let block_size = texture.format().block_copy_size(None).unwrap(); - - queue.write_texture( - texture.as_image_copy(), - image_data, - ImageDataLayout { - offset: 0, - bytes_per_row: Some(descriptor.size.width * block_size), - rows_per_image: Some(descriptor.size.height), - }, - descriptor.size, - ); + let format = texture.format(); + + let (block_width, block_height) = format.block_dimensions(); + let block_size = format.block_copy_size(None).unwrap(); + + let mut offset = 0; + let mut mip_width = descriptor.size.width; + let mut mip_height = descriptor.size.height; + + for mip_level in 0..descriptor.mip_level_count { + let width_blocks = mip_width.div_ceil(block_width); + let height_blocks = mip_height.div_ceil(block_height); + + let bytes_per_row = width_blocks * block_size; + let mip_size = bytes_per_row * height_blocks; + + if offset + mip_size as usize <= image_data.len() { + queue.write_texture( + ImageCopyTexture { + texture: &texture, + mip_level, + origin: Origin3d::ZERO, + aspect: TextureAspect::All, + }, + &image_data[offset..offset + mip_size as usize], + ImageDataLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row), + rows_per_image: None, + }, + Extent3d { + width: mip_width, + height: mip_height, + depth_or_array_layers: 1, + }, + ); + + offset += mip_size as usize; + mip_width = (mip_width / 2).max(1); + mip_height = (mip_height / 2).max(1); + } else { + break; + } + } let texture_view = texture.create_view(&TextureViewDescriptor { label: descriptor.label, diff --git a/korangar/src/interface/windows/settings/graphics.rs b/korangar/src/interface/windows/settings/graphics.rs index a7505b52..f7dad41e 100644 --- a/korangar/src/interface/windows/settings/graphics.rs +++ b/korangar/src/interface/windows/settings/graphics.rs @@ -4,7 +4,8 @@ use korangar_interface::windows::{PrototypeWindow, Window, WindowBuilder}; use korangar_interface::{dimension_bound, size_bound}; use crate::graphics::{ - LimitFramerate, Msaa, PresentModeInfo, ScreenSpaceAntiAliasing, ShadowDetail, ShadowQuality, Ssaa, TextureSamplerType, + LimitFramerate, Msaa, PresentModeInfo, ScreenSpaceAntiAliasing, ShadowDetail, ShadowQuality, Ssaa, TextureCompression, + TextureSamplerType, }; use crate::interface::application::InterfaceSettings; use crate::interface::layout::ScreenSize; @@ -23,6 +24,7 @@ pub struct GraphicsSettingsWindow< ShadowResolution, ShadowMode, HighQualityInterface, + Compression, > where LightingRenderMode: TrackedState + 'static, Vsync: TrackedStateBinary, @@ -35,6 +37,7 @@ pub struct GraphicsSettingsWindow< ShadowResolution: TrackedState + 'static, ShadowMode: TrackedState + 'static, HighQualityInterface: TrackedStateBinary, + Compression: TrackedState + 'static, { present_mode_info: PresentModeInfo, supported_msaa: Vec<(String, Msaa)>, @@ -49,6 +52,7 @@ pub struct GraphicsSettingsWindow< shadow_detail: ShadowResolution, shadow_quality: ShadowMode, high_quality_interface: HighQualityInterface, + texture_compression: Compression, } impl< @@ -63,6 +67,7 @@ impl< ShadowResolution, ShadowMode, HighQualityInterface, + Compression, > GraphicsSettingsWindow< LightingRenderMode, @@ -76,6 +81,7 @@ impl< ShadowResolution, ShadowMode, HighQualityInterface, + Compression, > where LightingRenderMode: TrackedState + 'static, @@ -89,6 +95,7 @@ where ShadowResolution: TrackedState + 'static, ShadowMode: TrackedState + 'static, HighQualityInterface: TrackedStateBinary, + Compression: TrackedState + 'static, { pub const WINDOW_CLASS: &'static str = "graphics_settings"; @@ -106,6 +113,7 @@ where shadow_detail: ShadowResolution, shadow_quality: ShadowMode, high_quality_interface: HighQualityInterface, + texture_compression: Compression, ) -> Self { Self { present_mode_info, @@ -121,6 +129,7 @@ where shadow_detail, shadow_quality, high_quality_interface, + texture_compression, } } } @@ -137,6 +146,7 @@ impl< ShadowResolution, ShadowMode, HighQualityInterface, + Compression, > PrototypeWindow for GraphicsSettingsWindow< LightingRenderMode, @@ -150,6 +160,7 @@ impl< ShadowResolution, ShadowMode, HighQualityInterface, + Compression, > where LightingRenderMode: TrackedState + 'static, @@ -163,6 +174,7 @@ where ShadowResolution: TrackedState + 'static, ShadowMode: TrackedState + 'static, HighQualityInterface: TrackedStateBinary, + Compression: TrackedState + 'static, { fn window_class(&self) -> Option<&str> { Self::WINDOW_CLASS.into() @@ -204,6 +216,20 @@ where .with_event(Box::new(Vec::new)) .with_width(dimension_bound!(!)) .wrap(), + Text::default() + .with_text("Texture compression") + .with_width(dimension_bound!(50%)) + .wrap(), + PickList::default() + .with_options(vec![ + ("Off", TextureCompression::Off), + ("BC3", TextureCompression::BC3), + ("BC7", TextureCompression::BC7), + ]) + .with_selected(self.texture_compression.clone()) + .with_event(Box::new(Vec::new)) + .with_width(dimension_bound!(!)) + .wrap(), Text::default().with_text("Multisampling").with_width(dimension_bound!(50%)).wrap(), PickList::default() .with_options(self.supported_msaa.clone()) diff --git a/korangar/src/loaders/async/mod.rs b/korangar/src/loaders/async/mod.rs index 5d104e92..939c226e 100644 --- a/korangar/src/loaders/async/mod.rs +++ b/korangar/src/loaders/async/mod.rs @@ -11,7 +11,7 @@ use korangar_util::texture_atlas::AtlasAllocation; use ragnarok_packets::{EntityId, ItemId, TilePosition}; use rayon::{ThreadPool, ThreadPoolBuilder}; -use crate::graphics::Texture; +use crate::graphics::{Texture, TextureCompression}; use crate::loaders::error::LoadError; use crate::loaders::{ActionLoader, AnimationLoader, ImageType, MapLoader, ModelLoader, SpriteLoader, TextureLoader}; #[cfg(feature = "debug")] @@ -33,8 +33,14 @@ pub enum LoaderId { pub enum LoadableResource { AnimationData(Arc), - ItemSprite { texture: Arc, location: ItemLocation }, - Map { map: Box, player_position: TilePosition }, + ItemSprite { + texture: Arc, + location: ItemLocation, + }, + Map { + map: Box, + player_position: Option, + }, } enum LoadStatus { @@ -70,7 +76,7 @@ impl AsyncLoader { texture_loader: Arc, ) -> Self { let thread_pool = ThreadPoolBuilder::new() - .num_threads(1) + .num_threads(2) .thread_name(|_| "async loader".to_string()) .build() .unwrap(); @@ -152,8 +158,9 @@ impl AsyncLoader { pub fn request_map_load( &self, + texture_compression: TextureCompression, map_name: String, - player_position: TilePosition, + player_position: Option, #[cfg(feature = "debug")] tile_texture_mapping: Arc>, ) { let map_loader = self.map_loader.clone(); @@ -165,6 +172,7 @@ impl AsyncLoader { let _load_measurement = Profiler::start_measurement("map load"); let map = map_loader.load( + texture_compression, map_name, &model_loader, texture_loader, diff --git a/korangar/src/loaders/map/mod.rs b/korangar/src/loaders/map/mod.rs index da07fa42..50ba26f7 100644 --- a/korangar/src/loaders/map/mod.rs +++ b/korangar/src/loaders/map/mod.rs @@ -21,7 +21,7 @@ use wgpu::{BufferUsages, Device, Queue}; pub use self::vertices::MAP_TILE_SIZE; use self::vertices::{generate_tile_vertices, ground_vertices}; use super::error::LoadError; -use crate::graphics::{Buffer, ModelVertex, NativeModelVertex, Texture}; +use crate::graphics::{Buffer, ModelVertex, NativeModelVertex, Texture, TextureCompression}; use crate::loaders::{GameFileLoader, ImageType, ModelLoader, TextureAtlasFactory, TextureLoader}; use crate::world::{LightSourceKey, Model}; use crate::{EffectSourceExt, LightSourceExt, Map, Object, ObjectKey, SoundSourceExt}; @@ -57,6 +57,7 @@ pub struct MapLoader { impl MapLoader { pub fn load( &self, + texture_compression: TextureCompression, resource_file: String, model_loader: &ModelLoader, texture_loader: Arc, @@ -65,7 +66,7 @@ impl MapLoader { #[cfg(feature = "debug")] let timer = Timer::new_dynamic(format!("load map from {}", &resource_file)); - let mut texture_atlas_factory = TextureAtlasFactory::new(texture_loader.clone(), "map", true, true); + let mut texture_atlas_factory = TextureAtlasFactory::new(texture_loader.clone(), "map", true, true, texture_compression); let mut deferred_vertex_generation: Vec = Vec::new(); let map_file_name = format!("data\\{}.rsw", resource_file); @@ -207,6 +208,7 @@ impl MapLoader { .filter(|_| !(water_bounds.min == Point2::from_value(f32::MAX) && water_bounds.max == Point2::from_value(f32::MIN))); let map = Map::new( + resource_file, gat_data.map_width as usize, gat_data.map_height as usize, water_settings, diff --git a/korangar/src/loaders/sprite/mod.rs b/korangar/src/loaders/sprite/mod.rs index 4f1b74a9..aa9a5508 100644 --- a/korangar/src/loaders/sprite/mod.rs +++ b/korangar/src/loaders/sprite/mod.rs @@ -11,9 +11,8 @@ use korangar_util::FileLoader; use ragnarok_bytes::{ByteReader, FromBytes}; use ragnarok_formats::sprite::{PaletteColor, RgbaImageData, SpriteData}; use ragnarok_formats::version::InternalVersion; -use wgpu::{Device, Extent3d, Queue, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; -use super::FALLBACK_SPRITE_FILE; +use super::{TextureLoader, FALLBACK_SPRITE_FILE}; use crate::graphics::Texture; use crate::loaders::error::LoadError; use crate::loaders::GameFileLoader; @@ -37,18 +36,16 @@ impl Cacheable for Sprite { } pub struct SpriteLoader { - device: Arc, - queue: Arc, game_file_loader: Arc, + texture_loader: Arc, cache: Mutex>>, } impl SpriteLoader { - pub fn new(device: Arc, queue: Arc, game_file_loader: Arc) -> Self { + pub fn new(game_file_loader: Arc, texture_loader: Arc) -> Self { Self { - device, - queue, game_file_loader, + texture_loader, cache: Mutex::new(SimpleCache::new( NonZeroU32::new(MAX_CACHE_COUNT).unwrap(), NonZeroUsize::new(MAX_CACHE_SIZE).unwrap(), @@ -151,29 +148,11 @@ impl SpriteLoader { .map(|mut image_data| { premultiply_alpha(&mut image_data.data); - let texture = Texture::new_with_data( - &self.device, - &self.queue, - &TextureDescriptor { - label: Some(path), - size: Extent3d { - width: image_data.width as u32, - height: image_data.height as u32, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, - usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - RgbaImage::from_raw(image_data.width as u32, image_data.height as u32, image_data.data) - .unwrap() - .as_raw(), + self.texture_loader.create_color( + path, + RgbaImage::from_raw(image_data.width as u32, image_data.height as u32, image_data.data).unwrap(), false, - ); - Arc::new(texture) + ) }) .collect(); diff --git a/korangar/src/loaders/texture/mod.rs b/korangar/src/loaders/texture/mod.rs index 54b1680c..233f435e 100644 --- a/korangar/src/loaders/texture/mod.rs +++ b/korangar/src/loaders/texture/mod.rs @@ -3,7 +3,8 @@ use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::{Arc, Mutex}; use hashbrown::HashMap; -use image::{GrayImage, ImageBuffer, ImageFormat, ImageReader, Rgba, RgbaImage}; +use image::{imageops, GrayImage, ImageBuffer, ImageFormat, ImageReader, Rgba, RgbaImage}; +use intel_tex_2::RgbaSurface; #[cfg(feature = "debug")] use korangar_debug::logging::{print_debug, Colorize, Timer}; use korangar_util::color::contains_transparent_pixel; @@ -17,7 +18,7 @@ use wgpu::{ use super::error::LoadError; use super::{FALLBACK_BMP_FILE, FALLBACK_JPEG_FILE, FALLBACK_PNG_FILE, FALLBACK_TGA_FILE, MIP_LEVELS}; -use crate::graphics::{Lanczos3Drawer, MipMapRenderPassContext, Texture}; +use crate::graphics::{Lanczos3Drawer, MipMapRenderPassContext, Texture, TextureCompression}; use crate::loaders::GameFileLoader; const MAX_CACHE_COUNT: u32 = 512; @@ -56,137 +57,233 @@ impl TextureLoader { } } - fn create(&self, name: &str, image: RgbaImage, transparent: bool) -> Arc { + fn create_raw( + &self, + name: &str, + width: u32, + height: u32, + mip_level_count: u32, + format: TextureFormat, + transparent: bool, + data: &[u8], + ) -> Arc { let texture = Texture::new_with_data( &self.device, &self.queue, &TextureDescriptor { label: Some(name), size: Extent3d { - width: image.width(), - height: image.height(), + width, + height, depth_or_array_layers: 1, }, - mip_level_count: 1, + mip_level_count, sample_count: 1, dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, + format, usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, view_formats: &[], }, - image.as_raw(), + data, transparent, ); Arc::new(texture) } - pub fn create_sdf(&self, name: &str, image: GrayImage) -> Arc { - let texture = Texture::new_with_data( - &self.device, - &self.queue, - &TextureDescriptor { - label: Some(name), - size: Extent3d { - width: image.width(), - height: image.height(), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::R8Unorm, - usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, + pub fn create_color(&self, name: &str, image: RgbaImage, transparent: bool) -> Arc { + self.create_raw( + name, + image.width(), + image.height(), + 1, + TextureFormat::Rgba8UnormSrgb, + transparent, image.as_raw(), - false, - ); - Arc::new(texture) + ) } - pub fn create_msdf(&self, name: &str, image: RgbaImage) -> Arc { - let texture = Texture::new_with_data( - &self.device, - &self.queue, - &TextureDescriptor { - label: Some(name), - size: Extent3d { - width: image.width(), - height: image.height(), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8Unorm, - usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - image.as_raw(), + pub fn create_sdf(&self, name: &str, image: GrayImage) -> Arc { + self.create_raw( + name, + image.width(), + image.height(), + 1, + TextureFormat::R8Unorm, false, - ); - Arc::new(texture) + image.as_raw(), + ) } - fn create_with_mip_maps(&self, name: &str, image: RgbaImage, transparent: bool) -> Arc { - let texture = Texture::new_with_data( - &self.device, - &self.queue, - &TextureDescriptor { - label: Some(name), - size: Extent3d { - width: image.width(), - height: image.height(), - depth_or_array_layers: 1, - }, - mip_level_count: MIP_LEVELS, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8UnormSrgb, - usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }, + pub(crate) fn create_msdf(&self, name: &str, image: RgbaImage) -> Arc { + self.create_raw( + name, + image.width(), + image.height(), + 1, + TextureFormat::Rgba8Unorm, + false, image.as_raw(), - transparent, - ); + ) + } - let mut mip_views = Vec::with_capacity(MIP_LEVELS as usize); - - for level in 0..MIP_LEVELS { - let view = texture.get_texture().create_view(&TextureViewDescriptor { - label: Some(&format!("mip map level {level}")), - format: None, - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, - base_mip_level: level, - mip_level_count: Some(1), - base_array_layer: 0, - array_layer_count: Some(1), - }); - mip_views.push(view); - } + pub(crate) fn create_with_mipmaps( + &self, + name: &str, + texture_compression: TextureCompression, + mips_level: u32, + transparent: bool, + image: RgbaImage, + ) -> Arc { + let mut width = image.width(); + let mut height = image.height(); + + match texture_compression { + TextureCompression::Off => { + let texture = Texture::new_with_data( + &self.device, + &self.queue, + &TextureDescriptor { + label: Some(name), + size: Extent3d { + width: image.width(), + height: image.height(), + depth_or_array_layers: 1, + }, + mip_level_count: MIP_LEVELS, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + image.as_raw(), + transparent, + ); + + if mips_level > 1 { + let mut mip_views = Vec::with_capacity(mips_level as usize); + + for level in 0..mips_level { + let view = texture.get_texture().create_view(&TextureViewDescriptor { + label: Some(&format!("mip map level {level}")), + format: None, + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: level, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(1), + }); + mip_views.push(view); + } + + let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("TextureLoader"), + }); + + for index in 0..(mips_level - 1) as usize { + let mut pass = + self.mip_map_render_context + .create_pass(&self.device, &mut encoder, &mip_views[index], &mip_views[index + 1]); + + self.lanczos3_drawer.draw(&mut pass); + } + + self.queue.submit(Some(encoder.finish())); + } - let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor { - label: Some("TextureLoader"), - }); + Arc::new(texture) + } + TextureCompression::BC3 | TextureCompression::BC7 => { + #[cfg(feature = "debug")] + let timer = Timer::new_dynamic(format!("compress texture for {}", name.magenta())); - for index in 0..(MIP_LEVELS - 1) as usize { - let mut pass = self - .mip_map_render_context - .create_pass(&self.device, &mut encoder, &mip_views[index], &mip_views[index + 1]); - self.lanczos3_drawer.draw(&mut pass); - } + let mut total_size = 0; + let atlas_width = width; + let atlas_height = height; - self.queue.submit(Some(encoder.finish())); + assert_eq!(width % 4, 0, "Texture width must be aligned to 4 pixels"); + assert_eq!(height % 4, 0, "Texture height must be aligned to 4 pixels"); - Arc::new(texture) + for _ in 0..mips_level { + // Compressed blocks are 16 bytes each, covering 4x4 pixels + let mip_size = (width / 4) * (height / 4) * 16; + total_size += mip_size as usize; + width = (width / 2).max(4); + height = (height / 2).max(4); + } + + let mut final_buffer = vec![0u8; total_size]; + let mut current_image = image; + let mut width = current_image.width(); + let mut height = current_image.height(); + let mut offset = 0; + + for level in 0..mips_level { + if level > 0 { + width = (width / 2).max(4); + height = (height / 2).max(4); + current_image = imageops::resize(¤t_image, width, height, imageops::FilterType::Lanczos3); + } + + assert_eq!(width % 4, 0, "Mipmap width must be aligned to 4 pixels"); + assert_eq!(height % 4, 0, "Mipmap height must be aligned to 4 pixels"); + + match texture_compression { + TextureCompression::Off => unreachable!(), + TextureCompression::BC3 => { + let mip_size = intel_tex_2::bc3::calc_output_size(width, height); + intel_tex_2::bc3::compress_blocks_into( + &RgbaSurface { + data: current_image.as_raw(), + width, + height, + stride: width * 4, + }, + &mut final_buffer[offset..offset + mip_size], + ); + offset += mip_size; + } + TextureCompression::BC7 => { + let mip_size = intel_tex_2::bc7::calc_output_size(width, height); + intel_tex_2::bc7::compress_blocks_into( + &intel_tex_2::bc7::alpha_ultra_fast_settings(), + &RgbaSurface { + data: current_image.as_raw(), + width, + height, + stride: width * 4, + }, + &mut final_buffer[offset..offset + mip_size], + ); + offset += mip_size; + } + } + } + + let texture = self.create_raw( + name, + atlas_width, + atlas_height, + mips_level, + texture_compression.into(), + transparent, + &final_buffer, + ); + + #[cfg(feature = "debug")] + timer.stop(); + + texture + } + } } pub fn load(&self, path: &str, image_type: ImageType) -> Result, LoadError> { let texture = match image_type { ImageType::Color => { let (texture_data, transparent) = self.load_texture_data(path, false)?; - self.create(path, texture_data, transparent) + self.create_color(path, texture_data, transparent) } ImageType::Sdf => { let texture_data = self.load_grayscale_texture_data(path)?; @@ -382,6 +479,7 @@ pub struct TextureAtlasFactory { lookup: HashMap, create_mip_map: bool, transparent: bool, + texture_compression: TextureCompression, } #[derive(Copy, Clone)] @@ -397,8 +495,9 @@ impl TextureAtlasFactory { name: impl Into, add_padding: bool, paths: &[&str], + texture_compression: TextureCompression, ) -> (Vec, Arc) { - let mut factory = Self::new(texture_loader, name, add_padding, false); + let mut factory = Self::new(texture_loader, name, add_padding, false, texture_compression); let mut ids: Vec = paths.iter().map(|path| factory.register(path)).collect(); factory.build_atlas(); @@ -412,7 +511,13 @@ impl TextureAtlasFactory { (mapping, texture) } - pub fn new(texture_loader: Arc, name: impl Into, add_padding: bool, create_mip_map: bool) -> Self { + pub fn new( + texture_loader: Arc, + name: impl Into, + add_padding: bool, + create_mip_map: bool, + texture_compression: TextureCompression, + ) -> Self { let mip_level_count = if create_mip_map { NonZeroU32::new(MIP_LEVELS) } else { None }; Self { @@ -422,6 +527,7 @@ impl TextureAtlasFactory { lookup: HashMap::default(), create_mip_map, transparent: false, + texture_compression, } } @@ -455,18 +561,15 @@ impl TextureAtlasFactory { } pub fn upload_texture_atlas_texture(self) -> Arc { - if self.create_mip_map { - self.texture_loader.create_with_mip_maps( - &format!("{} texture atlas", self.name), - self.texture_atlas.get_atlas(), - self.transparent, - ) - } else { - self.texture_loader.create( - &format!("{} texture atlas", self.name), - self.texture_atlas.get_atlas(), - self.transparent, - ) - } + let atlas = self.texture_atlas.get_atlas(); + let name = format!("{} texture atlas", self.name); + + let mips_level = match self.create_mip_map { + true => MIP_LEVELS, + false => 1, + }; + + self.texture_loader + .create_with_mipmaps(&name, self.texture_compression, mips_level, self.transparent, atlas) } } diff --git a/korangar/src/main.rs b/korangar/src/main.rs index 54e582c6..2db68741 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -203,9 +203,9 @@ struct Client { ssaa: MappedRemote, screen_space_anti_aliasing: MappedRemote, high_quality_interface: MappedRemote, + texture_compression: MappedRemote, #[cfg(feature = "debug")] render_settings: PlainTrackedState, - mute_on_focus_loss: MappedRemote, application: InterfaceSettings, @@ -291,6 +291,7 @@ impl Client { .mapped(|settings| &settings.screen_space_anti_aliasing) .new_remote(); let high_quality_interface = graphics_settings.mapped(|settings| &settings.high_quality_interface).new_remote(); + let texture_compression = graphics_settings.mapped(|settings| &settings.texture_compression).new_remote(); #[cfg(feature = "debug")] let render_settings = PlainTrackedState::new(RenderSettings::new()); @@ -379,7 +380,7 @@ impl Client { game_file_loader.clone(), audio_engine.clone(), )); - let sprite_loader = Arc::new(SpriteLoader::new(device.clone(), queue.clone(), game_file_loader.clone())); + let sprite_loader = Arc::new(SpriteLoader::new(game_file_loader.clone(), texture_loader.clone())); let action_loader = Arc::new(ActionLoader::new(game_file_loader.clone(), audio_engine.clone())); let effect_loader = Arc::new(EffectLoader::new(game_file_loader.clone())); let animation_loader = Arc::new(AnimationLoader::new()); @@ -530,23 +531,30 @@ impl Client { let bounding_box_object_set_buffer = ResourceSetBuffer::default(); #[cfg(feature = "debug")] - let (pathing_texture_mapping, pathing_texture) = - TextureAtlasFactory::create_from_group(texture_loader.clone(), "pathing", false, &[ - "pathing_goal.png", - "pathing_straight.png", - "pathing_diagonal.png", - ]); + let (pathing_texture_mapping, pathing_texture) = TextureAtlasFactory::create_from_group( + texture_loader.clone(), + "pathing", + false, + &["pathing_goal.png", "pathing_straight.png", "pathing_diagonal.png"], + graphics_engine.check_texture_compression_requirements(*texture_compression.get()), + ); #[cfg(feature = "debug")] - let (tile_texture_mapping, tile_texture) = TextureAtlasFactory::create_from_group(texture_loader.clone(), "tile", false, &[ - "tile_0.png", - "tile_1.png", - "tile_2.png", - "tile_3.png", - "tile_4.png", - "tile_5.png", - "tile_6.png", - ]); + let (tile_texture_mapping, tile_texture) = TextureAtlasFactory::create_from_group( + texture_loader.clone(), + "tile", + false, + &[ + "tile_0.png", + "tile_1.png", + "tile_2.png", + "tile_3.png", + "tile_4.png", + "tile_5.png", + "tile_6.png", + ], + graphics_engine.check_texture_compression_requirements(*texture_compression.get()), + ); #[cfg(feature = "debug")] let tile_texture_mapping = Arc::new(tile_texture_mapping); @@ -565,6 +573,7 @@ impl Client { time_phase!("load default map", { let map = map_loader .load( + graphics_engine.check_texture_compression_requirements(*texture_compression.get()), DEFAULT_MAP.to_string(), &model_loader, texture_loader.clone(), @@ -631,6 +640,7 @@ impl Client { ssaa, screen_space_anti_aliasing, high_quality_interface, + texture_compression, #[cfg(feature = "debug")] render_settings, mute_on_focus_loss, @@ -855,17 +865,22 @@ impl Client { self.networking_system.connect_to_character_server(login_data, server); self.map = None; - self.entities.clear(); self.particle_holder.clear(); self.effect_holder.clear(); self.point_light_manager.clear(); + self.audio_engine.clear_ambient_sound(); + + self.entities.clear(); + self.audio_engine.play_background_music_track(None); self.interface.close_all_windows_except(&mut self.focus_state); self.async_loader.request_map_load( + self.graphics_engine + .check_texture_compression_requirements(*self.texture_compression.get()), DEFAULT_MAP.to_string(), - TilePosition::new(0, 0), + Some(TilePosition::new(0, 0)), #[cfg(feature = "debug")] self.tile_texture_mapping.clone(), ); @@ -968,6 +983,10 @@ impl Client { self.dialog_system.close_dialog(); self.map = None; + self.particle_holder.clear(); + self.effect_holder.clear(); + self.point_light_manager.clear(); + self.audio_engine.clear_ambient_sound(); } NetworkEvent::CharacterCreated { character_information } => { self.saved_characters.push(character_information); @@ -1058,13 +1077,19 @@ impl Client { } NetworkEvent::ChangeMap(map_name, player_position) => { self.map = None; + self.particle_holder.clear(); + self.effect_holder.clear(); + self.point_light_manager.clear(); + self.audio_engine.clear_ambient_sound(); // Only the player must stay alive between map changes. self.entities.truncate(1); self.async_loader.request_map_load( + self.graphics_engine + .check_texture_compression_requirements(*self.texture_compression.get()), map_name, - player_position, + Some(player_position), #[cfg(feature = "debug")] self.tile_texture_mapping.clone(), ); @@ -1505,6 +1530,7 @@ impl Client { self.shadow_detail.clone_state(), self.shadow_quality.clone_state(), self.high_quality_interface.clone_state(), + self.texture_compression.clone_state(), ), ), UserEvent::OpenAudioSettingsWindow => self.interface.open_window( @@ -1858,13 +1884,12 @@ impl Client { map.set_ambient_sound_sources(&self.audio_engine); self.audio_engine.play_background_music_track(map.background_music_track_name()); - let player_position = Vector2::new(player_position.x as usize, player_position.y as usize); - self.entities[0].set_position(map, player_position, client_tick); - self.player_camera.set_focus_point(self.entities[0].get_position()); + if let Some(player_position) = player_position { + let player_position = Vector2::new(player_position.x as usize, player_position.y as usize); + self.entities[0].set_position(map, player_position, client_tick); + self.player_camera.set_focus_point(self.entities[0].get_position()); + } - self.particle_holder.clear(); - self.effect_holder.clear(); - self.point_light_manager.clear(); self.interface.schedule_render(); let _ = self.networking_system.map_loaded(); } @@ -2425,6 +2450,19 @@ impl Client { update_interface = true; } + if self.texture_compression.consume_changed() { + if let Some(map) = self.map.as_ref() { + self.async_loader.request_map_load( + self.graphics_engine + .check_texture_compression_requirements(*self.texture_compression.get()), + map.get_resource_file().to_string(), + None, + #[cfg(feature = "debug")] + self.tile_texture_mapping.clone(), + ); + } + } + if update_interface { self.interface.schedule_render(); } diff --git a/korangar/src/settings/graphic.rs b/korangar/src/settings/graphic.rs index 02ee2610..a94dce0a 100644 --- a/korangar/src/settings/graphic.rs +++ b/korangar/src/settings/graphic.rs @@ -3,7 +3,9 @@ use korangar_debug::logging::{print_debug, Colorize}; use ron::ser::PrettyConfig; use serde::{Deserialize, Serialize}; -use crate::graphics::{LimitFramerate, Msaa, ScreenSpaceAntiAliasing, ShadowDetail, ShadowQuality, Ssaa, TextureSamplerType}; +use crate::graphics::{ + LimitFramerate, Msaa, ScreenSpaceAntiAliasing, ShadowDetail, ShadowQuality, Ssaa, TextureCompression, TextureSamplerType, +}; #[derive(Serialize, Deserialize)] pub struct GraphicsSettings { @@ -18,6 +20,7 @@ pub struct GraphicsSettings { pub shadow_detail: ShadowDetail, pub shadow_quality: ShadowQuality, pub high_quality_interface: bool, + pub texture_compression: TextureCompression, } impl Default for GraphicsSettings { @@ -34,6 +37,7 @@ impl Default for GraphicsSettings { shadow_detail: ShadowDetail::Medium, shadow_quality: ShadowQuality::Soft, high_quality_interface: true, + texture_compression: TextureCompression::Off, } } } diff --git a/korangar/src/world/map/mod.rs b/korangar/src/world/map/mod.rs index 91801bfb..cd7f0492 100644 --- a/korangar/src/world/map/mod.rs +++ b/korangar/src/world/map/mod.rs @@ -116,6 +116,7 @@ impl MarkerIdentifier { #[derive(new)] pub struct Map { + resource_file: String, width: usize, height: usize, water_settings: Option, @@ -147,6 +148,10 @@ impl Map { self.water_bounds } + pub fn get_resource_file(&self) -> &str { + &self.resource_file + } + pub fn get_world_position(&self, position: Vector2) -> Point3 { let height = average_tile_height(self.get_tile(position)); Point3::new(position.x as f32 * 5.0 + 2.5, height, position.y as f32 * 5.0 + 2.5) diff --git a/korangar_util/src/texture_atlas/offline.rs b/korangar_util/src/texture_atlas/offline.rs index e8772a2f..1206a181 100644 --- a/korangar_util/src/texture_atlas/offline.rs +++ b/korangar_util/src/texture_atlas/offline.rs @@ -9,10 +9,6 @@ use super::AtlasAllocation; use crate::container::{SecondarySimpleSlab, SimpleSlab}; use crate::{create_simple_key, Rectangle}; -/// Factor we used to increase the texture size for inefficiency in -/// the packing algorithm. -const EFFICIENCY_FACTOR: f32 = 1.05; - create_simple_key!(AllocationId, "A key for an allocation"); /// A texture atlas implementation using the MAXRECTS-BSSF (Best Short Side Fit) @@ -161,18 +157,10 @@ impl OfflineTextureAtlas { } else { success = false; - if self.mip_level_count > 1 { - if width <= height { - width *= 2 - } else { - height *= 2 - } + if width <= height { + width *= 2; } else { - let current_area = width * height; - let adjusted_area = (current_area as f32 * EFFICIENCY_FACTOR) as u32; - let side = (adjusted_area as f32).sqrt() as u32; - width = side; - height = side; + height *= 2; } break; @@ -205,27 +193,18 @@ impl OfflineTextureAtlas { fn estimate_initial_size(&self, deferred_allocations: &[(AllocationId, DeferredAllocation)]) -> (u32, u32) { let total_area: u32 = deferred_allocations.iter().map(|r| r.1.padded_size.x * r.1.padded_size.y).sum(); - if self.mip_level_count > 1 { - let mut width = 128; - let mut height = 128; - let mut expand_width = true; - - while (width * height) < total_area { - if expand_width { - width *= 2; - expand_width = false; - } else { - height *= 2; - expand_width = true; - } - } + let mut width = 128; + let mut height = 128; - (width, height) - } else { - let adjusted_area = (total_area as f32 * EFFICIENCY_FACTOR) as u32; - let side = (adjusted_area as f32).sqrt() as u32; - (side, side) + while (width * height) < total_area { + if width <= height { + width *= 2; + } else { + height *= 2; + } } + + (width, height) } fn allocate(&mut self, padded_size: Vector2, original_size: Vector2) -> Option {