From 861b467b95be55db3a42182b77dba944869bf49f Mon Sep 17 00:00:00 2001 From: Micha White Date: Mon, 13 Feb 2023 00:24:07 -0500 Subject: Rename the subdirectories --- Cargo.toml | 4 +- alligator_render/Cargo.toml | 41 -- alligator_render/examples/black.rs | 31 -- alligator_render/examples/bmp.rs | 94 ---- alligator_render/examples/bunnymark.rs | 158 ------ alligator_render/examples/res/bunny.ff | Bin 8208 -> 0 bytes alligator_render/examples/res/gator.bmp | Bin 750054 -> 0 bytes alligator_render/examples/res/gator.ff | Bin 2000016 -> 0 bytes alligator_render/examples/res/ghost.ico | Bin 67646 -> 0 bytes alligator_render/shaders/sprite.wgsl | 62 --- alligator_render/src/camera.rs | 187 ------- alligator_render/src/config.rs | 198 ------- alligator_render/src/instance.rs | 167 ------ alligator_render/src/lib.rs | 21 - alligator_render/src/renderer.rs | 444 --------------- alligator_render/src/texture.rs | 250 --------- alligator_render/src/vertex.rs | 39 -- alligator_resources/Cargo.toml | 17 - alligator_resources/src/lib.rs | 13 - alligator_resources/src/texture.rs | 394 ------------- alligator_tvg/Cargo.toml | 12 - alligator_tvg/src/colors.rs | 333 ----------- alligator_tvg/src/commands.rs | 613 --------------------- alligator_tvg/src/header.rs | 149 ----- alligator_tvg/src/lib.rs | 151 ----- alligator_tvg/src/path.rs | 294 ---------- alligator_tvg/tests/examples/tvg/everything-32.tvg | Bin 2637 -> 0 bytes alligator_tvg/tests/examples/tvg/everything.tvg | Bin 1447 -> 0 bytes alligator_tvg/tests/examples/tvg/shield-16.tvg | Bin 203 -> 0 bytes alligator_tvg/tests/examples/tvg/shield-32.tvg | Bin 371 -> 0 bytes alligator_tvg/tests/examples/tvg/shield-8.tvg | Bin 119 -> 0 bytes alligator_tvg/tests/parse.rs | 14 - render/Cargo.toml | 41 ++ render/examples/black.rs | 31 ++ render/examples/bmp.rs | 94 ++++ render/examples/bunnymark.rs | 158 ++++++ render/examples/res/bunny.ff | Bin 0 -> 8208 bytes render/examples/res/gator.bmp | Bin 0 -> 750054 bytes render/examples/res/gator.ff | Bin 0 -> 2000016 bytes render/examples/res/ghost.ico | Bin 0 -> 67646 bytes render/shaders/sprite.wgsl | 62 +++ render/src/camera.rs | 187 +++++++ render/src/config.rs | 198 +++++++ render/src/instance.rs | 167 ++++++ render/src/lib.rs | 21 + render/src/renderer.rs | 444 +++++++++++++++ render/src/texture.rs | 250 +++++++++ render/src/vertex.rs | 39 ++ resources/Cargo.toml | 17 + resources/src/lib.rs | 13 + resources/src/texture.rs | 394 +++++++++++++ tvg/Cargo.toml | 12 + tvg/src/colors.rs | 333 +++++++++++ tvg/src/commands.rs | 613 +++++++++++++++++++++ tvg/src/header.rs | 149 +++++ tvg/src/lib.rs | 151 +++++ tvg/src/path.rs | 294 ++++++++++ tvg/tests/examples/tvg/everything-32.tvg | Bin 0 -> 2637 bytes tvg/tests/examples/tvg/everything.tvg | Bin 0 -> 1447 bytes tvg/tests/examples/tvg/shield-16.tvg | Bin 0 -> 203 bytes tvg/tests/examples/tvg/shield-32.tvg | Bin 0 -> 371 bytes tvg/tests/examples/tvg/shield-8.tvg | Bin 0 -> 119 bytes tvg/tests/parse.rs | 14 + 63 files changed, 3684 insertions(+), 3684 deletions(-) delete mode 100644 alligator_render/Cargo.toml delete mode 100644 alligator_render/examples/black.rs delete mode 100644 alligator_render/examples/bmp.rs delete mode 100644 alligator_render/examples/bunnymark.rs delete mode 100644 alligator_render/examples/res/bunny.ff delete mode 100644 alligator_render/examples/res/gator.bmp delete mode 100644 alligator_render/examples/res/gator.ff delete mode 100644 alligator_render/examples/res/ghost.ico delete mode 100644 alligator_render/shaders/sprite.wgsl delete mode 100644 alligator_render/src/camera.rs delete mode 100644 alligator_render/src/config.rs delete mode 100644 alligator_render/src/instance.rs delete mode 100644 alligator_render/src/lib.rs delete mode 100644 alligator_render/src/renderer.rs delete mode 100644 alligator_render/src/texture.rs delete mode 100644 alligator_render/src/vertex.rs delete mode 100644 alligator_resources/Cargo.toml delete mode 100644 alligator_resources/src/lib.rs delete mode 100644 alligator_resources/src/texture.rs delete mode 100644 alligator_tvg/Cargo.toml delete mode 100644 alligator_tvg/src/colors.rs delete mode 100644 alligator_tvg/src/commands.rs delete mode 100644 alligator_tvg/src/header.rs delete mode 100644 alligator_tvg/src/lib.rs delete mode 100644 alligator_tvg/src/path.rs delete mode 100644 alligator_tvg/tests/examples/tvg/everything-32.tvg delete mode 100644 alligator_tvg/tests/examples/tvg/everything.tvg delete mode 100644 alligator_tvg/tests/examples/tvg/shield-16.tvg delete mode 100644 alligator_tvg/tests/examples/tvg/shield-32.tvg delete mode 100644 alligator_tvg/tests/examples/tvg/shield-8.tvg delete mode 100644 alligator_tvg/tests/parse.rs create mode 100644 render/Cargo.toml create mode 100644 render/examples/black.rs create mode 100644 render/examples/bmp.rs create mode 100644 render/examples/bunnymark.rs create mode 100644 render/examples/res/bunny.ff create mode 100644 render/examples/res/gator.bmp create mode 100644 render/examples/res/gator.ff create mode 100644 render/examples/res/ghost.ico create mode 100644 render/shaders/sprite.wgsl create mode 100644 render/src/camera.rs create mode 100644 render/src/config.rs create mode 100644 render/src/instance.rs create mode 100644 render/src/lib.rs create mode 100644 render/src/renderer.rs create mode 100644 render/src/texture.rs create mode 100644 render/src/vertex.rs create mode 100644 resources/Cargo.toml create mode 100644 resources/src/lib.rs create mode 100644 resources/src/texture.rs create mode 100644 tvg/Cargo.toml create mode 100644 tvg/src/colors.rs create mode 100644 tvg/src/commands.rs create mode 100644 tvg/src/header.rs create mode 100644 tvg/src/lib.rs create mode 100644 tvg/src/path.rs create mode 100644 tvg/tests/examples/tvg/everything-32.tvg create mode 100644 tvg/tests/examples/tvg/everything.tvg create mode 100644 tvg/tests/examples/tvg/shield-16.tvg create mode 100644 tvg/tests/examples/tvg/shield-32.tvg create mode 100644 tvg/tests/examples/tvg/shield-8.tvg create mode 100644 tvg/tests/parse.rs diff --git a/Cargo.toml b/Cargo.toml index 6ce8ec1..212fdb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["alligator_render", "alligator_resources", "alligator_tvg"] +members = ["render", "resources", "tvg"] resolver = "2" [package] @@ -9,7 +9,7 @@ edition = "2021" rust-version = "1.65" [dependencies] -alligator_render = { path = "alligator_render" } +alligator_render = { path = "render" } [lib] crate-type = ["cdylib", "lib"] diff --git a/alligator_render/Cargo.toml b/alligator_render/Cargo.toml deleted file mode 100644 index 8917a2f..0000000 --- a/alligator_render/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "alligator_render" -version = "0.1.0" -edition = "2021" -rust-version = "1.65" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -alligator_resources = { path = "../alligator_resources" } - -bytemuck = { version = "1", features = ["derive"] } -thiserror = "1" -profiling = "1" -wgpu = "0.15" -winit = "0.28" -image = "0.24" -cgmath = "0.18" -pollster = "0.2" -log = "0.4" -parking_lot = "0.12" -texture_packer = { git="https://github.com/botahamec/piston_texture_packer", branch="u16" } - -tracy-client = { version = "0.15", optional = true } -dhat = { version = "0.3", optional = true } - -[lib] -crate-type = ["cdylib", "lib"] - -[features] -dhat = ["dep:dhat"] -profile-with-tracy = ["tracy-client", "profiling/profile-with-tracy"] - -[[example]] -name = "black" - -[[example]] -name = "bmp" - -[[example]] -name = "bunnymark" diff --git a/alligator_render/examples/black.rs b/alligator_render/examples/black.rs deleted file mode 100644 index 655cbde..0000000 --- a/alligator_render/examples/black.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use std::sync::Arc; - -use alligator_render::{RenderWindowConfig, Renderer}; -use alligator_resources::texture::{TextureManager, TextureManagerConfig}; - -fn update(_renderer: &mut Renderer) {} - -fn main() { - let start = std::time::Instant::now(); - - // configure the render window - let render_config = RenderWindowConfig { - //vsync: false, - //mode: alligator_render::config::WindowMode::BorderlessFullscreen, - title: "Black Screen.exe", - ..Default::default() - }; - - let texture_config = TextureManagerConfig { - initial_capacity: 0, - max_size: 0, - }; - - let texture_manager = Arc::new(TextureManager::new(&texture_config)); - let renderer = Renderer::new(&render_config, texture_manager).unwrap(); - println!("Startup time: {:?}", start.elapsed()); - - renderer.run(update); -} diff --git a/alligator_render/examples/bmp.rs b/alligator_render/examples/bmp.rs deleted file mode 100644 index 9d864d0..0000000 --- a/alligator_render/examples/bmp.rs +++ /dev/null @@ -1,94 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use std::{num::NonZeroU32, sync::Arc}; - -use alligator_render::{Instance, RenderWindowConfig, Renderer}; -use alligator_resources::texture::{ImageFormat, TextureManager, TextureManagerConfig}; - -#[profiling::function] -fn update(renderer: &mut Renderer) { - let camera = renderer.camera_mut(); - camera.set_rotation(camera.rotation() + 0.01); -} - -fn main() { - // configure the render window - let render_config = RenderWindowConfig { - title: "Bumper Stickers", - instance_capacity: 2, - default_width: NonZeroU32::new(1280).unwrap(), - default_height: NonZeroU32::new(720).unwrap(), - //mode: alligator_render::config::WindowMode::BorderlessFullscreen, - //vsync: false, - ..Default::default() - }; - - let texture_config = TextureManagerConfig { - initial_capacity: 3, - max_size: 3_000_000, - }; - - let texture_manager = Arc::new(TextureManager::new(&texture_config)); - let mut renderer = Renderer::new(&render_config, texture_manager.clone()).unwrap(); - - // render the alligator - let gator = include_bytes!("res/gator.ff"); - let gator_id = texture_manager - .load_from_memory(gator, ImageFormat::Farbfeld) - .unwrap(); - renderer.textures_mut().load_texture(gator_id).unwrap(); - let gator_width = renderer.textures_mut().texture_width(gator_id).unwrap(); - let gator_height = renderer.textures_mut().texture_height(gator_id).unwrap(); - let gator_x = renderer.textures_mut().texture_x(gator_id).unwrap(); - let gator_y = renderer.textures_mut().texture_y(gator_id).unwrap(); - - renderer.instances_mut().push_instance(Instance { - position: [-0.5, 0.5], - size: [1.5; 2], - z_index: 1.0, - texture_size: [gator_width, gator_height], - texture_coordinates: [gator_x, gator_y], - ..Default::default() - }); - - // render the ghost - let icon = include_bytes!("res/ghost.ico"); - let icon_id = texture_manager - .load_from_memory(icon, ImageFormat::Ico) - .unwrap(); - renderer.textures_mut().load_texture(icon_id).unwrap(); - let icon_width = renderer.textures_mut().texture_width(icon_id).unwrap(); - let icon_height = renderer.textures_mut().texture_height(icon_id).unwrap(); - let icon_x = renderer.textures_mut().texture_x(icon_id).unwrap(); - let icon_y = renderer.textures_mut().texture_y(icon_id).unwrap(); - - renderer.instances_mut().push_instance(Instance { - position: [0.5, 0.5], - size: [0.75; 2], - rotation: 0.5, - z_index: 1.0, - texture_size: [icon_width, icon_height], - texture_coordinates: [icon_x, icon_y], - ..Default::default() - }); - - // render the bitmap alligator - let gator = include_bytes!("res/gator.bmp"); - let gator_id = texture_manager - .load_from_memory(gator, ImageFormat::Bmp) - .unwrap(); - let gator_width = renderer.textures_mut().texture_width(gator_id).unwrap(); - let gator_height = renderer.textures_mut().texture_height(gator_id).unwrap(); - let gator_x = renderer.textures_mut().texture_x(gator_id).unwrap(); - let gator_y = renderer.textures_mut().texture_y(gator_id).unwrap(); - - renderer.instances_mut().push_instance(Instance { - position: [0.0, -0.5], - size: [1.5; 2], - texture_size: [gator_width, gator_height], - texture_coordinates: [gator_x, gator_y], - ..Default::default() - }); - - renderer.run(update); -} diff --git a/alligator_render/examples/bunnymark.rs b/alligator_render/examples/bunnymark.rs deleted file mode 100644 index 1579cf0..0000000 --- a/alligator_render/examples/bunnymark.rs +++ /dev/null @@ -1,158 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use std::{num::NonZeroU32, sync::Arc, time::Instant}; - -use alligator_render::{Instance, InstanceId, RenderWindowConfig, Renderer}; -use alligator_resources::texture::{ImageFormat, TextureId, TextureManager, TextureManagerConfig}; - -fn xorshift_plus(seed: &mut [u64; 2]) -> u64 { - let mut t = seed[0]; - let s = seed[1]; - - t ^= t << 23; - t ^= t >> 18; - t ^= s ^ (s >> 5); - - seed[0] = s; - seed[1] = t; - - t.wrapping_add(s) -} - -#[derive(Debug)] -struct State { - texture_id: TextureId, - bunnies: Vec, - previous_timestamp: Option, - seed: [u64; 2], - stopped: bool, -} - -impl State { - fn new(texture_id: TextureId) -> Self { - Self { - texture_id, - bunnies: Vec::with_capacity(10_000_000), - previous_timestamp: None, - seed: [0x0D15EA5E8BADF00D, 0xDECAFBADDEADBEAF], - stopped: false, - } - } - - #[profiling::function] - fn update(&mut self, renderer: &mut Renderer) { - let Some(instant) = self.previous_timestamp else { - self.previous_timestamp = Some(Instant::now()); - return; - }; - - let frame_time = instant.elapsed(); - let fps = 1.0 / frame_time.as_secs_f32(); - - renderer.set_title(&format!( - "BunnyMark - {} bunnies - {} FPS", - self.bunnies.len(), - fps.round() - )); - - if fps < 15.0 { - self.stopped = true; - } - - self.previous_timestamp = Some(Instant::now()); - - if self.stopped { - return; - } - - for bunny in self.bunnies.iter_mut() { - let instance = renderer - .instances_mut() - .get_instance_mut(bunny.instance_id) - .unwrap(); - - instance.position[0] += bunny.velocity_x; - instance.position[1] += bunny.velocity_y; - - if !(-1.5..1.5).contains(&instance.position[0]) { - instance.position[0] = instance.position[0].clamp(-1.0, 1.0); - bunny.velocity_x = -bunny.velocity_x; - } - - if !(-0.75..0.75).contains(&instance.position[1]) { - instance.position[1] = instance.position[1].clamp(-0.5, 0.5); - bunny.velocity_y *= -0.90; - } - - bunny.velocity_y -= 0.005; - } - - for _ in 0..=(fps as u64 * 50) { - let texture_x = renderer.textures_mut().texture_x(self.texture_id).unwrap(); - let texture_y = renderer.textures_mut().texture_y(self.texture_id).unwrap(); - let texture_height = renderer - .textures_mut() - .texture_height(self.texture_id) - .unwrap(); - let texture_width = renderer - .textures_mut() - .texture_width(self.texture_id) - .unwrap(); - let instance_id = renderer.instances_mut().push_instance(Instance { - texture_coordinates: [texture_x, texture_y], - texture_size: [texture_width, texture_height], - size: [0.08, 0.08], - position: [-1.5, 0.70], - ..Default::default() - }); - - let velocity_x = (xorshift_plus(&mut self.seed) % 1_000_000) as f32 / 25_000_000.0; - let velocity_y = (xorshift_plus(&mut self.seed) % 1_000_000) as f32 / 25_000_000.0; - self.bunnies.push(Bunny { - instance_id, - velocity_x, - velocity_y, - }); - } - } -} - -#[derive(Debug, Clone, Copy)] -struct Bunny { - instance_id: InstanceId, - velocity_x: f32, - velocity_y: f32, -} - -fn main() { - #[cfg(feature = "profile-with-tracy")] - profiling::tracy_client::Client::start(); - profiling::register_thread!("main"); - - // configure the render window - let render_config = RenderWindowConfig { - title: "BunnyMark", - instance_capacity: 150_000, - default_width: NonZeroU32::new(1280).unwrap(), - default_height: NonZeroU32::new(720).unwrap(), - vsync: false, - low_power: false, - ..Default::default() - }; - - let texture_config = TextureManagerConfig { - initial_capacity: 1, - max_size: 10_000, - }; - - let bunny = include_bytes!("res/bunny.ff"); - let texture_manager = Arc::new(TextureManager::new(&texture_config)); - let mut renderer = Renderer::new(&render_config, texture_manager.clone()).unwrap(); - let texture_id = texture_manager - .load_from_memory(bunny, ImageFormat::Farbfeld) - .unwrap(); - renderer.textures_mut().load_texture(texture_id).unwrap(); - - let state = Box::leak(Box::new(State::new(texture_id))); - renderer.run(|r| state.update(r)); -} diff --git a/alligator_render/examples/res/bunny.ff b/alligator_render/examples/res/bunny.ff deleted file mode 100644 index 64c5a69..0000000 Binary files a/alligator_render/examples/res/bunny.ff and /dev/null differ diff --git a/alligator_render/examples/res/gator.bmp b/alligator_render/examples/res/gator.bmp deleted file mode 100644 index e752b56..0000000 Binary files a/alligator_render/examples/res/gator.bmp and /dev/null differ diff --git a/alligator_render/examples/res/gator.ff b/alligator_render/examples/res/gator.ff deleted file mode 100644 index aac1bcb..0000000 Binary files a/alligator_render/examples/res/gator.ff and /dev/null differ diff --git a/alligator_render/examples/res/ghost.ico b/alligator_render/examples/res/ghost.ico deleted file mode 100644 index 102de00..0000000 Binary files a/alligator_render/examples/res/ghost.ico and /dev/null differ diff --git a/alligator_render/shaders/sprite.wgsl b/alligator_render/shaders/sprite.wgsl deleted file mode 100644 index 276a8ef..0000000 --- a/alligator_render/shaders/sprite.wgsl +++ /dev/null @@ -1,62 +0,0 @@ - -@group(0) @binding(0) -var camera: mat4x4; - -struct VertexInput { - @location(0) position: vec2 -} - -struct InstanceInput { - @location(1) position: vec2, - @location(2) size: vec2, - @location(3) texture_coordinates: vec2, - @location(4) texture_size: vec2, - @location(5) texture_atlas_index: u32, - @location(6) rotation: f32, - @location(7) z_index: f32, -} - -struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) texture_coordinates: vec2, - @location(1) texture_atlas_index: u32 -} - -@vertex -fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { - var out: VertexOutput; - - // rotate the sprite - let rotation = -instance.rotation; - let a = vec2(cos(rotation), sin(rotation)); - let b = vec2(-a[1], a[0]); - let rotation_mat = mat2x2(a, b); - let rotated = rotation_mat * model.position; - - // scale the sprite - let scaled = rotated * instance.size; - - // move the sprite - let position2d = scaled + instance.position; - - // camera stuff - let position4d = vec4(position2d, instance.z_index, 1.0); - let position = camera * position4d; - - let tex_coords = vec2(model.position[0] + 0.5, 1.0 - (model.position[1] + 0.5)); - - out.clip_position = position; - out.texture_atlas_index = instance.texture_atlas_index; - out.texture_coordinates = tex_coords * instance.texture_size + instance.texture_coordinates; - return out; -} - -@group(1) @binding(0) -var t_diffuse: texture_2d; -@group(1) @binding(1) -var s_diffuse: sampler; - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return textureSample(t_diffuse, s_diffuse, in.texture_coordinates); -} \ No newline at end of file diff --git a/alligator_render/src/camera.rs b/alligator_render/src/camera.rs deleted file mode 100644 index ecece90..0000000 --- a/alligator_render/src/camera.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::mem::size_of; - -use cgmath::{Matrix4, Vector2}; - -#[derive(Debug)] -pub struct Camera { - position: (f32, f32), - zoom: f32, - rotation: f32, - inverse_aspect_ratio: f32, - buffer: wgpu::Buffer, - bind_group: wgpu::BindGroup, -} - -type CameraUniform = [[f32; 4]; 4]; - -#[allow(clippy::cast_precision_loss)] -fn inverse_aspect_ratio(width: u32, height: u32) -> f32 { - (height as f32) / (width as f32) -} - -impl Camera { - /// Create a new camera, with a position of (0, 0), and a zoom of 1.0 - pub(crate) fn new( - device: &wgpu::Device, - width: u32, - height: u32, - ) -> (Self, wgpu::BindGroupLayout) { - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Camera Uniform"), - size: size_of::() as wgpu::BufferAddress, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Camera Bind Group Layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Camera Bind Group"), - layout: &bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); - - ( - Self { - position: (0.0, 0.0), - zoom: 1.0, - rotation: 0.0, - inverse_aspect_ratio: inverse_aspect_ratio(width, height), - buffer, - bind_group, - }, - bind_group_layout, - ) - } - - /// Get the camera's current x position - #[must_use] - pub const fn x(&self) -> f32 { - self.position.0 - } - - /// Get the camera's current y position - #[must_use] - pub const fn y(&self) -> f32 { - self.position.1 - } - - /// Get the camera's current zoom - #[must_use] - pub const fn zoom(&self) -> f32 { - self.zoom - } - - /// Get the camera's current rotation, in radians - #[must_use] - pub const fn rotation(&self) -> f32 { - self.rotation - } - - /// Set the position of the camera - pub fn set_position(&mut self, x: f32, y: f32) { - #[cfg(debug_assertions)] - if !(-1000.0..1000.0).contains(&x) || !(-1000.0..1000.0).contains(&y) { - log::warn!( - "The position of the camera is (x: {}, y: {}). \ - Please keep both the x and y positions above -1000 and below 1000 units. \ - Otherwise, everything will look crazy. \ - For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24", - x, - y - ); - } - - self.position = (x, y); - } - - /// Set the zoom of the camera - pub fn set_zoom(&mut self, zoom: f32) { - #[cfg(debug_assertions)] - if !(-1000.0..1000.0).contains(&zoom) { - log::warn!( - "The zoom of the camera is {}. \ - Please keep above -1000, and below 1000, or else smooth zoom may be difficult. \ - For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24", - zoom - ); - } - - self.zoom = zoom; - } - - /// Set the camera rotation, in radians - pub fn set_rotation(&mut self, rotation: f32) { - self.rotation = rotation % std::f32::consts::TAU; - } - - /// Set the aspect ratio of the camera - pub(crate) fn set_size(&mut self, width: u32, height: u32) { - self.inverse_aspect_ratio = inverse_aspect_ratio(width, height); - } - - /// Create a matrix that can be multiplied by any vector to transform it - /// according to the current camera - #[allow(clippy::wrong_self_convention)] - fn to_matrix(&self) -> CameraUniform { - let cos = self.rotation.cos(); - let sin = self.rotation.sin(); - - let x_axis = Vector2::new(cos, -sin); - let y_axis = Vector2::new(sin, cos); - - let eye = Vector2::new(self.position.0, self.position.1); - let x_dot = -cgmath::dot(x_axis, eye); - let y_dot = -cgmath::dot(y_axis, eye); - - #[rustfmt::skip] - let view_matrix = Matrix4::new( - x_axis.x, y_axis.x, 0.0, 0.0, - x_axis.y, y_axis.y, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - x_dot, y_dot, 0.0, 1.0 - ); - - #[rustfmt::skip] - // TODO implement more scaling coordinate systems - let projection_matrix = Matrix4::new( - self.inverse_aspect_ratio * self.zoom, 0.0, 0.0, 0.0, - 0.0, self.zoom, 0.0, 0.0, - 0.0, 0.0, 1.0 / 256.0, 0.0, - 0.0, 0.0, 0.0, 1.0 - ); - - let transform = projection_matrix * view_matrix; - transform.into() - } - - /// Get the bind group for the camera - pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup { - &self.bind_group - } - - /// Refresh the camera buffer for the next frame - #[profiling::function] - pub(crate) fn refresh(&self, queue: &wgpu::Queue) { - queue.write_buffer( - &self.buffer, - 0 as wgpu::BufferAddress, - bytemuck::cast_slice(&self.to_matrix()), - ); - } -} diff --git a/alligator_render/src/config.rs b/alligator_render/src/config.rs deleted file mode 100644 index c3cc6b6..0000000 --- a/alligator_render/src/config.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::num::NonZeroU32; - -use winit::dpi::{LogicalPosition, LogicalSize}; -use winit::window::{Fullscreen, WindowBuilder}; - -/// Describes how a window may be resized -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub struct Resizable { - /// The minimum width of the window, or None if unconstrained - pub min_width: Option, - /// The minimum height of the window, or None if unconstrained - pub min_height: Option, - /// The maximum width of the window, or None if unconstrained - pub max_width: Option, - /// The maximum height of the window, or None if unconstrained - pub max_height: Option, -} - -/// Information about a window, that is not fullscreened -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct WindowInfo { - pub default_x: i32, - pub default_y: i32, - pub resizable: Option, - pub default_maximized: bool, -} - -impl Default for WindowInfo { - fn default() -> Self { - Self { - default_x: 100, - default_y: 100, - resizable: Some(Resizable::default()), - default_maximized: false, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum WindowMode { - Windowed(WindowInfo), - // TODO support choosing a monitor - BorderlessFullscreen, // TODO exclusive fullscreen -} - -impl Default for WindowMode { - fn default() -> Self { - Self::Windowed(WindowInfo::default()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -// TODO window icon -pub struct RenderWindowConfig<'a> { - /// The width of the window, once initialized - pub default_width: NonZeroU32, - /// The height of the window, once initialized - pub default_height: NonZeroU32, - /// The window may be fullscreen - pub mode: WindowMode, - /// The title for the window - pub title: &'a str, - /// If true, a low-power device will be selected as the GPU, if possible - pub low_power: bool, - /// If true, Fifo mode is used to present frames. If false, then Mailbox or - /// Immediate will be used if available. Otherwise, Fifo will be used. - pub vsync: bool, - /// The initial capacity of the instance buffer. The size will increase if - /// it's not large enough. Increasing this value may improve performance - /// towards the beginning, if a lot of instances are being created. For - /// compatibility with older devices, it's recommended to keep this number - /// below 150 thousand. - pub instance_capacity: usize, -} - -impl<'a> Default for RenderWindowConfig<'a> { - fn default() -> Self { - Self { - default_width: NonZeroU32::new(640).unwrap(), - default_height: NonZeroU32::new(480).unwrap(), - mode: WindowMode::default(), - title: "Alligator Game", - low_power: true, - vsync: true, - instance_capacity: 500, - } - } -} - -impl<'a> RenderWindowConfig<'a> { - /// Based on the vsync settings, choose a presentation mode - pub(crate) fn present_mode( - vsync: bool, - supported_modes: &[wgpu::PresentMode], - ) -> wgpu::PresentMode { - if vsync { - wgpu::PresentMode::Fifo - } else if supported_modes.contains(&wgpu::PresentMode::Mailbox) { - wgpu::PresentMode::Mailbox - } else if supported_modes.contains(&wgpu::PresentMode::Immediate) { - wgpu::PresentMode::Immediate - } else { - wgpu::PresentMode::Fifo - } - } - - /// Pick an alpha mode - fn alpha_mode(supported_modes: &[wgpu::CompositeAlphaMode]) -> wgpu::CompositeAlphaMode { - if supported_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) { - wgpu::CompositeAlphaMode::PostMultiplied - } else { - wgpu::CompositeAlphaMode::Auto - } - } - - /// Create a `WindowBuilder` from the configuration given. This window is - /// initially invisible and must later be made visible. - pub(crate) fn to_window(&self) -> WindowBuilder { - // start building the window - let mut builder = WindowBuilder::new() - .with_title(self.title) - .with_visible(false) - .with_inner_size(LogicalSize::new( - self.default_width.get(), - self.default_height.get(), - )); - - match self.mode { - WindowMode::Windowed(window_info) => { - builder = builder.with_maximized(window_info.default_maximized); - - if let Some(resizing_options) = window_info.resizable { - if resizing_options.max_height.is_some() || resizing_options.max_width.is_some() - { - builder = builder.with_max_inner_size(LogicalSize::new( - resizing_options.max_width.unwrap_or(NonZeroU32::MAX).get(), - resizing_options.max_height.unwrap_or(NonZeroU32::MAX).get(), - )); - } - - if resizing_options.min_height.is_some() || resizing_options.min_width.is_some() - { - builder = builder.with_min_inner_size(LogicalSize::new( - resizing_options.min_width.unwrap_or(NonZeroU32::MAX).get(), - resizing_options.min_height.unwrap_or(NonZeroU32::MAX).get(), - )); - } - } else { - builder = builder.with_resizable(false); - } - - // TODO clamp the position to the monitor's size - builder = builder.with_position(LogicalPosition::new( - window_info.default_x, - window_info.default_y, - )); - } - WindowMode::BorderlessFullscreen => { - builder = builder.with_fullscreen(Some(Fullscreen::Borderless(None))); - } - } - - builder - } - - /// Gets a surface configuration out of the config. - pub(crate) fn to_surface_configuration( - &self, - supported_present_modes: &[wgpu::PresentMode], - supported_alpha_modes: &[wgpu::CompositeAlphaMode], - ) -> wgpu::SurfaceConfiguration { - let present_mode = Self::present_mode(self.vsync, supported_present_modes); - let alpha_mode = Self::alpha_mode(supported_alpha_modes); - - // configuration for the surface - wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8Unorm, - width: self.default_width.get(), - height: self.default_height.get(), - alpha_mode, - present_mode, - view_formats: vec![ - wgpu::TextureFormat::Bgra8Unorm, - wgpu::TextureFormat::Bgra8UnormSrgb, - ], - } - } - - /// Get the power preference - pub(crate) const fn power_preference(&self) -> wgpu::PowerPreference { - if self.low_power { - wgpu::PowerPreference::LowPower - } else { - wgpu::PowerPreference::HighPerformance - } - } -} diff --git a/alligator_render/src/instance.rs b/alligator_render/src/instance.rs deleted file mode 100644 index e346cae..0000000 --- a/alligator_render/src/instance.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::mem::size_of; - -use bytemuck::{Pod, Zeroable}; - -/// The ID for an instance -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct InstanceId(usize); - -/// A sprite, that can be used by the alligator shader -#[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)] -pub struct Instance { - /// Position on the screen - pub position: [f32; 2], - /// Relative size - pub size: [f32; 2], - /// The location of the texture in the texture atlas - pub texture_coordinates: [f32; 2], - /// The size of the sprite's texture - pub texture_size: [f32; 2], - /// The index of the texture atlas to use - pub texture_atlas_index: u32, - /// Rotation, in radians - pub rotation: f32, - /// z-index - pub z_index: f32, -} - -impl Default for Instance { - fn default() -> Self { - Self { - position: [0.0; 2], - size: [1.0; 2], - rotation: 0.0, - z_index: 0.0, - texture_coordinates: [0.0; 2], - texture_size: [1.0; 2], - texture_atlas_index: 0, - } - } -} - -impl Instance { - // whenever this is updated, please also update `sprite.wgsl` - const ATTRIBUTES: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![ - 1 => Float32x2, 2 => Float32x2, 3 => Float32x2, 4 => Float32x2, - 5 => Uint32, 6 => Float32, 7 => Float32 - ]; - - pub(crate) fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - // make sure these two don't conflict - debug_assert_eq!( - Self::ATTRIBUTES[0].shader_location as usize, - crate::Vertex::ATTRIBUTES.len() - ); - wgpu::VertexBufferLayout { - array_stride: size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &Self::ATTRIBUTES, - } - } -} - -/// A buffer of sprites, for both CPU and GPU memory -pub struct InstanceBuffer { - instances: Vec, - instance_buffer: wgpu::Buffer, - instance_buffer_size: usize, -} - -fn create_buffer(device: &wgpu::Device, instances: &Vec) -> wgpu::Buffer { - device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Sprite Instance Buffer"), - size: (instances.capacity() * size_of::()) as wgpu::BufferAddress, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }) -} - -impl InstanceBuffer { - /// Create a new buffer with the given capacity - pub(crate) fn new(device: &wgpu::Device, capacity: usize) -> Self { - let instances = Vec::with_capacity(capacity); - let instance_buffer_size = instances.capacity(); - let instance_buffer = create_buffer(device, &instances); - - Self { - instances, - instance_buffer, - instance_buffer_size, - } - } - - /// The number of sprites - pub fn len(&self) -> u32 { - self.instances - .len() - .try_into() - .expect("expected less than 3 billion instances") - } - - /// Returns `true` if there are no sprites - pub fn is_empty(&self) -> bool { - self.instances.is_empty() - } - - /// The capacity of the buffer - pub const fn buffer_size(&self) -> usize { - self.instance_buffer_size - } - - /// Get a slice containing the entire buffer - pub(crate) fn buffer_slice(&self) -> wgpu::BufferSlice { - self.instance_buffer.slice(..) - } - - /// Add a new sprite. The new sprite's `InstanceId` is returned. This ID - /// becomes invalid if the buffer is cleared. - pub fn push_instance(&mut self, instance: Instance) -> InstanceId { - let index = self.instances.len(); - self.instances.push(instance); - InstanceId(index) - } - - /// Get a specific instance - pub fn get_instance(&self, id: InstanceId) -> Option<&Instance> { - self.instances.get(id.0) - } - - /// Get a mutable reference to a specific sprite - pub fn get_instance_mut(&mut self, id: InstanceId) -> Option<&mut Instance> { - self.instances.get_mut(id.0) - } - - /// Clear the instance buffer. This invalidates all `InstanceId`'s - pub fn clear(&mut self) { - self.instances.clear(); - } - - /// Increase the capacity of the buffer - fn expand_buffer(&mut self, device: &wgpu::Device) { - self.instance_buffer_size = self.instances.capacity(); - self.instance_buffer = create_buffer(device, &self.instances); - } - - /// Fill the GPU buffer with the sprites in the CPU buffer. - #[profiling::function] - pub(crate) fn fill_buffer(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { - if self.instances.len() > self.instance_buffer_size { - self.expand_buffer(device); - } - - // the instances must be sorted by z-index before being handed to the GPU - let sorted = { - profiling::scope!("depth sorting"); - let mut sorted = self.instances.clone(); - sorted.sort_by(|a, b| a.z_index.total_cmp(&b.z_index)); - sorted - }; - - queue.write_buffer( - &self.instance_buffer, - 0 as wgpu::BufferAddress, - bytemuck::cast_slice(&sorted), - ); - } -} diff --git a/alligator_render/src/lib.rs b/alligator_render/src/lib.rs deleted file mode 100644 index 0d76cc8..0000000 --- a/alligator_render/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -#![feature(nonzero_min_max)] -#![feature(type_alias_impl_trait)] -#![warn(clippy::pedantic)] -#![warn(clippy::nursery)] -#![allow(clippy::module_name_repetitions)] - -mod camera; -pub mod config; -pub mod instance; -pub mod renderer; -mod texture; -mod vertex; - -pub(crate) use camera::Camera; -pub use config::*; -pub use instance::Instance; -pub(crate) use instance::InstanceBuffer; -pub use instance::InstanceId; -pub use renderer::Renderer; -pub(crate) use texture::TextureAtlas; -pub(crate) use vertex::Vertex; diff --git a/alligator_render/src/renderer.rs b/alligator_render/src/renderer.rs deleted file mode 100644 index f5b486d..0000000 --- a/alligator_render/src/renderer.rs +++ /dev/null @@ -1,444 +0,0 @@ -use std::num::NonZeroU32; -use std::{convert::TryInto, sync::Arc}; - -use crate::{ - vertex::SQUARE, Camera, Instance, InstanceBuffer, RenderWindowConfig, TextureAtlas, Vertex, -}; -use alligator_resources::texture::TextureManager; -use pollster::FutureExt; -use thiserror::Error; -use wgpu::{include_wgsl, util::DeviceExt}; -use winit::{ - dpi::PhysicalSize, - error::OsError, - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::Window, -}; - -/// No device could be found which supports the given surface -#[derive(Clone, Copy, Debug, Error)] -#[error("No GPU could be found on this machine")] -pub struct NoGpuError { - /// Prevents this type from being constructed - _priv: (), -} - -impl NoGpuError { - /// Create a new error - const fn new() -> Self { - Self { _priv: () } - } -} - -/// No device could be found which supports the given surface -#[derive(Clone, Copy, Debug, Error)] -#[error("A WebGPU or WebGL context could not be obtained")] -pub struct NoWebContextError { - /// Prevents this type from being constructed - _priv: (), -} - -impl NoWebContextError { - /// Create a new error - const fn new() -> Self { - Self { _priv: () } - } -} - -#[derive(Debug, Error)] -pub enum NewRendererError { - #[error(transparent)] - NoGpu(#[from] NoGpuError), - #[error(transparent)] - NoWebContext(#[from] NoWebContextError), - #[error(transparent)] - // TODO better error - WindowInitError(#[from] OsError), -} - -// TODO make this Debug -pub struct Renderer { - // TODO move some of this data elsewhere - surface: wgpu::Surface, - surface_config: wgpu::SurfaceConfiguration, - supported_present_modes: Box<[wgpu::PresentMode]>, - device: wgpu::Device, - queue: wgpu::Queue, - render_pipeline: wgpu::RenderPipeline, - square_vertex_buffer: wgpu::Buffer, - square_vertices: u32, - instances: InstanceBuffer, - camera: Camera, - textures: TextureAtlas, - event_loop: Option>, - window: Window, -} - -fn get_adapter( - instance: &wgpu::Instance, - surface: &wgpu::Surface, - power_preference: wgpu::PowerPreference, -) -> Result { - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference, - compatible_surface: Some(surface), - force_fallback_adapter: false, - }) - .block_on(); // TODO this takes too long - - let adapter = adapter.or_else(|| { - instance - .enumerate_adapters(wgpu::Backends::PRIMARY) - .find(|adapter| !surface.get_capabilities(adapter).formats.is_empty()) - }); - - adapter.ok_or(NoGpuError::new()) -} - -fn sprite_render_pipeline( - device: &wgpu::Device, - texture_format: wgpu::TextureFormat, - render_pipeline_layout: &wgpu::PipelineLayout, -) -> wgpu::RenderPipeline { - let shader = device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl")); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Sprite Render Pipeline"), - layout: Some(render_pipeline_layout), - // information about the vertex shader - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[Vertex::desc(), Instance::desc()], - }, - // information about the fragment shader - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format: texture_format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - // don't render the back of a sprite - cull_mode: Some(wgpu::Face::Back), - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }) -} - -impl Renderer { - /// Initializes the renderer - /// - /// # Errors - /// - /// Returns a [`NoGpu`] error if no device could be detected that can - /// display to the window - /// - /// # Panics - /// - /// This function **must** be called on the main thread, or else it may - /// panic on some platforms. - // TODO make it possible to use without a window (ie, use a bitmap in memory as a surface) - // TODO this function needs to be smaller - pub fn new( - config: &RenderWindowConfig, - textures: Arc, - ) -> Result { - // build the window - let event_loop = EventLoop::new(); - let window = config.to_window().build(&event_loop)?; - let event_loop = Some(event_loop); - - // the instance's main purpose is to create an adapter and a surface - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN, - dx12_shader_compiler: wgpu::Dx12Compiler::Fxc, // TODO support DXC - }); - - // the surface is the part of the screen we'll draw to - let surface = - unsafe { instance.create_surface(&window) }.map_err(|_| NoWebContextError::new())?; - - let power_preference = config.power_preference(); - - // the adapter is the handle to the GPU - let adapter = get_adapter(&instance, &surface, power_preference)?; - - // gets a connection to the device, as well as a handle to its command queue - // the options chosen here ensure that this is guaranteed to not panic - let (device, queue) = adapter - .request_device( - &wgpu::DeviceDescriptor { - features: wgpu::Features::empty(), - limits: wgpu::Limits { - max_buffer_size: adapter.limits().max_buffer_size, - max_texture_dimension_2d: adapter.limits().max_texture_dimension_2d, - ..Default::default() - }, - ..Default::default() - }, - None, - ) - .block_on() - .expect("there was no device with the selected features"); - - // configuration for the surface - let capabilities = surface.get_capabilities(&adapter); - let supported_present_modes = capabilities.present_modes.into_boxed_slice(); - let supported_alpha_modes = capabilities.alpha_modes.into_boxed_slice(); - let surface_config = - config.to_surface_configuration(&supported_present_modes, &supported_alpha_modes); - surface.configure(&device, &surface_config); - - // create the camera - let width = window.inner_size().width; - let height = window.inner_size().height; - let (camera, camera_bind_group_layout) = Camera::new(&device, width, height); - - // the vertex buffer used for rendering squares - let square_vertices = SQUARE - .len() - .try_into() - .expect("expected fewer than 3 billion vertices in a square"); - let square_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Square Vertex Buffer"), - contents: bytemuck::cast_slice(&SQUARE), - usage: wgpu::BufferUsages::VERTEX, - }); - - // create the instance buffer - let instances = InstanceBuffer::new(&device, config.instance_capacity); - - // TODO make this configurable - let (textures, texture_layout) = TextureAtlas::new( - &device, - textures, - window.inner_size().width, - window.inner_size().height, - ); - - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Sprite Render Pipeline Layout"), - bind_group_layouts: &[&camera_bind_group_layout, &texture_layout], - push_constant_ranges: &[], - }); - - // set up a pipeline for sprite rendering - let render_pipeline = - sprite_render_pipeline(&device, surface_config.format, &render_pipeline_layout); - - Ok(Self { - surface, - surface_config, - supported_present_modes, - device, - queue, - render_pipeline, - square_vertex_buffer, - square_vertices, - instances, - camera, - textures, - event_loop, - window, - }) - } - - /// Reconfigure the surface - fn reconfigure(&mut self) { - self.surface.configure(&self.device, &self.surface_config); - } - - /// Resize just the renderer. The window will remain unchanged - fn resize_renderer(&mut self, size: PhysicalSize) { - if size.width == 0 || size.height == 0 { - log::error!("The window was somehow set to a size of zero"); - return; - } - - self.surface_config.height = size.height; - self.surface_config.width = size.width; - self.camera.set_size(size.width, size.height); - self.reconfigure(); - } - - /// Set the physical window and renderer size - pub fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) { - let size = PhysicalSize::new(width.get(), height.get()); - self.window.set_inner_size(size); - self.resize_renderer(size); - } - - /// Set vsync on or off. See `[RenderWindowConfig::present_mode]` for more details. - pub fn set_vsync(&mut self, vsync: bool) { - self.surface_config.present_mode = - RenderWindowConfig::present_mode(vsync, &self.supported_present_modes); - self.reconfigure(); - } - - /// Set the window's title - pub fn set_title(&mut self, title: &str) { - self.window.set_title(title); - } - - /// The reference buffer - pub const fn instances(&self) -> &InstanceBuffer { - &self.instances - } - - /// The reference buffer - pub fn instances_mut(&mut self) -> &mut InstanceBuffer { - &mut self.instances - } - - /// Get the camera information - pub const fn camera(&self) -> &Camera { - &self.camera - } - - /// Get a mutable reference to the camera - pub fn camera_mut(&mut self) -> &mut Camera { - &mut self.camera - } - - /// Get a reference to the texture atlas - pub const fn textures(&self) -> &TextureAtlas { - &self.textures - } - - /// Get a mutable reference to the texture atlas - pub fn textures_mut(&mut self) -> &mut TextureAtlas { - &mut self.textures - } - - /// Renders a new frame to the window - /// - /// # Errors - /// - /// A number of problems could occur here. A timeout could occur while - /// trying to acquire the next frame. There may also be no more memory left - /// that can be used for the new frame. - // TODO this needs to be smaller - // TODO don't return wgpu errors - #[profiling::function] - fn render(&mut self) -> Result<(), wgpu::SurfaceError> { - // this will allow us to send commands to the gpu - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Render Encoder"), - }); - - let num_instances = self.instances.len(); - self.instances.fill_buffer(&self.device, &self.queue); - self.camera.refresh(&self.queue); - self.textures.fill_textures(&self.queue); - - // the new texture we can render to - let output = self.surface.get_current_texture()?; - let view = output - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - { - profiling::scope!("encode render pass"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: true, - }, - })], - depth_stencil_attachment: None, - }); - - render_pass.set_pipeline(&self.render_pipeline); - render_pass.set_bind_group(0, self.camera.bind_group(), &[]); - render_pass.set_bind_group(1, self.textures.bind_group(), &[]); - render_pass.set_vertex_buffer(0, self.square_vertex_buffer.slice(..)); - render_pass.set_vertex_buffer(1, self.instances.buffer_slice()); - render_pass.draw(0..self.square_vertices, 0..num_instances); - } - // the encoder can't finish building the command buffer until the - // render pass is dropped - - // submit the command buffer to the GPU - profiling::scope!("submit render"); - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); - - Ok(()) - } - - /// Take the event loop out of the Renderer, without moving it - /// - /// # Panics - /// - /// This method must only be called once - // TODO This is a quick fix to get the event loop inside the renderer. - // In the future, we should make a separate struct that contains the - // renderer and the event loop, which we move the event loop out of - // while still being able to move the renderer. - fn event_loop(&mut self) -> EventLoop<()> { - self.event_loop.take().unwrap() - } - - /// Run the renderer indefinitely - // TODO this needs to be smaller - pub fn run(mut self, mut f: F) -> ! { - self.window.set_visible(true); - let event_loop = self.event_loop(); - event_loop.run(move |event, _, control_flow| match event { - Event::WindowEvent { window_id, event } => { - if window_id == self.window.id() { - match event { - WindowEvent::Resized(size) => self.resize_renderer(size), - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::ExitWithCode(0); - } - _ => (), - } - } - } - Event::MainEventsCleared => { - f(&mut self); - - // a memory leak occurs if we render a zero-size window, - // along with a `SurfaceError::Outdated`. I don't know why that - // happens, but let's make wgpu happy. - // https://github.com/gfx-rs/wgpu/issues/1783#issuecomment-1328463201 - if self.window.inner_size().width != 0 && self.window.inner_size().height != 0 { - match self.render() { - Ok(_) => {} - // reconfigure the surface if it's been lost - Err(wgpu::SurfaceError::Lost) => { - self.reconfigure(); - } - // if we ran out of memory, then we'll die - Err(wgpu::SurfaceError::OutOfMemory) => { - *control_flow = ControlFlow::ExitWithCode(1); - } - // otherwise, we'll just log the error - Err(e) => log::error!("{}", e), - } - } else { - *control_flow = ControlFlow::Wait; - } - profiling::finish_frame!(); - } - _ => {} - }); - } -} diff --git a/alligator_render/src/texture.rs b/alligator_render/src/texture.rs deleted file mode 100644 index 76e77a8..0000000 --- a/alligator_render/src/texture.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::error::Error; -use std::num::NonZeroU32; -use std::sync::Arc; - -use alligator_resources::texture::{LoadError, Rgba16Texture, TextureId, TextureManager}; -use image::{EncodableLayout, GenericImage, RgbaImage}; -use texture_packer::TexturePacker; -use texture_packer::{ - exporter::{ExportResult, ImageExporter}, - TexturePackerConfig, -}; -use thiserror::Error; - -/// The texture did not fit in the texture atlas -#[derive(Debug, Error)] -#[error("{:?}", .0)] -pub struct PackError(PackErrorInternal); - -// TODO this can be removed when a new texture packer is made -type PackErrorInternal = impl std::fmt::Debug; - -#[derive(Error, Debug)] -pub enum TextureError { - #[error("{:?}", .0)] - TextureTooLarge(#[from] PackError), - #[error("{}", .0)] - BadImage(#[from] LoadError), - #[error("Unexpected Error (this is a bug in alligator_render): {}", .0)] - Unexpected(#[source] Box), -} - -/// Simpler constructor for a wgpu extent3d -const fn extent_3d(width: u32, height: u32) -> wgpu::Extent3d { - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - } -} - -/// A texture atlas, usable by the renderer -// TODO make this Debug -// TODO make these resizable -pub struct TextureAtlas { - textures: Arc, - packer: TexturePacker<'static, Rgba16Texture, TextureId>, - diffuse_texture: wgpu::Texture, - diffuse_bind_group: wgpu::BindGroup, - image: RgbaImage, - width: u32, - height: u32, - changed: bool, -} - -macro_rules! texture_info { - ($name: ident, $prop: ident, $divisor: ident) => { - pub fn $name(&mut self, id: TextureId) -> Result { - let frame = match self.texture_frame(id) { - Some(frame) => frame, - None => { - self.load_texture(id)?; - self.texture_frame(id).unwrap() - } - }; - let property = frame.frame.$prop; - let value = property as f32 / self.$divisor as f32; - Ok(value) - } - }; -} - -impl TextureAtlas { - /// Creates a new texture atlas, with the given size - // TODO why is this u32? - // TODO this is still too large - pub fn new( - device: &wgpu::Device, - textures: Arc, - width: u32, - height: u32, - ) -> (Self, wgpu::BindGroupLayout) { - let atlas_size = extent_3d(width, height); - let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("Diffuse Texture"), - size: atlas_size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], - }); - - // TODO I don't think this refreshes anything - let diffuse_texture_view = - diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); - - let texture_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Texture Bind Group Layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - - let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Diffuse Bind Group"), - layout: &texture_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse_sampler), - }, - ], - }); - - ( - Self { - textures, - packer: TexturePacker::new_skyline(TexturePackerConfig { - max_width: width, - max_height: height, - allow_rotation: false, - trim: false, - texture_padding: 0, - ..Default::default() - }), - diffuse_texture, - diffuse_bind_group, - width, - height, - image: RgbaImage::from_raw( - width, - height, - vec![0; 4 * width as usize * height as usize], - ) - .unwrap(), - changed: true, - }, - texture_bind_group_layout, - ) - } - - /// get the bind group for the texture - pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup { - &self.diffuse_bind_group - } - - /// Load a new subtexture from memory - pub fn load_texture(&mut self, id: TextureId) -> Result { - self.changed = true; - let img = self.textures.load_texture(id)?; - self.packer.pack_own(id, img).map_err(PackError)?; - Ok(id) - } - - /// Get the frame for s particular subtexture - fn texture_frame(&self, id: TextureId) -> Option<&texture_packer::Frame> { - self.packer.get_frame(&id) - } - - texture_info!(texture_width, w, width); - texture_info!(texture_height, h, height); - texture_info!(texture_x, x, width); - texture_info!(texture_y, y, height); - - /// Fill the cached image - fn fill_image(&mut self) -> ExportResult<()> { - let atlas = { - profiling::scope!("export atlas"); - ImageExporter::export(&self.packer)? - }; - profiling::scope!("copy image"); - self.image - .copy_from(&atlas, 0, 0) - .expect("image cache is too small"); - Ok(()) - } - - /// Clear the texture atlas, and give it a new size - pub fn clear(&mut self, width: u32, height: u32) { - self.changed = true; - self.width = width; - self.height = height; - self.packer = TexturePacker::new_skyline(TexturePackerConfig { - max_width: self.width, - max_height: self.height, - ..Default::default() - }); - } - - /// Fill the GPU texture atlas - #[profiling::function] - pub(crate) fn fill_textures(&mut self, queue: &wgpu::Queue) { - // saves time if nothing changed since the last time we did this - // FIXME This doesn't do much good once we get procedurally generated animation - // We'll have to create our own texture packer, with mutable subtextures, - // and more efficient algorithms. This'll also make frame times more consistent - if !self.changed { - return; - } - - let atlas_size = extent_3d(self.width, self.height); - - // put the packed texture into the base image - if let Err(e) = self.fill_image() { - log::error!("{}", e); - } - - // copy that to the gpu - queue.write_texture( - wgpu::ImageCopyTexture { - texture: &self.diffuse_texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - self.image.as_bytes(), - wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: NonZeroU32::new(atlas_size.width * 4), - rows_per_image: NonZeroU32::new(atlas_size.height), - }, - atlas_size, - ); - - self.changed = false; - } -} diff --git a/alligator_render/src/vertex.rs b/alligator_render/src/vertex.rs deleted file mode 100644 index 570eec4..0000000 --- a/alligator_render/src/vertex.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::mem::size_of; - -use bytemuck::{Pod, Zeroable}; - -/// The vertices needed to form a square -pub const SQUARE: [Vertex; 6] = [ - Vertex::new(-0.5, -0.5), - Vertex::new(0.5, -0.5), - Vertex::new(-0.5, 0.5), - Vertex::new(0.5, 0.5), - Vertex::new(-0.5, 0.5), - Vertex::new(0.5, -0.5), -]; - -/// A vertex that is usable by the alligator shader -#[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)] -pub struct Vertex { - position: [f32; 2], -} - -impl Vertex { - // whenever this is updated, please also update `sprite.wgsl` - pub(crate) const ATTRIBUTES: [wgpu::VertexAttribute; 1] = - wgpu::vertex_attr_array![0 => Float32x2]; - - /// Create a new vertex - const fn new(x: f32, y: f32) -> Self { - Self { position: [x, y] } - } - - pub(crate) const fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - wgpu::VertexBufferLayout { - array_stride: size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &Self::ATTRIBUTES, - } - } -} diff --git a/alligator_resources/Cargo.toml b/alligator_resources/Cargo.toml deleted file mode 100644 index d5d8276..0000000 --- a/alligator_resources/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "alligator_resources" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -image = "0.24" -thiserror = "1" -exun = "0.1" -texture_packer = { git = "https://github.com/botahamec/piston_texture_packer", branch = "u16" } -profiling = "1" -bytemuck = { version = "1", features = ["extern_crate_alloc"] } -parking_lot = "0.12" -log = "0.4" -dashmap = "5" diff --git a/alligator_resources/src/lib.rs b/alligator_resources/src/lib.rs deleted file mode 100644 index 9cbbba0..0000000 --- a/alligator_resources/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![feature(new_uninit, let_chains)] -#![warn(clippy::nursery, clippy::pedantic)] -#![allow(clippy::module_name_repetitions)] - -pub mod texture; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Priority { - Unnecessary, - Possible(u8), - Eventual(u8), - Urgent, -} diff --git a/alligator_resources/src/texture.rs b/alligator_resources/src/texture.rs deleted file mode 100644 index 3a5bf3e..0000000 --- a/alligator_resources/src/texture.rs +++ /dev/null @@ -1,394 +0,0 @@ -use std::cmp::Reverse; -use std::mem; -use std::path::Path; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::Arc; - -use dashmap::DashMap; -use image::ImageBuffer; -use parking_lot::Mutex; -use thiserror::Error; - -use crate::Priority; - -/// The next texture ID -static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); - -/// A unique identifier for a texture -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct TextureId(usize); - -impl TextureId { - fn new() -> Self { - Self(NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed)) - } -} - -/// These are the formats supported by the renderer. -// TODO make these feature-enabled -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum ImageFormat { - Bmp, - Ico, - Farbfeld, -} - -impl From for image::ImageFormat { - fn from(format: ImageFormat) -> Self { - match format { - ImageFormat::Bmp => Self::Bmp, - ImageFormat::Ico => Self::Ico, - ImageFormat::Farbfeld => Self::Farbfeld, - } - } -} - -#[derive(Debug, Error)] -#[error("{}", .0)] -pub struct DecodingError(#[from] image::error::DecodingError); - -#[allow(clippy::missing_const_for_fn)] -fn convert_image_decoding(e: image::ImageError) -> DecodingError { - if let image::ImageError::Decoding(de) = e { - de.into() - } else { - unreachable!("No other error should be possible") - } -} - -#[derive(Debug, Error)] -pub enum LoadError { - #[error("{}", .0)] - Decoding(#[from] DecodingError), - #[error("{}", .0)] - Io(#[from] std::io::Error), -} - -fn convert_image_load_error(e: image::ImageError) -> LoadError { - match e { - image::ImageError::Decoding(de) => LoadError::Decoding(de.into()), - image::ImageError::IoError(ioe) => ioe.into(), - _ => unreachable!("No other error should be possible"), - } -} - -pub type Rgba16Texture = image::ImageBuffer, Box<[u16]>>; - -fn vec_image_to_box(vec_image: image::ImageBuffer, Vec>) -> Rgba16Texture { - let width = vec_image.width(); - let height = vec_image.height(); - let buf = vec_image.into_raw().into_boxed_slice(); - ImageBuffer::from_raw(width, height, buf).expect("image buffer is too small") -} - -/// Get the size, in bytes, of the texture -#[allow(clippy::missing_const_for_fn)] -fn texture_size(image: &Rgba16Texture) -> usize { - image.len() * mem::size_of::>() -} - -/// A texture from disk -struct TextureFile { - path: Box, - texture: Option>, -} - -impl TextureFile { - /// This doesn't load the texture - #[allow(clippy::missing_const_for_fn)] - fn new(path: impl AsRef) -> Self { - Self { - path: path.as_ref().into(), - texture: None, - } - } - - const fn is_loaded(&self) -> bool { - self.texture.is_some() - } - - fn load(&mut self) -> Result<&Rgba16Texture, LoadError> { - if self.texture.is_none() { - log::warn!("{} was not pre-loaded", self.path.to_string_lossy()); - let texture = image::open(&self.path).map_err(convert_image_load_error)?; - let texture = texture.to_rgba16(); - let texture = Arc::new(vec_image_to_box(texture)); - self.texture = Some(texture); - } - - Ok(self.texture.as_ref().expect("the texture wasn't loaded")) - } - - fn loaded_texture(&self) -> Option<&Rgba16Texture> { - self.texture.as_deref() - } - - fn is_used(&self) -> bool { - let Some(arc) = &self.texture else { return false }; - Arc::strong_count(arc) > 1 - } - - /// Unloads the texture from memory if it isn't being used - fn unload(&mut self) { - if !self.is_used() { - self.texture = None; - } - } - - /// The amount of heap memory used, in bytes. This returns 0 if the texture - /// hasn't been loaded yet. - fn allocated_size(&self) -> usize { - self.texture.as_ref().map_or(0, |t| texture_size(t)) - } -} - -enum TextureBuffer { - Memory(Arc), - Disk(TextureFile), -} - -struct Texture { - priority: Priority, - queued_priority: Arc>>, - buffer: TextureBuffer, -} - -impl Texture { - fn from_buffer(texture: Rgba16Texture) -> Self { - Self { - priority: Priority::Urgent, // indicates that it can't be unloaded - queued_priority: Arc::new(Mutex::new(None)), - buffer: TextureBuffer::Memory(Arc::new(texture)), - } - } - - fn from_path(path: impl AsRef, priority: Priority) -> Self { - Self { - priority, - queued_priority: Arc::new(Mutex::new(None)), - buffer: TextureBuffer::Disk(TextureFile::new(path)), - } - } - - const fn priority(&self) -> Priority { - self.priority - } - - fn _set_priority(buffer: &TextureBuffer, src: &mut Priority, priority: Priority) -> bool { - // memory textures and textures in use should always be urgent - if let TextureBuffer::Disk(disk) = buffer && !disk.is_used() { - *src = priority; - true - } else { - false - } - } - - fn unqueue_priority(&mut self) { - let mut queued_priority = self.queued_priority.lock(); - let unqueued_priority = queued_priority.unwrap_or(Priority::Unnecessary); - - if Self::_set_priority(&self.buffer, &mut self.priority, unqueued_priority) { - *queued_priority = None; - } - } - - fn set_priority(&mut self, priority: Priority) { - Self::_set_priority(&self.buffer, &mut self.priority, priority); - } - - fn load_texture(&mut self) -> Result<&Rgba16Texture, LoadError> { - match &mut self.buffer { - TextureBuffer::Memory(ref texture) => Ok(texture), - TextureBuffer::Disk(file) => file.load(), - } - } - - /// If the texture is loaded, return it. - fn loaded_texture(&self) -> Option<&Rgba16Texture> { - match &self.buffer { - TextureBuffer::Memory(ref texture) => Some(texture), - TextureBuffer::Disk(file) => file.loaded_texture(), - } - } - - fn unload(&mut self) { - if let TextureBuffer::Disk(file) = &mut self.buffer { - file.unload(); - } - } - - /// The amount of heap memory used for the texture, if any - fn allocated_size(&self) -> usize { - match &self.buffer { - TextureBuffer::Memory(texture) => texture_size(texture), - TextureBuffer::Disk(file) => file.allocated_size(), - } - } - - const fn is_loaded(&self) -> bool { - match &self.buffer { - TextureBuffer::Memory(_) => true, - TextureBuffer::Disk(file) => file.is_loaded(), - } - } -} - -pub struct TextureManager { - textures: DashMap, - max_size: usize, - needs_atlas_update: AtomicBool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct TextureManagerConfig { - /// The initial capacity of the texture manager. This defaults to 500 textures. - pub initial_capacity: usize, - /// The maximum amount of heap usage acceptable. Defaults to 10 MiB. - pub max_size: usize, -} - -impl Default for TextureManagerConfig { - fn default() -> Self { - Self { - initial_capacity: 500, - max_size: 10 * 1024 * 1024, // 10 MiB - } - } -} - -impl TextureManager { - /// Create a new `TextureManager` with the given config options. - #[must_use] - pub fn new(config: &TextureManagerConfig) -> Self { - let textures = DashMap::with_capacity(config.initial_capacity); - - Self { - textures, - max_size: config.max_size, - needs_atlas_update: AtomicBool::new(false), - } - } - - /// Load textures into memory that will be needed soon. Unload unnecessary textures - pub fn cache_files(&self) { - let mut textures: Vec<_> = self - .textures - .iter_mut() - .map(|mut t| { - t.value_mut().unqueue_priority(); - t - }) - .collect(); - textures.sort_by_key(|t2| Reverse(t2.priority())); - - let max_size = self.max_size; - let mut total_size = 0; - - for texture in &mut textures { - drop(texture.load_texture()); - total_size += texture.allocated_size(); - if total_size > max_size && texture.priority() != Priority::Urgent { - texture.unload(); - return; - } - } - } - - /// Loads a texture from memory in the given format. - /// - /// # Errors - /// - /// This returns `Expected(DecodingError)` if the given buffer was invalid - /// for the given format. - pub fn load_from_memory( - &self, - buf: &[u8], - format: ImageFormat, - ) -> Result { - let id = TextureId::new(); - let texture = image::load_from_memory_with_format(buf, format.into()); - let texture = texture.map_err(convert_image_decoding)?; - let texture = texture.into_rgba16(); - let texture = vec_image_to_box(texture); - let texture = Texture::from_buffer(texture); - - self.textures.insert(id, texture); - self.needs_atlas_update.store(true, Ordering::Release); - - Ok(id) - } - - /// Loads a texture from disk. - /// - /// # Errors - /// - /// This returns an error if `priority` is set to [`Priority::Urgent`] but - /// there was an error in loading the file to a texture. - pub fn load_from_file( - &self, - path: impl AsRef, - priority: Priority, - ) -> Result { - let id = TextureId::new(); - let mut texture = Texture::from_path(path, priority); - - if priority == Priority::Urgent { - match texture.load_texture() { - Ok(_) => { - self.textures.insert(id, texture); - self.needs_atlas_update.store(true, Ordering::Release); - } - Err(e) => { - self.textures.insert(id, texture); - return Err(e); - } - } - } else { - self.textures.insert(id, texture); - } - - Ok(id) - } - - /// Loads a texture from disk. - /// - /// # Errors - /// - /// This returns an error if `priority` is set to [`Priority::Urgent`] but - /// there was an error in loading the file to a texture. - pub fn set_priority(&self, id: TextureId, priority: Priority) -> Result<(), LoadError> { - let mut texture = self.textures.get_mut(&id).expect("invalid texture id"); - texture.set_priority(priority); - - if !texture.is_loaded() && priority == Priority::Urgent { - let mut texture = self.textures.get_mut(&id).expect("invalid texture id"); - texture.load_texture()?; - self.needs_atlas_update.store(true, Ordering::Release); - } - - Ok(()) - } - - /// This returns `true` if a texture has been set to have an urgent - /// priority since the last time this function was called. - pub fn needs_atlas_update(&self) -> bool { - self.needs_atlas_update.fetch_and(false, Ordering::AcqRel) - } - - /// Load a texture into memory, if it hasn't been already. Then return a - /// copy of the texture. - /// - /// # Errors - /// - /// This returns an error if an error occurs in loading the texture from - /// disk, such as the file not existing, or not being a valid texture. - pub fn load_texture(&self, id: TextureId) -> Result { - self.textures - .get_mut(&id) - .expect("the TextureId was invalid") - .load_texture() - .cloned() - } -} diff --git a/alligator_tvg/Cargo.toml b/alligator_tvg/Cargo.toml deleted file mode 100644 index d7e7a6b..0000000 --- a/alligator_tvg/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "alligator_tvg" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -byteorder = "1" -thiserror = "1" -raise = "2" -num_enum = "0.5" diff --git a/alligator_tvg/src/colors.rs b/alligator_tvg/src/colors.rs deleted file mode 100644 index 10bc41c..0000000 --- a/alligator_tvg/src/colors.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::io::{self, Read}; - -use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; -use num_enum::TryFromPrimitive; - -/// The color table encodes the palette for this file. -/// -/// It’s binary content is defined by the `color_encoding` field in the header. -/// For the three defined color encodings, each will yield a list of -/// `color_count` RGBA tuples. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ColorTable { - colors: Box<[C]>, -} - -impl ColorTable { - /// Read in one encoding, and convert it to another - pub fn read_from_encoding( - reader: &mut impl Read, - color_count: u32, - encoding: ColorEncoding, - ) -> io::Result { - Ok(match encoding { - ColorEncoding::Rgba8888 => (&ColorTable::::read(reader, color_count)?).into(), - ColorEncoding::Rgb565 => (&ColorTable::::read(reader, color_count)?).into(), - ColorEncoding::RgbaF32 => (&ColorTable::::read(reader, color_count)?).into(), - ColorEncoding::Custom => (&ColorTable::::read(reader, color_count)?).into(), - }) - } - - /// Parse a color table. - fn read(reader: &mut impl Read, color_count: u32) -> io::Result { - let mut colors = Vec::with_capacity(color_count as usize); - for _ in 0..color_count { - colors.push(C::parse_bytes(reader)?); - } - - let colors = colors.into_boxed_slice(); - Ok(Self { colors }) - } - - /// Returns the number of colors in the table. - fn len(&self) -> usize { - self.colors.len() - } - - /// Returns a reference to a color, or `None` if out-of-bounds. - fn get(&self, index: usize) -> Option<&C> { - self.colors.get(index) - } - - fn iter(&self) -> impl Iterator { - self.colors.iter() - } -} - -impl ColorTable {} - -impl From<&ColorTable> for ColorTable { - fn from(value: &ColorTable) -> Self { - let mut colors = Vec::with_capacity(value.len()); - - for color in value.iter() { - let r = color.red_u16(); - let g = color.green_u16(); - let b = color.blue_u16(); - let a = color.alpha_u16(); - colors.push(New::from_rgba16_lossy(r, g, b, a)); - } - - let colors = colors.into_boxed_slice(); - Self { colors } - } -} - -/// The color encoding defines which format the colors in the color table will -/// have. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u8)] -pub enum ColorEncoding { - /// Each color is a 4-tuple (red, green, blue, alpha) of bytes with the - /// color channels encoded in sRGB and the alpha as linear alpha. - Rgba8888 = 0, - /// Each color is encoded as a 3-tuple (red, green, blue) with 16 bits per - /// color. - /// - /// While red and blue both use 5 bits, the green channel uses 6 bits. Red - /// uses bit range 0...4, green bits 5...10 and blue bits 11...15. This - /// color also uses the sRGB color space. - Rgb565 = 1, - /// Each color is a 4-tuple (red, green, blue, alpha) of binary32 IEEE 754 - /// floating point value with the color channels encoded in scRGB and the - /// alpha as linear alpha. A color value of 1.0 is full intensity, while a - /// value of 0.0 is zero intensity. - RgbaF32 = 2, - /// The custom color encoding is defined *undefined*. The information how - /// these colors are encoded must be implemented via external means. - Custom = 3, -} - -pub trait Color: Sized { - /// The size of the color's representation in bits - const SIZE: usize; - - /// Attempt to read the color. Returns `Err` if an error occurred while - /// attempting to read [`SIZE`] bytes from `bytes`. - fn parse_bytes(reader: &mut impl Read) -> io::Result; - - /// Convert from the RGBA16 format to this format. This may be lossy. - fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self; - - fn red_u16(&self) -> u16; - fn blue_u16(&self) -> u16; - fn green_u16(&self) -> u16; - fn alpha_u16(&self) -> u16; -} - -/// Each color value is encoded as a sequence of four bytes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct Rgba8888 { - /// Red color channel between 0 and 100% intensity, mapped to byte values 0 - /// to 255. - red: u8, - /// Green color channel between 0 and 100% intensity, mapped to byte values - /// 0 to 255. - green: u8, - /// Blue color channel between 0 and 100% intensity, mapped to byte values - /// 0 to 255. - blue: u8, - /// Transparency channel between 0 and 100% transparency, mapped to byte - /// values 0 to 255. - alpha: u8, -} - -impl Color for Rgba8888 { - const SIZE: usize = 4; - - fn parse_bytes(reader: &mut impl Read) -> io::Result { - Ok(Self { - red: reader.read_u8()?, - green: reader.read_u8()?, - blue: reader.read_u8()?, - alpha: reader.read_u8()?, - }) - } - - fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self { - Self { - red: (red >> 8) as u8, - green: (green >> 8) as u8, - blue: (blue >> 8) as u8, - alpha: (alpha >> 8) as u8, - } - } - - fn red_u16(&self) -> u16 { - (self.red as u16) << 8 - } - - fn green_u16(&self) -> u16 { - (self.green as u16) << 8 - } - - fn blue_u16(&self) -> u16 { - (self.blue as u16) << 8 - } - - fn alpha_u16(&self) -> u16 { - (self.alpha as u16) << 8 - } -} - -/// Each color value is encoded as a sequence of 2 bytes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct Rgb565 { - /// Red color channel between 0 and 100% intensity, mapped to integer - /// values 0 to 31. - red: u8, - /// Green color channel between 0 and 100% intensity, mapped to integer - /// values 0 to 63. - green: u8, - /// Blue color channel between 0 and 100% intensity, mapped to integer - /// values 0 to 31. - blue: u8, -} - -impl Color for Rgb565 { - const SIZE: usize = 2; - - fn parse_bytes(reader: &mut impl Read) -> io::Result { - let color = reader.read_u16::()?; - - let red = ((color & 0x001F) << 3) as u8; - let green = ((color & 0x07E0) >> 3) as u8; - let blue = ((color & 0xF800) >> 8) as u8; - - Ok(Self { red, blue, green }) - } - - fn from_rgba16_lossy(red: u16, green: u16, blue: u16, _a: u16) -> Self { - Self { - red: (red >> 11) as u8, - green: (green >> 10) as u8, - blue: (blue >> 11) as u8, - } - } - - fn red_u16(&self) -> u16 { - (self.red as u16) << 11 - } - - fn green_u16(&self) -> u16 { - (self.green as u16) << 11 - } - - fn blue_u16(&self) -> u16 { - (self.blue as u16) << 10 - } - - fn alpha_u16(&self) -> u16 { - 0 - } -} - -/// Each color value is encoded as a sequence of 16 bytes. -#[derive(Debug, Clone, Copy, PartialEq)] -struct RgbaF32 { - /// Red color channel, using 0.0 for 0% intensity and 1.0 for 100% - /// intensity. - red: f32, - /// Green color channel, using 0.0 for 0% intensity and 1.0 for 100% - /// intensity. - green: f32, - /// Blue color channel, using 0.0 for 0% intensity and 1.0 for 100% - /// intensity. - blue: f32, - /// Transparency channel between 0 and 100% transparency, mapped to byte - /// values 0.0 to 1.0. - alpha: f32, -} - -impl Color for RgbaF32 { - const SIZE: usize = 16; - - fn parse_bytes(reader: &mut impl Read) -> io::Result { - Ok(Self { - red: reader.read_f32::()?, - green: reader.read_f32::()?, - blue: reader.read_f32::()?, - alpha: reader.read_f32::()?, - }) - } - - fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self { - Self { - red: (red as f32) / (u16::MAX as f32), - green: (green as f32) / (u16::MAX as f32), - blue: (blue as f32) / (u16::MAX as f32), - alpha: (alpha as f32) / (u16::MAX as f32), - } - } - - fn red_u16(&self) -> u16 { - (self.red * (u16::MAX as f32)) as u16 - } - - fn green_u16(&self) -> u16 { - (self.green * (u16::MAX as f32)) as u16 - } - - fn blue_u16(&self) -> u16 { - (self.blue * (u16::MAX as f32)) as u16 - } - - fn alpha_u16(&self) -> u16 { - (self.alpha * (u16::MAX as f32)) as u16 - } -} - -/// Each color value is encoded as a sequence of 8 bytes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Rgba16 { - /// Red color channel between 0 and 100% intensity, mapped to a big-endian - /// 16-bit integer. - red: u16, - /// Green color channel between 0 and 100% intensity, mapped to a - /// big-endian 16-bit integer. - green: u16, - /// Blue color channel between 0 and 100% intensity, mapped to a big-endian - /// 16-bit integer. - blue: u16, - /// Transparency channel between 0 and 100% intensity, mapped to a - /// big-endian 16-bit integer. - alpha: u16, -} - -impl Color for Rgba16 { - const SIZE: usize = 8; - - fn parse_bytes(reader: &mut impl Read) -> io::Result { - Ok(Self { - red: reader.read_u16::()?, - green: reader.read_u16::()?, - blue: reader.read_u16::()?, - alpha: reader.read_u16::()?, - }) - } - - fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self { - Self { - red, - green, - blue, - alpha, - } - } - - fn red_u16(&self) -> u16 { - self.red - } - - fn green_u16(&self) -> u16 { - self.green - } - - fn blue_u16(&self) -> u16 { - self.blue - } - - fn alpha_u16(&self) -> u16 { - self.alpha - } -} diff --git a/alligator_tvg/src/commands.rs b/alligator_tvg/src/commands.rs deleted file mode 100644 index f316a53..0000000 --- a/alligator_tvg/src/commands.rs +++ /dev/null @@ -1,613 +0,0 @@ -use std::io::{self, Read}; - -use byteorder::ReadBytesExt; -use raise::yeet; - -use crate::{header::TvgHeader, path::Path, read_unit, read_varuint, Decode, Point, TvgError}; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Rectangle { - /// Horizontal distance of the left side to the origin. - x: f64, - /// Vertical distance of the upper side to the origin. - y: f64, - /// Horizontal extent of the rectangle. - width: f64, - /// Vertical extent of the rectangle. - height: f64, -} - -impl Decode for Rectangle { - fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self { - x: read_unit(reader, header)?, - y: read_unit(reader, header)?, - width: read_unit(reader, header)?, - height: read_unit(reader, header)?, - }) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Line { - /// Start point of the line. - start: Point, - /// End point of the line. - end: Point, -} - -impl Decode for Line { - fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self { - start: Point::read(reader, header)?, - end: Point::read(reader, header)?, - }) - } -} - -/// A command that can be encoded. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(u8)] -enum CommandName { - /// Determines end of file. - EndOfDocument = 0, - /// Fills an N-gon. - FillPolygon = 1, - /// Fills a set of [`Rectangle`]s. - FillRectangles = 2, - /// Fills a free-form [`Path`]. - FillPath = 3, - /// Draws a set of lines. - DrawLines = 4, - /// Draws the outline of a polygon. - DrawLineLoop = 5, - /// Draws a list of end-to-end lines. - DrawLineStrip = 6, - /// Draws a free-form [`Path`]. - DrawLinePath = 7, - /// Draws a filled polygon with an outline. - OutlineFillPolygon = 8, - /// Draws several filled [`Rectangle`]s with an outline. - OutlineFillRectangles = 9, - /// This command combines the [`FillPath`] and [`DrawLinePath`] command - /// into one. - OutlineFillPath = 10, -} - -impl TryFrom for CommandName { - type Error = TvgError; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::EndOfDocument), - 1 => Ok(Self::FillPolygon), - 2 => Ok(Self::FillRectangles), - 3 => Ok(Self::FillPath), - 4 => Ok(Self::DrawLines), - 5 => Ok(Self::DrawLineLoop), - 6 => Ok(Self::DrawLineStrip), - 7 => Ok(Self::DrawLinePath), - 8 => Ok(Self::OutlineFillPolygon), - 9 => Ok(Self::OutlineFillRectangles), - 10 => Ok(Self::OutlineFillPath), - v => Err(TvgError::InvalidCommand(v)), - } - } -} - -/// The type of style the command uses as a primary style. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(u8)] -pub enum StyleKind { - /// The shape is uniformly colored with a single color. - FlatColored = 0, - /// The shape is colored with a linear gradient. - LinearGradient = 1, - /// The shape is colored with a radial gradient. - RadialGradient = 2, -} - -impl TryFrom for StyleKind { - type Error = TvgError; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::FlatColored), - 1 => Ok(Self::LinearGradient), - 2 => Ok(Self::RadialGradient), - _ => Err(TvgError::InvalidStyleKind), - } - } -} - -/// The style kind, along with the colors and points used to render the style. -#[derive(Debug, Clone, Copy)] -pub enum Style { - /// The shape is uniformly colored with the color at `color_index` in the - /// [`ColorTable`]. - FlatColored { - /// The index in the [`ColorTable`]. - color_index: u32, - }, - /// The gradient is formed by a mental line between `point_0` and - /// `point_1`. - /// - /// The color at `point_0` is the color at `color_index_0` in the color - /// table. The color at `point_1` is the color at `color_index_1` in the - /// [`ColorTable`]. On the line, the color is interpolated between the two - /// points. Each point that is not on the line is orthogonally projected to - /// the line and the color at that point is sampled. Points that are not - /// projectable onto the line have either the color at `point_0` if they - /// are closed to `point_0` or vice versa for `point_1`. - LinearGradient { - /// The start point of the gradient. - point_0: Point, - /// The end point of the gradient. - point_1: Point, - /// The color at [`point_0`]. - color_index_0: u32, - /// The color at [`point_1`]. - color_index_1: u32, - }, - /// The gradient is formed by a mental circle with the center at `point_0` - /// and `point_1` being somewhere on the circle outline. Thus, the radius - /// of said circle is the distance between `point_0` and `point_1`. - /// - /// The color at `point_0` is the color at `color_index_0` in the color - /// table. The color on the circle outline is the color at `color_index_1` - /// in the [`ColorTable`]. If a sampled point is inside the circle, the - /// color is interpolated based on the distance to the center and the - /// radius. If the point is not in the circle itself, the color at - /// `color_index_1` is always taken. - RadialGradient { - /// The center point of the mental circle. - point_0: Point, - /// The end point of the gradient. - point_1: Point, - /// The color at `point_0`. - color_index_0: u32, - /// The color at `point_1`. - color_index_1: u32, - }, -} - -impl Style { - fn read(reader: &mut impl Read, header: &TvgHeader, kind: StyleKind) -> io::Result { - match kind { - StyleKind::FlatColored => Self::read_flat_colored(reader), - StyleKind::LinearGradient => Self::read_linear_gradient(reader, header), - StyleKind::RadialGradient => Self::read_radial_gradient(reader, header), - } - } - - fn read_flat_colored(reader: &mut impl Read) -> io::Result { - Ok(Self::FlatColored { - color_index: read_varuint(reader)?, - }) - } - - fn read_linear_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self::LinearGradient { - point_0: Point::read(reader, header)?, - point_1: Point::read(reader, header)?, - color_index_0: read_varuint(reader)?, - color_index_1: read_varuint(reader)?, - }) - } - - fn read_radial_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self::RadialGradient { - point_0: Point::read(reader, header)?, - point_1: Point::read(reader, header)?, - color_index_0: read_varuint(reader)?, - color_index_1: read_varuint(reader)?, - }) - } -} - -/// TinyVG files contain a sequence of draw commands that must be executed in -/// the defined order to get the final result. Each draw command adds a new 2D -/// primitive to the graphic. -#[derive(Debug, Clone)] -pub enum Command { - /// If this command is read, the TinyVG file has ended. - /// - /// This command must have prim_style_kind to be set to 0, so the last byte - /// of every TinyVG file is `0x00`. Every byte after this command is - /// considered not part of the TinyVG data and can be used for other - /// purposes like metadata or similar. - EndOfDocument, - /// Fills a polygon with N [`Point`]s. - /// - /// The number of points must be at least 3. Files that encode a lower - /// value must be discarded as ”invalid” by a conforming implementation. - /// - /// The polygon specified in polygon must be drawn using the even-odd rule. - /// That means that if for any point to be inside the polygon, a line to - /// infinity must cross an odd number of polygon segments. - FillPolygon { - /// The style that is used to fill the polygon. - fill_style: Style, - /// The points of the polygon. - polygon: Box<[Point]>, - }, - /// Fills a list of [`Rectangle`]s. - /// - /// The rectangles must be drawn first to last, which is the order they - /// appear in the file. - FillRectangles { - /// The style that is used to fill all rectangles. - fill_style: Style, - /// The list of rectangles to be filled. - rectangles: Box<[Rectangle]>, - }, - /// Fills a [`Path`]. - /// - /// For the filling, all path segments are considered a polygon each (drawn - /// with even-odd rule) that, when overlap, also perform the even odd rule. - /// This allows the user to carve out parts of the path and create - /// arbitrarily shaped surfaces. - FillPath { - /// The style that is used to fill the path. - fill_style: Style, - /// A [`Path`] with `segment_count` segments. - path: Path, - }, - /// Draws a set of [`Line`]s. - /// - /// Each line is `line_width` units wide, and at least a single display - /// pixel. This means that line_width of 0 is still visible, even though - /// only marginally. This allows very thin outlines. - DrawLines { - /// The style that is used to draw the all lines. - line_style: Style, - /// The width of the lines. - line_width: f64, - /// The list of lines. - lines: Box<[Line]>, - }, - /// Draws a polygon. - /// - /// Each line is `line_width` units wide. The lines are drawn between - /// consecutive points as well as the first and the last point. - DrawLineLoop { - /// The style that is used to draw the all lines. - line_style: Style, - /// The width of the line. - line_width: f64, - /// The points of the polygon. - points: Box<[Point]>, - }, - /// Draws a list of consecutive lines. - /// - /// The lines are drawn between consecutive points, but contrary to - /// [`DrawLineLoop`], the first and the last point are not connected. - DrawLineStrip { - /// The style that is used to draw the all rectangles. - line_style: Style, - /// The width of the line. - line_width: f64, - /// The points of the line strip. - points: Box<[Point]>, - }, - /// Draws a [`Path`]. - /// - /// The outline of the path is `line_width` units wide. - DrawLinePath { - /// The style that is used to draw the path. - line_style: Style, - /// The width of the line. - line_width: f64, - /// A path with `segment_count` segments. - path: Path, - }, - /// Fills a polygon and draws an outline at the same time. - /// - /// This command is a combination of [`FillPolygon`] and [`DrawLineLoop`]. - /// It first performs a [`FillPolygon`] with the `fill_style`, then - /// performs [`DrawLineLoop`] with `line_style` and `line_width`. - /// - /// The outline commands use a reduced number of elements. The maximum - /// number of points is 64. - OutlineFillPolygon { - /// The style that is used to fill the polygon. - fill_style: Style, - /// The style that is used to draw the outline of the polygon. - line_style: Style, - /// The width of the line. - line_width: f64, - /// The set of points of this polygon. - points: Box<[Point]>, - }, - /// Fills and outlines a list of [`Rectangle`]s. - /// - /// For each rectangle, it is first filled, then its outline is drawn, then - /// the next rectangle is drawn. - /// - /// The outline commands use a reduced number of elements, the maximum - /// number of points is 64. - OutlineFillRectangles { - /// The style that is used to fill the rectangles. - fill_style: Style, - /// The style that is used to draw the outline of the rectangles. - line_style: Style, - /// The width of the line. - line_width: f64, - /// The list of rectangles to be drawn. - rectangles: Box<[Rectangle]>, - }, - /// Fills a path and draws an outline at the same time. - /// - /// This command is a combination of [`FillPath`] and [`DrawLinePath`]. It - /// first performs a [`FillPath`] with the `fill_style`, then performs - /// [`DrawLinePath`] with `line_style` and `line_width`. - OutlineFillPath { - /// The style that is used to fill the path. - fill_style: Style, - /// The style that is used to draw the outline of the path. - line_style: Style, - /// The width of the line. - line_width: f64, - /// The path that should be drawn. - path: Path, - }, -} - -/// The header is different for outline commands, so we use this as a helper -struct OutlineHeader { - count: u32, - fill_style: Style, - line_style: Style, - line_width: f64, -} - -impl OutlineHeader { - fn read( - reader: &mut impl Read, - header: &TvgHeader, - prim_style_kind: StyleKind, - ) -> Result { - // the count and secondary style kind are stores in the same byte - let byte = reader.read_u8()?; - let count = (byte & 0b0011_1111) as u32 + 1; - let sec_style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?; - - let fill_style = Style::read(reader, header, prim_style_kind)?; - let line_style = Style::read(reader, header, sec_style_kind)?; - - let line_width = read_unit(reader, header)?; - - Ok(Self { - count, - fill_style, - line_style, - line_width, - }) - } -} - -impl Command { - pub fn read(reader: &mut impl Read, header: &TvgHeader) -> Result { - // the command name and primary style kind are stores in the same byte - let byte = reader.read_u8()?; - let command = CommandName::try_from(byte & 0b0011_1111)?; - let style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?; - - match command { - CommandName::EndOfDocument => Self::end_of_document(style_kind), - CommandName::FillPolygon => Self::read_fill_polygon(reader, header, style_kind), - CommandName::FillRectangles => Self::read_fill_rectangles(reader, header, style_kind), - CommandName::FillPath => Self::read_fill_path(reader, header, style_kind), - CommandName::DrawLines => Self::read_draw_lines(reader, header, style_kind), - CommandName::DrawLineLoop => Self::read_draw_line_loop(reader, header, style_kind), - CommandName::DrawLineStrip => Self::read_draw_line_strip(reader, header, style_kind), - CommandName::DrawLinePath => Self::read_draw_line_path(reader, header, style_kind), - CommandName::OutlineFillPolygon => { - Self::read_outline_fill_polygon(reader, header, style_kind) - } - CommandName::OutlineFillRectangles => { - Self::read_outline_fill_rectangles(reader, header, style_kind) - } - CommandName::OutlineFillPath => { - Self::read_outline_fill_path(reader, header, style_kind) - } - } - } - - pub fn is_end_of_document(&self) -> bool { - matches!(self, Self::EndOfDocument) - } - - fn read_command_header( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> io::Result<(u32, Style)> { - // every command adds one to the count - let count = read_varuint(reader)? + 1; - let style = Style::read(reader, header, style_kind)?; - - Ok((count, style)) - } - - fn end_of_document(style_kind: StyleKind) -> Result { - if style_kind != StyleKind::FlatColored { - Err(TvgError::InvalidEndOfDocument(style_kind as u8)) - } else { - Ok(Self::EndOfDocument) - } - } - - fn read_fill_polygon( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let (point_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; - if point_count < 3 { - yeet!(TvgError::InvalidPolygon(point_count)); - } - - let polygon = Point::read_multiple(reader, header, point_count)?; - - Ok(Self::FillPolygon { - fill_style, - polygon, - }) - } - - fn read_fill_rectangles( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let (rectangle_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; - let rectangles = Rectangle::read_multiple(reader, header, rectangle_count)?; - - Ok(Self::FillRectangles { - fill_style, - rectangles, - }) - } - - fn read_fill_path( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let (segment_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; - let path = Path::read(reader, header, segment_count)?; - - Ok(Self::FillPath { fill_style, path }) - } - - fn read_draw_lines( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let (line_count, line_style) = Self::read_command_header(reader, header, style_kind)?; - let line_width = read_unit(reader, header)?; - let lines = Line::read_multiple(reader, header, line_count)?; - - Ok(Self::DrawLines { - line_style, - line_width, - lines, - }) - } - - fn read_draw_line_loop( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?; - let line_width = read_unit(reader, header)?; - let points = Point::read_multiple(reader, header, point_count)?; - - Ok(Self::DrawLineLoop { - line_style, - line_width, - points, - }) - } - - fn read_draw_line_strip( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?; - let line_width = read_unit(reader, header)?; - let points = Point::read_multiple(reader, header, point_count)?; - - Ok(Self::DrawLineStrip { - line_style, - line_width, - points, - }) - } - - fn read_draw_line_path( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let (segment_count, line_style) = Self::read_command_header(reader, header, style_kind)?; - let line_width = read_unit(reader, header)?; - let path = Path::read(reader, header, segment_count)?; - - Ok(Self::DrawLinePath { - line_style, - line_width, - path, - }) - } - - fn read_outline_fill_polygon( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let OutlineHeader { - count: segment_count, - fill_style, - line_style, - line_width, - } = OutlineHeader::read(reader, header, style_kind)?; - - let points = Point::read_multiple(reader, header, segment_count)?; - - Ok(Self::OutlineFillPolygon { - fill_style, - line_style, - line_width, - points, - }) - } - - fn read_outline_fill_rectangles( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let OutlineHeader { - count: rect_count, - fill_style, - line_style, - line_width, - } = OutlineHeader::read(reader, header, style_kind)?; - - let rectangles = Rectangle::read_multiple(reader, header, rect_count)?; - - Ok(Self::OutlineFillRectangles { - fill_style, - line_style, - line_width, - rectangles, - }) - } - - fn read_outline_fill_path( - reader: &mut impl Read, - header: &TvgHeader, - style_kind: StyleKind, - ) -> Result { - let OutlineHeader { - count: segment_count, - fill_style, - line_style, - line_width, - } = OutlineHeader::read(reader, header, style_kind)?; - - let path = Path::read(reader, header, segment_count)?; - - Ok(Self::OutlineFillPath { - fill_style, - line_style, - line_width, - path, - }) - } -} diff --git a/alligator_tvg/src/header.rs b/alligator_tvg/src/header.rs deleted file mode 100644 index b3be494..0000000 --- a/alligator_tvg/src/header.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::io::{self, Read}; - -use byteorder::{LittleEndian, ReadBytesExt}; -use num_enum::TryFromPrimitive; -use raise::yeet; - -use crate::colors::ColorEncoding; -use crate::read_varuint; -use crate::TvgError; - -const MAGIC: [u8; 2] = [0x72, 0x56]; -pub const SUPPORTED_VERSION: u8 = 1; - -/// The coordinate range defines how many bits a Unit value uses. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u8)] -pub enum CoordinateRange { - /// Each [`Unit`] takes up 16 bits. - #[default] - Default = 0, - /// Each [`Unit`] takes up 8 bits. - Reduced = 1, - /// Each [`Unit`] takes up 32 bits. - Enhanced = 2, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u8)] -pub enum Scale { - _1 = 0, - _2 = 1, - _4 = 2, - _8 = 3, - _16 = 4, - _32 = 5, - _64 = 6, - _128 = 7, - _256 = 8, - _512 = 9, - _1024 = 10, - _2048 = 11, - _4096 = 12, - _8192 = 13, - _16384 = 14, - _32768 = 15, -} - -/// Each TVG file starts with a header defining some global values for the file -/// like scale and image size. This is a representation of the header, but not -/// necessarily an exact representation of the bits of a TVG header. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct TvgHeader { - /// Must always be [0x72, 0x56] - magic: [u8; 2], - /// Must be 1. For future versions, this field might decide how the rest of - /// the format looks like. - version: u8, - /// Defines the number of fraction bits in a Unit value. - scale: Scale, - /// Defines the type of color information that is used in the - /// [`ColorTable`]. - color_encoding: ColorEncoding, - /// Defines the number of total bits in a Unit value and thus the overall - /// precision of the file. - coordinate_range: CoordinateRange, - /// Encodes the maximum width of the output file in *display units*. - /// - /// A value of 0 indicates that the image has the maximum possible width. - width: u32, - /// Encodes the maximum height of the output file in *display units*. - /// - /// A value of 0 indicates that the image has the maximum possible height. - height: u32, - /// The number of colors in the color table. - color_count: u32, -} - -impl TvgHeader { - pub fn read(reader: &mut R) -> Result { - // magic number is used as a first line defense against invalid data - let magic = [reader.read_u8()?, reader.read_u8()?]; - if magic != MAGIC { - yeet!(TvgError::InvalidFile); - } - - // the version of tvg being used - let version = reader.read_u8()?; - if version != SUPPORTED_VERSION { - yeet!(TvgError::UnsupportedVersion(version)) - } - - // scale, color_encoding, and coordinate_range are stored in one byte - let byte = reader.read_u8()?; - let scale = Scale::try_from_primitive(byte & 0b0000_1111).expect("invalid scale"); - let color_encoding = ColorEncoding::try_from_primitive((byte & 0b0011_0000) >> 4) - .expect("invalid color encoding"); - let coordinate_range = CoordinateRange::try_from_primitive((byte & 0b1100_0000) >> 6) - .expect("invalid coordinate range"); - - // width and height depend on the coordinate range - let width = read_unsigned_unit(coordinate_range, reader)?; - let height = read_unsigned_unit(coordinate_range, reader)?; - - let color_count = read_varuint(reader)?; - - Ok(Self { - magic, - version, - scale, - color_encoding, - coordinate_range, - width, - height, - color_count, - }) - } - - /// Defines the number of total bits in a Unit value and thus the overall - /// precision of the file. - pub fn coordinate_range(&self) -> CoordinateRange { - self.coordinate_range - } - - /// Defines the type of color information that is used in the [`ColorTable`]. - pub fn color_encoding(&self) -> ColorEncoding { - self.color_encoding - } - - /// Defines the number of fraction bits in a Unit value. - pub fn scale(&self) -> Scale { - self.scale - } - - /// The number of colors in the [`ColorTable`]. - pub fn color_count(&self) -> u32 { - self.color_count - } -} - -fn read_unsigned_unit( - coordinate_range: CoordinateRange, - bytes: &mut R, -) -> io::Result { - Ok(match coordinate_range { - CoordinateRange::Reduced => bytes.read_u8()? as u32, - CoordinateRange::Default => bytes.read_u16::()? as u32, - CoordinateRange::Enhanced => bytes.read_u32::()?, - }) -} diff --git a/alligator_tvg/src/lib.rs b/alligator_tvg/src/lib.rs deleted file mode 100644 index 5cbe33c..0000000 --- a/alligator_tvg/src/lib.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::io::{self, Read}; - -use byteorder::{LittleEndian, ReadBytesExt}; -use colors::{Color, ColorTable}; -use commands::Command; -use header::{CoordinateRange, Scale, TvgHeader}; -use thiserror::Error; - -mod colors; -mod commands; -mod header; -mod path; - -pub use colors::Rgba16; -pub use header::SUPPORTED_VERSION; - -pub struct TvgFile { - header: TvgHeader, - color_table: ColorTable, - commands: Box<[Command]>, -} - -impl TvgFile { - pub fn read_from(reader: &mut impl Read) -> Result { - let header = TvgHeader::read(reader)?; - let color_table = - ColorTable::read_from_encoding(reader, header.color_count(), header.color_encoding())?; - - let mut commands = Vec::new(); - loop { - let command = Command::read(reader, &header)?; - commands.push(command.clone()); - - if command.is_end_of_document() { - break; - } - } - - Ok(Self { - header, - color_table, - commands: commands.into_boxed_slice(), - }) - } -} - -#[derive(Debug, Error)] -pub enum TvgError { - #[error("Not a valid TVG file. The magic number was invalid.")] - InvalidFile, - #[error("Expected version 1, but found version {}", .0)] - UnsupportedVersion(u8), - #[error("Found a coordinate range with an index of 3, which is invalid")] - InvalidCoordinateRange, - #[error("Found a command with an index of {}, which is invalid", .0)] - InvalidCommand(u8), - #[error("Found a style kind with an index of 3, which is invalid")] - InvalidStyleKind, - #[error("Polygons must have at least 3 points, found {}", .0)] - InvalidPolygon(u32), - #[error("The end of the document must have a `prim_style_kind` value of 0. Found {}", .0)] - InvalidEndOfDocument(u8), - #[error("{}", .0)] - IoError(#[from] io::Error), -} - -trait Decode: Sized { - fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result; - - fn read_multiple( - reader: &mut impl Read, - header: &TvgHeader, - count: u32, - ) -> io::Result> { - let mut vec = Vec::with_capacity(count as usize); - for _ in 0..count { - vec.push(Self::read(reader, header)?); - } - - Ok(vec.into_boxed_slice()) - } -} - -/// The unit is the common type for both positions and sizes in the vector -/// graphic. It is encoded as a signed integer with a configurable amount of -/// bits (see [`CoordinateRange`]) and fractional bits. -fn read_unit(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - let value = match header.coordinate_range() { - CoordinateRange::Reduced => reader.read_i8()? as i32, - CoordinateRange::Default => reader.read_i16::()? as i32, - CoordinateRange::Enhanced => reader.read_i32::()?, - }; - - let fractional = match header.scale() { - Scale::_1 => 1.0, - Scale::_2 => 2.0, - Scale::_4 => 4.0, - Scale::_8 => 8.0, - Scale::_16 => 16.0, - Scale::_32 => 32.0, - Scale::_64 => 64.0, - Scale::_128 => 128.0, - Scale::_256 => 256.0, - Scale::_512 => 512.0, - Scale::_1024 => 1024.0, - Scale::_2048 => 2048.0, - Scale::_4096 => 4096.0, - Scale::_8192 => 8192.0, - Scale::_16384 => 16_384.0, - Scale::_32768 => 32_768.0, - }; - - Ok((value as f64) / fractional) -} - -/// A X and Y coordinate pair. -#[derive(Debug, Clone, Copy)] -pub struct Point { - /// Horizontal distance of the point to the origin. - x: f64, - /// Vertical distance of the point to the origin. - y: f64, -} - -impl Decode for Point { - fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self { - x: read_unit(reader, header)?, - y: read_unit(reader, header)?, - }) - } -} - -fn read_varuint(reader: &mut impl Read) -> io::Result { - let mut count = 0; - let mut result = 0; - - loop { - let byte = reader.read_u8()? as u32; - let value = (byte & 0x7F) << (7 * count); - result |= value; - - if (byte & 0x80) == 0 { - break; - } - - count += 1; - } - - Ok(result) -} diff --git a/alligator_tvg/src/path.rs b/alligator_tvg/src/path.rs deleted file mode 100644 index d2bf4fb..0000000 --- a/alligator_tvg/src/path.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::io::{self, Read}; - -use byteorder::ReadBytesExt; -use num_enum::TryFromPrimitive; - -use crate::{header::TvgHeader, read_unit, read_varuint, Decode, Point}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u8)] -enum Sweep { - Right = 0, - Left = 1, -} - -/// An instruction to move a hypothetical "pen". -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u8)] -enum InstructionKind { - /// A straight line is drawn from the current point to a new point. - Line = 0, - /// A straight horizontal line is drawn from the current point to a new x - /// coordinate. - HorizontalLine = 1, - /// A straight vertical line is drawn from the current point to a new y - /// coordiante. - VerticalLine = 2, - /// A cubic Bézier curve is drawn from the current point to a new point. - CubicBezier = 3, - /// A circle segment is drawn from current point to a new point. - ArcCircle = 4, - /// An ellipse segment is drawn from current point to a new point. - ArcEllipse = 5, - /// The path is closed, and a straight line is drawn to the starting point. - ClosePath = 6, - /// A quadratic Bézier curve is drawn from the current point to a new point. - QuadraticBezier = 7, -} - -#[derive(Debug, Clone, Copy)] -enum InstructionData { - /// The line instruction draws a straight line to the position. - Line { - /// The end point of the line. - position: Point, - }, - /// The horizontal line instruction draws a straight horizontal line to a - /// given x coordinate. - HorizontalLine { - /// The new x coordinate. - x: f64, - }, - /// The vertical line instruction draws a straight vertical line to a given - /// y coordinate. - VerticalLine { - /// The new y coordinate. - y: f64, - }, - /// The cubic bezier instruction draws a Bézier curve with two control - /// points. - /// - /// The curve is drawn between the current location and `point_1` with - /// `control_0` being the first control point and `control_1` being the - /// second one. - CubicBezier { - /// The first control point. - control_0: Point, - /// The second control point. - control_1: Point, - /// The end point of the Bézier curve. - point_1: Point, - }, - /// Draws a circle segment between the current and the target point. - /// - /// The `radius` field determines the radius of the circle. If the distance - /// between the current point and `target` is larger than `radius`, the - /// distance is used as the radius. - ArcCircle { - /// If `true`, the large portion of the circle segment is drawn. - large_arc: bool, - /// Determines if the circle segment is left- or right bending. - sweep: Sweep, - /// The radius of the circle. - radius: f64, - /// The end point of the circle segment. - target: Point, - }, - /// Draws an ellipse segment between the current and the target point. - /// - /// The `radius_x` and `radius_y` fields determine the both radii of the - /// ellipse. If the distance between the current point and target is not - /// enough to fit any ellipse segment between the two points, `radius_x` - /// and `radius_y` are scaled uniformly so that it fits exactly. - ArcEllipse { - /// If `true`, the large portion of the ellipse segment is drawn. - large_arc: bool, - /// Determines if the ellipse segment is left- or right bending. - sweep: Sweep, - /// The radius of the ellipse segment in the horizontal direction. - radius_x: f64, - /// The radius of the ellipse segment in the vertical direction. - radius_y: f64, - /// The rotation of the ellipse in mathematical negative direction, in - /// degrees. - rotation: f64, - /// The end point of the ellipse segment. - target: Point, - }, - /// A straight line is drawn to the start location of the current segment. - /// This instruction doesn’t have additional data encoded. - ClosePath, - /// The quadratic bezier instruction draws a Bézier curve with a single - /// control point. - /// - /// The curve is drawn between the current location and `point_1` with - /// control being the control point. - QuadraticBezier { - /// The control point. - control: Point, - /// The end point of the Bézier curve. - target: Point, - }, -} - -impl InstructionData { - fn read(reader: &mut impl Read, header: &TvgHeader, kind: InstructionKind) -> io::Result { - match kind { - InstructionKind::Line => Self::read_line(reader, header), - InstructionKind::HorizontalLine => Self::read_horizontal_line(reader, header), - InstructionKind::VerticalLine => Self::read_vertical_line(reader, header), - InstructionKind::CubicBezier => Self::read_cubic_bezier(reader, header), - InstructionKind::ArcCircle => Self::read_arc_circle(reader, header), - InstructionKind::ArcEllipse => Self::read_arc_ellipse(reader, header), - InstructionKind::ClosePath => Ok(Self::ClosePath), - InstructionKind::QuadraticBezier => Self::read_quadratic_bezier(reader, header), - } - } - - fn read_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self::Line { - position: Point::read(reader, header)?, - }) - } - - fn read_horizontal_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self::HorizontalLine { - x: read_unit(reader, header)?, - }) - } - - fn read_vertical_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self::VerticalLine { - y: read_unit(reader, header)?, - }) - } - - fn read_cubic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self::CubicBezier { - control_0: Point::read(reader, header)?, - control_1: Point::read(reader, header)?, - point_1: Point::read(reader, header)?, - }) - } - - fn read_arc_header(reader: &mut impl Read, header: &TvgHeader) -> io::Result<(bool, Sweep)> { - // large_arc and sweep are stored in the same byte - let byte = reader.read_u8()?; - let large_arc = (byte & 1) != 0; - let sweep = match byte & 2 { - 0 => Sweep::Left, - _ => Sweep::Right, - }; - - Ok((large_arc, sweep)) - } - - fn read_arc_circle(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - let (large_arc, sweep) = Self::read_arc_header(reader, header)?; - let radius = read_unit(reader, header)?; - let target = Point::read(reader, header)?; - - Ok(Self::ArcCircle { - large_arc, - sweep, - radius, - target, - }) - } - - fn read_arc_ellipse(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - let (large_arc, sweep) = Self::read_arc_header(reader, header)?; - let radius_x = read_unit(reader, header)?; - let radius_y = read_unit(reader, header)?; - let rotation = read_unit(reader, header)?; - let target = Point::read(reader, header)?; - - Ok(Self::ArcEllipse { - large_arc, - sweep, - radius_x, - radius_y, - rotation, - target, - }) - } - - fn read_quadratic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - Ok(Self::QuadraticBezier { - control: Point::read(reader, header)?, - target: Point::read(reader, header)?, - }) - } -} - -#[derive(Debug, Clone)] -struct Instruction { - /// The width of the line the "pen" makes, if it makes one at all. - line_width: Option, - /// The arguments to the instruction. - data: InstructionData, -} - -impl Instruction { - fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { - let byte = reader.read_u8()?; - let instruction_kind = - InstructionKind::try_from_primitive(byte & 0b0000_0111).expect("invalid instruction"); - let has_line_width = (byte & 0b0001_0000) != 0; - - let line_width = has_line_width - .then(|| read_unit(reader, header)) - .transpose()?; - let data = InstructionData::read(reader, header, instruction_kind)?; - - Ok(Self { line_width, data }) - } -} - -#[derive(Debug, Clone)] -struct Segment { - /// The starting point of the segment. - start: Point, - /// The list of instructions for tha segment. - instructions: Box<[Instruction]>, -} - -impl Segment { - fn read(reader: &mut impl Read, header: &TvgHeader, segment_length: u32) -> io::Result { - let start = Point::read(reader, header)?; - - let mut instructions = Vec::with_capacity(segment_length as usize); - for _ in 0..segment_length { - instructions.push(Instruction::read(reader, header)?) - } - - Ok(Segment { - start, - instructions: instructions.into_boxed_slice(), - }) - } -} - -/// Paths describe instructions to create complex 2D graphics. -/// -/// Each path segment generates a shape by moving a ”pen” around. The path this -/// ”pen” takes is the outline of our segment. Each segment, the ”pen” starts -/// at a defined position and is moved by instructions. Each instruction will -/// leave the ”pen” at a new position. The line drawn by our ”pen” is the -/// outline of the shape. -#[derive(Debug, Clone)] -pub struct Path { - segments: Box<[Segment]>, -} - -impl Path { - pub(crate) fn read( - reader: &mut impl Read, - header: &TvgHeader, - segment_count: u32, - ) -> io::Result { - let mut segment_lengths = Vec::with_capacity(segment_count as usize); - for _ in 0..segment_count { - segment_lengths.push(read_varuint(reader)? + 1); - } - - let mut segments = Vec::with_capacity(segment_count as usize); - for segment_length in segment_lengths { - segments.push(Segment::read(reader, header, segment_length)?); - } - - Ok(Self { - segments: segments.into_boxed_slice(), - }) - } -} diff --git a/alligator_tvg/tests/examples/tvg/everything-32.tvg b/alligator_tvg/tests/examples/tvg/everything-32.tvg deleted file mode 100644 index 7ea4bdd..0000000 Binary files a/alligator_tvg/tests/examples/tvg/everything-32.tvg and /dev/null differ diff --git a/alligator_tvg/tests/examples/tvg/everything.tvg b/alligator_tvg/tests/examples/tvg/everything.tvg deleted file mode 100644 index b211c53..0000000 Binary files a/alligator_tvg/tests/examples/tvg/everything.tvg and /dev/null differ diff --git a/alligator_tvg/tests/examples/tvg/shield-16.tvg b/alligator_tvg/tests/examples/tvg/shield-16.tvg deleted file mode 100644 index aacd3ea..0000000 Binary files a/alligator_tvg/tests/examples/tvg/shield-16.tvg and /dev/null differ diff --git a/alligator_tvg/tests/examples/tvg/shield-32.tvg b/alligator_tvg/tests/examples/tvg/shield-32.tvg deleted file mode 100644 index a2abc92..0000000 Binary files a/alligator_tvg/tests/examples/tvg/shield-32.tvg and /dev/null differ diff --git a/alligator_tvg/tests/examples/tvg/shield-8.tvg b/alligator_tvg/tests/examples/tvg/shield-8.tvg deleted file mode 100644 index 57033be..0000000 Binary files a/alligator_tvg/tests/examples/tvg/shield-8.tvg and /dev/null differ diff --git a/alligator_tvg/tests/parse.rs b/alligator_tvg/tests/parse.rs deleted file mode 100644 index eb6e801..0000000 --- a/alligator_tvg/tests/parse.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::fs::File; - -use alligator_tvg::{Rgba16, TvgFile}; - -#[test] -fn main() { - for entry in std::fs::read_dir("tests/examples/tvg").unwrap() { - let entry = entry.unwrap().file_name(); - let entry = entry.to_string_lossy(); - let mut file = File::open(format!("./tests/examples/tvg/{}", &entry)).unwrap(); - let _: TvgFile = TvgFile::read_from(&mut file).unwrap(); - println!("{:?} succeeded!", entry); - } -} diff --git a/render/Cargo.toml b/render/Cargo.toml new file mode 100644 index 0000000..87536c1 --- /dev/null +++ b/render/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "alligator_render" +version = "0.1.0" +edition = "2021" +rust-version = "1.65" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +alligator_resources = { path = "../resources" } + +bytemuck = { version = "1", features = ["derive"] } +thiserror = "1" +profiling = "1" +wgpu = "0.15" +winit = "0.28" +image = "0.24" +cgmath = "0.18" +pollster = "0.2" +log = "0.4" +parking_lot = "0.12" +texture_packer = { git="https://github.com/botahamec/piston_texture_packer", branch="u16" } + +tracy-client = { version = "0.15", optional = true } +dhat = { version = "0.3", optional = true } + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +dhat = ["dep:dhat"] +profile-with-tracy = ["tracy-client", "profiling/profile-with-tracy"] + +[[example]] +name = "black" + +[[example]] +name = "bmp" + +[[example]] +name = "bunnymark" diff --git a/render/examples/black.rs b/render/examples/black.rs new file mode 100644 index 0000000..655cbde --- /dev/null +++ b/render/examples/black.rs @@ -0,0 +1,31 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::sync::Arc; + +use alligator_render::{RenderWindowConfig, Renderer}; +use alligator_resources::texture::{TextureManager, TextureManagerConfig}; + +fn update(_renderer: &mut Renderer) {} + +fn main() { + let start = std::time::Instant::now(); + + // configure the render window + let render_config = RenderWindowConfig { + //vsync: false, + //mode: alligator_render::config::WindowMode::BorderlessFullscreen, + title: "Black Screen.exe", + ..Default::default() + }; + + let texture_config = TextureManagerConfig { + initial_capacity: 0, + max_size: 0, + }; + + let texture_manager = Arc::new(TextureManager::new(&texture_config)); + let renderer = Renderer::new(&render_config, texture_manager).unwrap(); + println!("Startup time: {:?}", start.elapsed()); + + renderer.run(update); +} diff --git a/render/examples/bmp.rs b/render/examples/bmp.rs new file mode 100644 index 0000000..9d864d0 --- /dev/null +++ b/render/examples/bmp.rs @@ -0,0 +1,94 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::{num::NonZeroU32, sync::Arc}; + +use alligator_render::{Instance, RenderWindowConfig, Renderer}; +use alligator_resources::texture::{ImageFormat, TextureManager, TextureManagerConfig}; + +#[profiling::function] +fn update(renderer: &mut Renderer) { + let camera = renderer.camera_mut(); + camera.set_rotation(camera.rotation() + 0.01); +} + +fn main() { + // configure the render window + let render_config = RenderWindowConfig { + title: "Bumper Stickers", + instance_capacity: 2, + default_width: NonZeroU32::new(1280).unwrap(), + default_height: NonZeroU32::new(720).unwrap(), + //mode: alligator_render::config::WindowMode::BorderlessFullscreen, + //vsync: false, + ..Default::default() + }; + + let texture_config = TextureManagerConfig { + initial_capacity: 3, + max_size: 3_000_000, + }; + + let texture_manager = Arc::new(TextureManager::new(&texture_config)); + let mut renderer = Renderer::new(&render_config, texture_manager.clone()).unwrap(); + + // render the alligator + let gator = include_bytes!("res/gator.ff"); + let gator_id = texture_manager + .load_from_memory(gator, ImageFormat::Farbfeld) + .unwrap(); + renderer.textures_mut().load_texture(gator_id).unwrap(); + let gator_width = renderer.textures_mut().texture_width(gator_id).unwrap(); + let gator_height = renderer.textures_mut().texture_height(gator_id).unwrap(); + let gator_x = renderer.textures_mut().texture_x(gator_id).unwrap(); + let gator_y = renderer.textures_mut().texture_y(gator_id).unwrap(); + + renderer.instances_mut().push_instance(Instance { + position: [-0.5, 0.5], + size: [1.5; 2], + z_index: 1.0, + texture_size: [gator_width, gator_height], + texture_coordinates: [gator_x, gator_y], + ..Default::default() + }); + + // render the ghost + let icon = include_bytes!("res/ghost.ico"); + let icon_id = texture_manager + .load_from_memory(icon, ImageFormat::Ico) + .unwrap(); + renderer.textures_mut().load_texture(icon_id).unwrap(); + let icon_width = renderer.textures_mut().texture_width(icon_id).unwrap(); + let icon_height = renderer.textures_mut().texture_height(icon_id).unwrap(); + let icon_x = renderer.textures_mut().texture_x(icon_id).unwrap(); + let icon_y = renderer.textures_mut().texture_y(icon_id).unwrap(); + + renderer.instances_mut().push_instance(Instance { + position: [0.5, 0.5], + size: [0.75; 2], + rotation: 0.5, + z_index: 1.0, + texture_size: [icon_width, icon_height], + texture_coordinates: [icon_x, icon_y], + ..Default::default() + }); + + // render the bitmap alligator + let gator = include_bytes!("res/gator.bmp"); + let gator_id = texture_manager + .load_from_memory(gator, ImageFormat::Bmp) + .unwrap(); + let gator_width = renderer.textures_mut().texture_width(gator_id).unwrap(); + let gator_height = renderer.textures_mut().texture_height(gator_id).unwrap(); + let gator_x = renderer.textures_mut().texture_x(gator_id).unwrap(); + let gator_y = renderer.textures_mut().texture_y(gator_id).unwrap(); + + renderer.instances_mut().push_instance(Instance { + position: [0.0, -0.5], + size: [1.5; 2], + texture_size: [gator_width, gator_height], + texture_coordinates: [gator_x, gator_y], + ..Default::default() + }); + + renderer.run(update); +} diff --git a/render/examples/bunnymark.rs b/render/examples/bunnymark.rs new file mode 100644 index 0000000..1579cf0 --- /dev/null +++ b/render/examples/bunnymark.rs @@ -0,0 +1,158 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::{num::NonZeroU32, sync::Arc, time::Instant}; + +use alligator_render::{Instance, InstanceId, RenderWindowConfig, Renderer}; +use alligator_resources::texture::{ImageFormat, TextureId, TextureManager, TextureManagerConfig}; + +fn xorshift_plus(seed: &mut [u64; 2]) -> u64 { + let mut t = seed[0]; + let s = seed[1]; + + t ^= t << 23; + t ^= t >> 18; + t ^= s ^ (s >> 5); + + seed[0] = s; + seed[1] = t; + + t.wrapping_add(s) +} + +#[derive(Debug)] +struct State { + texture_id: TextureId, + bunnies: Vec, + previous_timestamp: Option, + seed: [u64; 2], + stopped: bool, +} + +impl State { + fn new(texture_id: TextureId) -> Self { + Self { + texture_id, + bunnies: Vec::with_capacity(10_000_000), + previous_timestamp: None, + seed: [0x0D15EA5E8BADF00D, 0xDECAFBADDEADBEAF], + stopped: false, + } + } + + #[profiling::function] + fn update(&mut self, renderer: &mut Renderer) { + let Some(instant) = self.previous_timestamp else { + self.previous_timestamp = Some(Instant::now()); + return; + }; + + let frame_time = instant.elapsed(); + let fps = 1.0 / frame_time.as_secs_f32(); + + renderer.set_title(&format!( + "BunnyMark - {} bunnies - {} FPS", + self.bunnies.len(), + fps.round() + )); + + if fps < 15.0 { + self.stopped = true; + } + + self.previous_timestamp = Some(Instant::now()); + + if self.stopped { + return; + } + + for bunny in self.bunnies.iter_mut() { + let instance = renderer + .instances_mut() + .get_instance_mut(bunny.instance_id) + .unwrap(); + + instance.position[0] += bunny.velocity_x; + instance.position[1] += bunny.velocity_y; + + if !(-1.5..1.5).contains(&instance.position[0]) { + instance.position[0] = instance.position[0].clamp(-1.0, 1.0); + bunny.velocity_x = -bunny.velocity_x; + } + + if !(-0.75..0.75).contains(&instance.position[1]) { + instance.position[1] = instance.position[1].clamp(-0.5, 0.5); + bunny.velocity_y *= -0.90; + } + + bunny.velocity_y -= 0.005; + } + + for _ in 0..=(fps as u64 * 50) { + let texture_x = renderer.textures_mut().texture_x(self.texture_id).unwrap(); + let texture_y = renderer.textures_mut().texture_y(self.texture_id).unwrap(); + let texture_height = renderer + .textures_mut() + .texture_height(self.texture_id) + .unwrap(); + let texture_width = renderer + .textures_mut() + .texture_width(self.texture_id) + .unwrap(); + let instance_id = renderer.instances_mut().push_instance(Instance { + texture_coordinates: [texture_x, texture_y], + texture_size: [texture_width, texture_height], + size: [0.08, 0.08], + position: [-1.5, 0.70], + ..Default::default() + }); + + let velocity_x = (xorshift_plus(&mut self.seed) % 1_000_000) as f32 / 25_000_000.0; + let velocity_y = (xorshift_plus(&mut self.seed) % 1_000_000) as f32 / 25_000_000.0; + self.bunnies.push(Bunny { + instance_id, + velocity_x, + velocity_y, + }); + } + } +} + +#[derive(Debug, Clone, Copy)] +struct Bunny { + instance_id: InstanceId, + velocity_x: f32, + velocity_y: f32, +} + +fn main() { + #[cfg(feature = "profile-with-tracy")] + profiling::tracy_client::Client::start(); + profiling::register_thread!("main"); + + // configure the render window + let render_config = RenderWindowConfig { + title: "BunnyMark", + instance_capacity: 150_000, + default_width: NonZeroU32::new(1280).unwrap(), + default_height: NonZeroU32::new(720).unwrap(), + vsync: false, + low_power: false, + ..Default::default() + }; + + let texture_config = TextureManagerConfig { + initial_capacity: 1, + max_size: 10_000, + }; + + let bunny = include_bytes!("res/bunny.ff"); + let texture_manager = Arc::new(TextureManager::new(&texture_config)); + let mut renderer = Renderer::new(&render_config, texture_manager.clone()).unwrap(); + let texture_id = texture_manager + .load_from_memory(bunny, ImageFormat::Farbfeld) + .unwrap(); + renderer.textures_mut().load_texture(texture_id).unwrap(); + + let state = Box::leak(Box::new(State::new(texture_id))); + renderer.run(|r| state.update(r)); +} diff --git a/render/examples/res/bunny.ff b/render/examples/res/bunny.ff new file mode 100644 index 0000000..64c5a69 Binary files /dev/null and b/render/examples/res/bunny.ff differ diff --git a/render/examples/res/gator.bmp b/render/examples/res/gator.bmp new file mode 100644 index 0000000..e752b56 Binary files /dev/null and b/render/examples/res/gator.bmp differ diff --git a/render/examples/res/gator.ff b/render/examples/res/gator.ff new file mode 100644 index 0000000..aac1bcb Binary files /dev/null and b/render/examples/res/gator.ff differ diff --git a/render/examples/res/ghost.ico b/render/examples/res/ghost.ico new file mode 100644 index 0000000..102de00 Binary files /dev/null and b/render/examples/res/ghost.ico differ diff --git a/render/shaders/sprite.wgsl b/render/shaders/sprite.wgsl new file mode 100644 index 0000000..276a8ef --- /dev/null +++ b/render/shaders/sprite.wgsl @@ -0,0 +1,62 @@ + +@group(0) @binding(0) +var camera: mat4x4; + +struct VertexInput { + @location(0) position: vec2 +} + +struct InstanceInput { + @location(1) position: vec2, + @location(2) size: vec2, + @location(3) texture_coordinates: vec2, + @location(4) texture_size: vec2, + @location(5) texture_atlas_index: u32, + @location(6) rotation: f32, + @location(7) z_index: f32, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) texture_coordinates: vec2, + @location(1) texture_atlas_index: u32 +} + +@vertex +fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { + var out: VertexOutput; + + // rotate the sprite + let rotation = -instance.rotation; + let a = vec2(cos(rotation), sin(rotation)); + let b = vec2(-a[1], a[0]); + let rotation_mat = mat2x2(a, b); + let rotated = rotation_mat * model.position; + + // scale the sprite + let scaled = rotated * instance.size; + + // move the sprite + let position2d = scaled + instance.position; + + // camera stuff + let position4d = vec4(position2d, instance.z_index, 1.0); + let position = camera * position4d; + + let tex_coords = vec2(model.position[0] + 0.5, 1.0 - (model.position[1] + 0.5)); + + out.clip_position = position; + out.texture_atlas_index = instance.texture_atlas_index; + out.texture_coordinates = tex_coords * instance.texture_size + instance.texture_coordinates; + return out; +} + +@group(1) @binding(0) +var t_diffuse: texture_2d; +@group(1) @binding(1) +var s_diffuse: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(t_diffuse, s_diffuse, in.texture_coordinates); +} \ No newline at end of file diff --git a/render/src/camera.rs b/render/src/camera.rs new file mode 100644 index 0000000..ecece90 --- /dev/null +++ b/render/src/camera.rs @@ -0,0 +1,187 @@ +use std::mem::size_of; + +use cgmath::{Matrix4, Vector2}; + +#[derive(Debug)] +pub struct Camera { + position: (f32, f32), + zoom: f32, + rotation: f32, + inverse_aspect_ratio: f32, + buffer: wgpu::Buffer, + bind_group: wgpu::BindGroup, +} + +type CameraUniform = [[f32; 4]; 4]; + +#[allow(clippy::cast_precision_loss)] +fn inverse_aspect_ratio(width: u32, height: u32) -> f32 { + (height as f32) / (width as f32) +} + +impl Camera { + /// Create a new camera, with a position of (0, 0), and a zoom of 1.0 + pub(crate) fn new( + device: &wgpu::Device, + width: u32, + height: u32, + ) -> (Self, wgpu::BindGroupLayout) { + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Camera Uniform"), + size: size_of::() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Camera Bind Group Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Camera Bind Group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + ( + Self { + position: (0.0, 0.0), + zoom: 1.0, + rotation: 0.0, + inverse_aspect_ratio: inverse_aspect_ratio(width, height), + buffer, + bind_group, + }, + bind_group_layout, + ) + } + + /// Get the camera's current x position + #[must_use] + pub const fn x(&self) -> f32 { + self.position.0 + } + + /// Get the camera's current y position + #[must_use] + pub const fn y(&self) -> f32 { + self.position.1 + } + + /// Get the camera's current zoom + #[must_use] + pub const fn zoom(&self) -> f32 { + self.zoom + } + + /// Get the camera's current rotation, in radians + #[must_use] + pub const fn rotation(&self) -> f32 { + self.rotation + } + + /// Set the position of the camera + pub fn set_position(&mut self, x: f32, y: f32) { + #[cfg(debug_assertions)] + if !(-1000.0..1000.0).contains(&x) || !(-1000.0..1000.0).contains(&y) { + log::warn!( + "The position of the camera is (x: {}, y: {}). \ + Please keep both the x and y positions above -1000 and below 1000 units. \ + Otherwise, everything will look crazy. \ + For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24", + x, + y + ); + } + + self.position = (x, y); + } + + /// Set the zoom of the camera + pub fn set_zoom(&mut self, zoom: f32) { + #[cfg(debug_assertions)] + if !(-1000.0..1000.0).contains(&zoom) { + log::warn!( + "The zoom of the camera is {}. \ + Please keep above -1000, and below 1000, or else smooth zoom may be difficult. \ + For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24", + zoom + ); + } + + self.zoom = zoom; + } + + /// Set the camera rotation, in radians + pub fn set_rotation(&mut self, rotation: f32) { + self.rotation = rotation % std::f32::consts::TAU; + } + + /// Set the aspect ratio of the camera + pub(crate) fn set_size(&mut self, width: u32, height: u32) { + self.inverse_aspect_ratio = inverse_aspect_ratio(width, height); + } + + /// Create a matrix that can be multiplied by any vector to transform it + /// according to the current camera + #[allow(clippy::wrong_self_convention)] + fn to_matrix(&self) -> CameraUniform { + let cos = self.rotation.cos(); + let sin = self.rotation.sin(); + + let x_axis = Vector2::new(cos, -sin); + let y_axis = Vector2::new(sin, cos); + + let eye = Vector2::new(self.position.0, self.position.1); + let x_dot = -cgmath::dot(x_axis, eye); + let y_dot = -cgmath::dot(y_axis, eye); + + #[rustfmt::skip] + let view_matrix = Matrix4::new( + x_axis.x, y_axis.x, 0.0, 0.0, + x_axis.y, y_axis.y, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + x_dot, y_dot, 0.0, 1.0 + ); + + #[rustfmt::skip] + // TODO implement more scaling coordinate systems + let projection_matrix = Matrix4::new( + self.inverse_aspect_ratio * self.zoom, 0.0, 0.0, 0.0, + 0.0, self.zoom, 0.0, 0.0, + 0.0, 0.0, 1.0 / 256.0, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); + + let transform = projection_matrix * view_matrix; + transform.into() + } + + /// Get the bind group for the camera + pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup { + &self.bind_group + } + + /// Refresh the camera buffer for the next frame + #[profiling::function] + pub(crate) fn refresh(&self, queue: &wgpu::Queue) { + queue.write_buffer( + &self.buffer, + 0 as wgpu::BufferAddress, + bytemuck::cast_slice(&self.to_matrix()), + ); + } +} diff --git a/render/src/config.rs b/render/src/config.rs new file mode 100644 index 0000000..c3cc6b6 --- /dev/null +++ b/render/src/config.rs @@ -0,0 +1,198 @@ +use std::num::NonZeroU32; + +use winit::dpi::{LogicalPosition, LogicalSize}; +use winit::window::{Fullscreen, WindowBuilder}; + +/// Describes how a window may be resized +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct Resizable { + /// The minimum width of the window, or None if unconstrained + pub min_width: Option, + /// The minimum height of the window, or None if unconstrained + pub min_height: Option, + /// The maximum width of the window, or None if unconstrained + pub max_width: Option, + /// The maximum height of the window, or None if unconstrained + pub max_height: Option, +} + +/// Information about a window, that is not fullscreened +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct WindowInfo { + pub default_x: i32, + pub default_y: i32, + pub resizable: Option, + pub default_maximized: bool, +} + +impl Default for WindowInfo { + fn default() -> Self { + Self { + default_x: 100, + default_y: 100, + resizable: Some(Resizable::default()), + default_maximized: false, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum WindowMode { + Windowed(WindowInfo), + // TODO support choosing a monitor + BorderlessFullscreen, // TODO exclusive fullscreen +} + +impl Default for WindowMode { + fn default() -> Self { + Self::Windowed(WindowInfo::default()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +// TODO window icon +pub struct RenderWindowConfig<'a> { + /// The width of the window, once initialized + pub default_width: NonZeroU32, + /// The height of the window, once initialized + pub default_height: NonZeroU32, + /// The window may be fullscreen + pub mode: WindowMode, + /// The title for the window + pub title: &'a str, + /// If true, a low-power device will be selected as the GPU, if possible + pub low_power: bool, + /// If true, Fifo mode is used to present frames. If false, then Mailbox or + /// Immediate will be used if available. Otherwise, Fifo will be used. + pub vsync: bool, + /// The initial capacity of the instance buffer. The size will increase if + /// it's not large enough. Increasing this value may improve performance + /// towards the beginning, if a lot of instances are being created. For + /// compatibility with older devices, it's recommended to keep this number + /// below 150 thousand. + pub instance_capacity: usize, +} + +impl<'a> Default for RenderWindowConfig<'a> { + fn default() -> Self { + Self { + default_width: NonZeroU32::new(640).unwrap(), + default_height: NonZeroU32::new(480).unwrap(), + mode: WindowMode::default(), + title: "Alligator Game", + low_power: true, + vsync: true, + instance_capacity: 500, + } + } +} + +impl<'a> RenderWindowConfig<'a> { + /// Based on the vsync settings, choose a presentation mode + pub(crate) fn present_mode( + vsync: bool, + supported_modes: &[wgpu::PresentMode], + ) -> wgpu::PresentMode { + if vsync { + wgpu::PresentMode::Fifo + } else if supported_modes.contains(&wgpu::PresentMode::Mailbox) { + wgpu::PresentMode::Mailbox + } else if supported_modes.contains(&wgpu::PresentMode::Immediate) { + wgpu::PresentMode::Immediate + } else { + wgpu::PresentMode::Fifo + } + } + + /// Pick an alpha mode + fn alpha_mode(supported_modes: &[wgpu::CompositeAlphaMode]) -> wgpu::CompositeAlphaMode { + if supported_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) { + wgpu::CompositeAlphaMode::PostMultiplied + } else { + wgpu::CompositeAlphaMode::Auto + } + } + + /// Create a `WindowBuilder` from the configuration given. This window is + /// initially invisible and must later be made visible. + pub(crate) fn to_window(&self) -> WindowBuilder { + // start building the window + let mut builder = WindowBuilder::new() + .with_title(self.title) + .with_visible(false) + .with_inner_size(LogicalSize::new( + self.default_width.get(), + self.default_height.get(), + )); + + match self.mode { + WindowMode::Windowed(window_info) => { + builder = builder.with_maximized(window_info.default_maximized); + + if let Some(resizing_options) = window_info.resizable { + if resizing_options.max_height.is_some() || resizing_options.max_width.is_some() + { + builder = builder.with_max_inner_size(LogicalSize::new( + resizing_options.max_width.unwrap_or(NonZeroU32::MAX).get(), + resizing_options.max_height.unwrap_or(NonZeroU32::MAX).get(), + )); + } + + if resizing_options.min_height.is_some() || resizing_options.min_width.is_some() + { + builder = builder.with_min_inner_size(LogicalSize::new( + resizing_options.min_width.unwrap_or(NonZeroU32::MAX).get(), + resizing_options.min_height.unwrap_or(NonZeroU32::MAX).get(), + )); + } + } else { + builder = builder.with_resizable(false); + } + + // TODO clamp the position to the monitor's size + builder = builder.with_position(LogicalPosition::new( + window_info.default_x, + window_info.default_y, + )); + } + WindowMode::BorderlessFullscreen => { + builder = builder.with_fullscreen(Some(Fullscreen::Borderless(None))); + } + } + + builder + } + + /// Gets a surface configuration out of the config. + pub(crate) fn to_surface_configuration( + &self, + supported_present_modes: &[wgpu::PresentMode], + supported_alpha_modes: &[wgpu::CompositeAlphaMode], + ) -> wgpu::SurfaceConfiguration { + let present_mode = Self::present_mode(self.vsync, supported_present_modes); + let alpha_mode = Self::alpha_mode(supported_alpha_modes); + + // configuration for the surface + wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8Unorm, + width: self.default_width.get(), + height: self.default_height.get(), + alpha_mode, + present_mode, + view_formats: vec![ + wgpu::TextureFormat::Bgra8Unorm, + wgpu::TextureFormat::Bgra8UnormSrgb, + ], + } + } + + /// Get the power preference + pub(crate) const fn power_preference(&self) -> wgpu::PowerPreference { + if self.low_power { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + } + } +} diff --git a/render/src/instance.rs b/render/src/instance.rs new file mode 100644 index 0000000..e346cae --- /dev/null +++ b/render/src/instance.rs @@ -0,0 +1,167 @@ +use std::mem::size_of; + +use bytemuck::{Pod, Zeroable}; + +/// The ID for an instance +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct InstanceId(usize); + +/// A sprite, that can be used by the alligator shader +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)] +pub struct Instance { + /// Position on the screen + pub position: [f32; 2], + /// Relative size + pub size: [f32; 2], + /// The location of the texture in the texture atlas + pub texture_coordinates: [f32; 2], + /// The size of the sprite's texture + pub texture_size: [f32; 2], + /// The index of the texture atlas to use + pub texture_atlas_index: u32, + /// Rotation, in radians + pub rotation: f32, + /// z-index + pub z_index: f32, +} + +impl Default for Instance { + fn default() -> Self { + Self { + position: [0.0; 2], + size: [1.0; 2], + rotation: 0.0, + z_index: 0.0, + texture_coordinates: [0.0; 2], + texture_size: [1.0; 2], + texture_atlas_index: 0, + } + } +} + +impl Instance { + // whenever this is updated, please also update `sprite.wgsl` + const ATTRIBUTES: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![ + 1 => Float32x2, 2 => Float32x2, 3 => Float32x2, 4 => Float32x2, + 5 => Uint32, 6 => Float32, 7 => Float32 + ]; + + pub(crate) fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + // make sure these two don't conflict + debug_assert_eq!( + Self::ATTRIBUTES[0].shader_location as usize, + crate::Vertex::ATTRIBUTES.len() + ); + wgpu::VertexBufferLayout { + array_stride: size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &Self::ATTRIBUTES, + } + } +} + +/// A buffer of sprites, for both CPU and GPU memory +pub struct InstanceBuffer { + instances: Vec, + instance_buffer: wgpu::Buffer, + instance_buffer_size: usize, +} + +fn create_buffer(device: &wgpu::Device, instances: &Vec) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Sprite Instance Buffer"), + size: (instances.capacity() * size_of::()) as wgpu::BufferAddress, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) +} + +impl InstanceBuffer { + /// Create a new buffer with the given capacity + pub(crate) fn new(device: &wgpu::Device, capacity: usize) -> Self { + let instances = Vec::with_capacity(capacity); + let instance_buffer_size = instances.capacity(); + let instance_buffer = create_buffer(device, &instances); + + Self { + instances, + instance_buffer, + instance_buffer_size, + } + } + + /// The number of sprites + pub fn len(&self) -> u32 { + self.instances + .len() + .try_into() + .expect("expected less than 3 billion instances") + } + + /// Returns `true` if there are no sprites + pub fn is_empty(&self) -> bool { + self.instances.is_empty() + } + + /// The capacity of the buffer + pub const fn buffer_size(&self) -> usize { + self.instance_buffer_size + } + + /// Get a slice containing the entire buffer + pub(crate) fn buffer_slice(&self) -> wgpu::BufferSlice { + self.instance_buffer.slice(..) + } + + /// Add a new sprite. The new sprite's `InstanceId` is returned. This ID + /// becomes invalid if the buffer is cleared. + pub fn push_instance(&mut self, instance: Instance) -> InstanceId { + let index = self.instances.len(); + self.instances.push(instance); + InstanceId(index) + } + + /// Get a specific instance + pub fn get_instance(&self, id: InstanceId) -> Option<&Instance> { + self.instances.get(id.0) + } + + /// Get a mutable reference to a specific sprite + pub fn get_instance_mut(&mut self, id: InstanceId) -> Option<&mut Instance> { + self.instances.get_mut(id.0) + } + + /// Clear the instance buffer. This invalidates all `InstanceId`'s + pub fn clear(&mut self) { + self.instances.clear(); + } + + /// Increase the capacity of the buffer + fn expand_buffer(&mut self, device: &wgpu::Device) { + self.instance_buffer_size = self.instances.capacity(); + self.instance_buffer = create_buffer(device, &self.instances); + } + + /// Fill the GPU buffer with the sprites in the CPU buffer. + #[profiling::function] + pub(crate) fn fill_buffer(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { + if self.instances.len() > self.instance_buffer_size { + self.expand_buffer(device); + } + + // the instances must be sorted by z-index before being handed to the GPU + let sorted = { + profiling::scope!("depth sorting"); + let mut sorted = self.instances.clone(); + sorted.sort_by(|a, b| a.z_index.total_cmp(&b.z_index)); + sorted + }; + + queue.write_buffer( + &self.instance_buffer, + 0 as wgpu::BufferAddress, + bytemuck::cast_slice(&sorted), + ); + } +} diff --git a/render/src/lib.rs b/render/src/lib.rs new file mode 100644 index 0000000..0d76cc8 --- /dev/null +++ b/render/src/lib.rs @@ -0,0 +1,21 @@ +#![feature(nonzero_min_max)] +#![feature(type_alias_impl_trait)] +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![allow(clippy::module_name_repetitions)] + +mod camera; +pub mod config; +pub mod instance; +pub mod renderer; +mod texture; +mod vertex; + +pub(crate) use camera::Camera; +pub use config::*; +pub use instance::Instance; +pub(crate) use instance::InstanceBuffer; +pub use instance::InstanceId; +pub use renderer::Renderer; +pub(crate) use texture::TextureAtlas; +pub(crate) use vertex::Vertex; diff --git a/render/src/renderer.rs b/render/src/renderer.rs new file mode 100644 index 0000000..f5b486d --- /dev/null +++ b/render/src/renderer.rs @@ -0,0 +1,444 @@ +use std::num::NonZeroU32; +use std::{convert::TryInto, sync::Arc}; + +use crate::{ + vertex::SQUARE, Camera, Instance, InstanceBuffer, RenderWindowConfig, TextureAtlas, Vertex, +}; +use alligator_resources::texture::TextureManager; +use pollster::FutureExt; +use thiserror::Error; +use wgpu::{include_wgsl, util::DeviceExt}; +use winit::{ + dpi::PhysicalSize, + error::OsError, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; + +/// No device could be found which supports the given surface +#[derive(Clone, Copy, Debug, Error)] +#[error("No GPU could be found on this machine")] +pub struct NoGpuError { + /// Prevents this type from being constructed + _priv: (), +} + +impl NoGpuError { + /// Create a new error + const fn new() -> Self { + Self { _priv: () } + } +} + +/// No device could be found which supports the given surface +#[derive(Clone, Copy, Debug, Error)] +#[error("A WebGPU or WebGL context could not be obtained")] +pub struct NoWebContextError { + /// Prevents this type from being constructed + _priv: (), +} + +impl NoWebContextError { + /// Create a new error + const fn new() -> Self { + Self { _priv: () } + } +} + +#[derive(Debug, Error)] +pub enum NewRendererError { + #[error(transparent)] + NoGpu(#[from] NoGpuError), + #[error(transparent)] + NoWebContext(#[from] NoWebContextError), + #[error(transparent)] + // TODO better error + WindowInitError(#[from] OsError), +} + +// TODO make this Debug +pub struct Renderer { + // TODO move some of this data elsewhere + surface: wgpu::Surface, + surface_config: wgpu::SurfaceConfiguration, + supported_present_modes: Box<[wgpu::PresentMode]>, + device: wgpu::Device, + queue: wgpu::Queue, + render_pipeline: wgpu::RenderPipeline, + square_vertex_buffer: wgpu::Buffer, + square_vertices: u32, + instances: InstanceBuffer, + camera: Camera, + textures: TextureAtlas, + event_loop: Option>, + window: Window, +} + +fn get_adapter( + instance: &wgpu::Instance, + surface: &wgpu::Surface, + power_preference: wgpu::PowerPreference, +) -> Result { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference, + compatible_surface: Some(surface), + force_fallback_adapter: false, + }) + .block_on(); // TODO this takes too long + + let adapter = adapter.or_else(|| { + instance + .enumerate_adapters(wgpu::Backends::PRIMARY) + .find(|adapter| !surface.get_capabilities(adapter).formats.is_empty()) + }); + + adapter.ok_or(NoGpuError::new()) +} + +fn sprite_render_pipeline( + device: &wgpu::Device, + texture_format: wgpu::TextureFormat, + render_pipeline_layout: &wgpu::PipelineLayout, +) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl")); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Sprite Render Pipeline"), + layout: Some(render_pipeline_layout), + // information about the vertex shader + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc(), Instance::desc()], + }, + // information about the fragment shader + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + // don't render the back of a sprite + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) +} + +impl Renderer { + /// Initializes the renderer + /// + /// # Errors + /// + /// Returns a [`NoGpu`] error if no device could be detected that can + /// display to the window + /// + /// # Panics + /// + /// This function **must** be called on the main thread, or else it may + /// panic on some platforms. + // TODO make it possible to use without a window (ie, use a bitmap in memory as a surface) + // TODO this function needs to be smaller + pub fn new( + config: &RenderWindowConfig, + textures: Arc, + ) -> Result { + // build the window + let event_loop = EventLoop::new(); + let window = config.to_window().build(&event_loop)?; + let event_loop = Some(event_loop); + + // the instance's main purpose is to create an adapter and a surface + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::VULKAN, + dx12_shader_compiler: wgpu::Dx12Compiler::Fxc, // TODO support DXC + }); + + // the surface is the part of the screen we'll draw to + let surface = + unsafe { instance.create_surface(&window) }.map_err(|_| NoWebContextError::new())?; + + let power_preference = config.power_preference(); + + // the adapter is the handle to the GPU + let adapter = get_adapter(&instance, &surface, power_preference)?; + + // gets a connection to the device, as well as a handle to its command queue + // the options chosen here ensure that this is guaranteed to not panic + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits { + max_buffer_size: adapter.limits().max_buffer_size, + max_texture_dimension_2d: adapter.limits().max_texture_dimension_2d, + ..Default::default() + }, + ..Default::default() + }, + None, + ) + .block_on() + .expect("there was no device with the selected features"); + + // configuration for the surface + let capabilities = surface.get_capabilities(&adapter); + let supported_present_modes = capabilities.present_modes.into_boxed_slice(); + let supported_alpha_modes = capabilities.alpha_modes.into_boxed_slice(); + let surface_config = + config.to_surface_configuration(&supported_present_modes, &supported_alpha_modes); + surface.configure(&device, &surface_config); + + // create the camera + let width = window.inner_size().width; + let height = window.inner_size().height; + let (camera, camera_bind_group_layout) = Camera::new(&device, width, height); + + // the vertex buffer used for rendering squares + let square_vertices = SQUARE + .len() + .try_into() + .expect("expected fewer than 3 billion vertices in a square"); + let square_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Square Vertex Buffer"), + contents: bytemuck::cast_slice(&SQUARE), + usage: wgpu::BufferUsages::VERTEX, + }); + + // create the instance buffer + let instances = InstanceBuffer::new(&device, config.instance_capacity); + + // TODO make this configurable + let (textures, texture_layout) = TextureAtlas::new( + &device, + textures, + window.inner_size().width, + window.inner_size().height, + ); + + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Sprite Render Pipeline Layout"), + bind_group_layouts: &[&camera_bind_group_layout, &texture_layout], + push_constant_ranges: &[], + }); + + // set up a pipeline for sprite rendering + let render_pipeline = + sprite_render_pipeline(&device, surface_config.format, &render_pipeline_layout); + + Ok(Self { + surface, + surface_config, + supported_present_modes, + device, + queue, + render_pipeline, + square_vertex_buffer, + square_vertices, + instances, + camera, + textures, + event_loop, + window, + }) + } + + /// Reconfigure the surface + fn reconfigure(&mut self) { + self.surface.configure(&self.device, &self.surface_config); + } + + /// Resize just the renderer. The window will remain unchanged + fn resize_renderer(&mut self, size: PhysicalSize) { + if size.width == 0 || size.height == 0 { + log::error!("The window was somehow set to a size of zero"); + return; + } + + self.surface_config.height = size.height; + self.surface_config.width = size.width; + self.camera.set_size(size.width, size.height); + self.reconfigure(); + } + + /// Set the physical window and renderer size + pub fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) { + let size = PhysicalSize::new(width.get(), height.get()); + self.window.set_inner_size(size); + self.resize_renderer(size); + } + + /// Set vsync on or off. See `[RenderWindowConfig::present_mode]` for more details. + pub fn set_vsync(&mut self, vsync: bool) { + self.surface_config.present_mode = + RenderWindowConfig::present_mode(vsync, &self.supported_present_modes); + self.reconfigure(); + } + + /// Set the window's title + pub fn set_title(&mut self, title: &str) { + self.window.set_title(title); + } + + /// The reference buffer + pub const fn instances(&self) -> &InstanceBuffer { + &self.instances + } + + /// The reference buffer + pub fn instances_mut(&mut self) -> &mut InstanceBuffer { + &mut self.instances + } + + /// Get the camera information + pub const fn camera(&self) -> &Camera { + &self.camera + } + + /// Get a mutable reference to the camera + pub fn camera_mut(&mut self) -> &mut Camera { + &mut self.camera + } + + /// Get a reference to the texture atlas + pub const fn textures(&self) -> &TextureAtlas { + &self.textures + } + + /// Get a mutable reference to the texture atlas + pub fn textures_mut(&mut self) -> &mut TextureAtlas { + &mut self.textures + } + + /// Renders a new frame to the window + /// + /// # Errors + /// + /// A number of problems could occur here. A timeout could occur while + /// trying to acquire the next frame. There may also be no more memory left + /// that can be used for the new frame. + // TODO this needs to be smaller + // TODO don't return wgpu errors + #[profiling::function] + fn render(&mut self) -> Result<(), wgpu::SurfaceError> { + // this will allow us to send commands to the gpu + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + let num_instances = self.instances.len(); + self.instances.fill_buffer(&self.device, &self.queue); + self.camera.refresh(&self.queue); + self.textures.fill_textures(&self.queue); + + // the new texture we can render to + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + { + profiling::scope!("encode render pass"); + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, self.camera.bind_group(), &[]); + render_pass.set_bind_group(1, self.textures.bind_group(), &[]); + render_pass.set_vertex_buffer(0, self.square_vertex_buffer.slice(..)); + render_pass.set_vertex_buffer(1, self.instances.buffer_slice()); + render_pass.draw(0..self.square_vertices, 0..num_instances); + } + // the encoder can't finish building the command buffer until the + // render pass is dropped + + // submit the command buffer to the GPU + profiling::scope!("submit render"); + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } + + /// Take the event loop out of the Renderer, without moving it + /// + /// # Panics + /// + /// This method must only be called once + // TODO This is a quick fix to get the event loop inside the renderer. + // In the future, we should make a separate struct that contains the + // renderer and the event loop, which we move the event loop out of + // while still being able to move the renderer. + fn event_loop(&mut self) -> EventLoop<()> { + self.event_loop.take().unwrap() + } + + /// Run the renderer indefinitely + // TODO this needs to be smaller + pub fn run(mut self, mut f: F) -> ! { + self.window.set_visible(true); + let event_loop = self.event_loop(); + event_loop.run(move |event, _, control_flow| match event { + Event::WindowEvent { window_id, event } => { + if window_id == self.window.id() { + match event { + WindowEvent::Resized(size) => self.resize_renderer(size), + WindowEvent::CloseRequested => { + *control_flow = ControlFlow::ExitWithCode(0); + } + _ => (), + } + } + } + Event::MainEventsCleared => { + f(&mut self); + + // a memory leak occurs if we render a zero-size window, + // along with a `SurfaceError::Outdated`. I don't know why that + // happens, but let's make wgpu happy. + // https://github.com/gfx-rs/wgpu/issues/1783#issuecomment-1328463201 + if self.window.inner_size().width != 0 && self.window.inner_size().height != 0 { + match self.render() { + Ok(_) => {} + // reconfigure the surface if it's been lost + Err(wgpu::SurfaceError::Lost) => { + self.reconfigure(); + } + // if we ran out of memory, then we'll die + Err(wgpu::SurfaceError::OutOfMemory) => { + *control_flow = ControlFlow::ExitWithCode(1); + } + // otherwise, we'll just log the error + Err(e) => log::error!("{}", e), + } + } else { + *control_flow = ControlFlow::Wait; + } + profiling::finish_frame!(); + } + _ => {} + }); + } +} diff --git a/render/src/texture.rs b/render/src/texture.rs new file mode 100644 index 0000000..76e77a8 --- /dev/null +++ b/render/src/texture.rs @@ -0,0 +1,250 @@ +use std::error::Error; +use std::num::NonZeroU32; +use std::sync::Arc; + +use alligator_resources::texture::{LoadError, Rgba16Texture, TextureId, TextureManager}; +use image::{EncodableLayout, GenericImage, RgbaImage}; +use texture_packer::TexturePacker; +use texture_packer::{ + exporter::{ExportResult, ImageExporter}, + TexturePackerConfig, +}; +use thiserror::Error; + +/// The texture did not fit in the texture atlas +#[derive(Debug, Error)] +#[error("{:?}", .0)] +pub struct PackError(PackErrorInternal); + +// TODO this can be removed when a new texture packer is made +type PackErrorInternal = impl std::fmt::Debug; + +#[derive(Error, Debug)] +pub enum TextureError { + #[error("{:?}", .0)] + TextureTooLarge(#[from] PackError), + #[error("{}", .0)] + BadImage(#[from] LoadError), + #[error("Unexpected Error (this is a bug in alligator_render): {}", .0)] + Unexpected(#[source] Box), +} + +/// Simpler constructor for a wgpu extent3d +const fn extent_3d(width: u32, height: u32) -> wgpu::Extent3d { + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + } +} + +/// A texture atlas, usable by the renderer +// TODO make this Debug +// TODO make these resizable +pub struct TextureAtlas { + textures: Arc, + packer: TexturePacker<'static, Rgba16Texture, TextureId>, + diffuse_texture: wgpu::Texture, + diffuse_bind_group: wgpu::BindGroup, + image: RgbaImage, + width: u32, + height: u32, + changed: bool, +} + +macro_rules! texture_info { + ($name: ident, $prop: ident, $divisor: ident) => { + pub fn $name(&mut self, id: TextureId) -> Result { + let frame = match self.texture_frame(id) { + Some(frame) => frame, + None => { + self.load_texture(id)?; + self.texture_frame(id).unwrap() + } + }; + let property = frame.frame.$prop; + let value = property as f32 / self.$divisor as f32; + Ok(value) + } + }; +} + +impl TextureAtlas { + /// Creates a new texture atlas, with the given size + // TODO why is this u32? + // TODO this is still too large + pub fn new( + device: &wgpu::Device, + textures: Arc, + width: u32, + height: u32, + ) -> (Self, wgpu::BindGroupLayout) { + let atlas_size = extent_3d(width, height); + let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Diffuse Texture"), + size: atlas_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], + }); + + // TODO I don't think this refreshes anything + let diffuse_texture_view = + diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Texture Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Diffuse Bind Group"), + layout: &texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + ], + }); + + ( + Self { + textures, + packer: TexturePacker::new_skyline(TexturePackerConfig { + max_width: width, + max_height: height, + allow_rotation: false, + trim: false, + texture_padding: 0, + ..Default::default() + }), + diffuse_texture, + diffuse_bind_group, + width, + height, + image: RgbaImage::from_raw( + width, + height, + vec![0; 4 * width as usize * height as usize], + ) + .unwrap(), + changed: true, + }, + texture_bind_group_layout, + ) + } + + /// get the bind group for the texture + pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup { + &self.diffuse_bind_group + } + + /// Load a new subtexture from memory + pub fn load_texture(&mut self, id: TextureId) -> Result { + self.changed = true; + let img = self.textures.load_texture(id)?; + self.packer.pack_own(id, img).map_err(PackError)?; + Ok(id) + } + + /// Get the frame for s particular subtexture + fn texture_frame(&self, id: TextureId) -> Option<&texture_packer::Frame> { + self.packer.get_frame(&id) + } + + texture_info!(texture_width, w, width); + texture_info!(texture_height, h, height); + texture_info!(texture_x, x, width); + texture_info!(texture_y, y, height); + + /// Fill the cached image + fn fill_image(&mut self) -> ExportResult<()> { + let atlas = { + profiling::scope!("export atlas"); + ImageExporter::export(&self.packer)? + }; + profiling::scope!("copy image"); + self.image + .copy_from(&atlas, 0, 0) + .expect("image cache is too small"); + Ok(()) + } + + /// Clear the texture atlas, and give it a new size + pub fn clear(&mut self, width: u32, height: u32) { + self.changed = true; + self.width = width; + self.height = height; + self.packer = TexturePacker::new_skyline(TexturePackerConfig { + max_width: self.width, + max_height: self.height, + ..Default::default() + }); + } + + /// Fill the GPU texture atlas + #[profiling::function] + pub(crate) fn fill_textures(&mut self, queue: &wgpu::Queue) { + // saves time if nothing changed since the last time we did this + // FIXME This doesn't do much good once we get procedurally generated animation + // We'll have to create our own texture packer, with mutable subtextures, + // and more efficient algorithms. This'll also make frame times more consistent + if !self.changed { + return; + } + + let atlas_size = extent_3d(self.width, self.height); + + // put the packed texture into the base image + if let Err(e) = self.fill_image() { + log::error!("{}", e); + } + + // copy that to the gpu + queue.write_texture( + wgpu::ImageCopyTexture { + texture: &self.diffuse_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + self.image.as_bytes(), + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: NonZeroU32::new(atlas_size.width * 4), + rows_per_image: NonZeroU32::new(atlas_size.height), + }, + atlas_size, + ); + + self.changed = false; + } +} diff --git a/render/src/vertex.rs b/render/src/vertex.rs new file mode 100644 index 0000000..570eec4 --- /dev/null +++ b/render/src/vertex.rs @@ -0,0 +1,39 @@ +use std::mem::size_of; + +use bytemuck::{Pod, Zeroable}; + +/// The vertices needed to form a square +pub const SQUARE: [Vertex; 6] = [ + Vertex::new(-0.5, -0.5), + Vertex::new(0.5, -0.5), + Vertex::new(-0.5, 0.5), + Vertex::new(0.5, 0.5), + Vertex::new(-0.5, 0.5), + Vertex::new(0.5, -0.5), +]; + +/// A vertex that is usable by the alligator shader +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)] +pub struct Vertex { + position: [f32; 2], +} + +impl Vertex { + // whenever this is updated, please also update `sprite.wgsl` + pub(crate) const ATTRIBUTES: [wgpu::VertexAttribute; 1] = + wgpu::vertex_attr_array![0 => Float32x2]; + + /// Create a new vertex + const fn new(x: f32, y: f32) -> Self { + Self { position: [x, y] } + } + + pub(crate) const fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBUTES, + } + } +} diff --git a/resources/Cargo.toml b/resources/Cargo.toml new file mode 100644 index 0000000..d5d8276 --- /dev/null +++ b/resources/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "alligator_resources" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +image = "0.24" +thiserror = "1" +exun = "0.1" +texture_packer = { git = "https://github.com/botahamec/piston_texture_packer", branch = "u16" } +profiling = "1" +bytemuck = { version = "1", features = ["extern_crate_alloc"] } +parking_lot = "0.12" +log = "0.4" +dashmap = "5" diff --git a/resources/src/lib.rs b/resources/src/lib.rs new file mode 100644 index 0000000..9cbbba0 --- /dev/null +++ b/resources/src/lib.rs @@ -0,0 +1,13 @@ +#![feature(new_uninit, let_chains)] +#![warn(clippy::nursery, clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] + +pub mod texture; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Priority { + Unnecessary, + Possible(u8), + Eventual(u8), + Urgent, +} diff --git a/resources/src/texture.rs b/resources/src/texture.rs new file mode 100644 index 0000000..3a5bf3e --- /dev/null +++ b/resources/src/texture.rs @@ -0,0 +1,394 @@ +use std::cmp::Reverse; +use std::mem; +use std::path::Path; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; + +use dashmap::DashMap; +use image::ImageBuffer; +use parking_lot::Mutex; +use thiserror::Error; + +use crate::Priority; + +/// The next texture ID +static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); + +/// A unique identifier for a texture +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TextureId(usize); + +impl TextureId { + fn new() -> Self { + Self(NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed)) + } +} + +/// These are the formats supported by the renderer. +// TODO make these feature-enabled +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ImageFormat { + Bmp, + Ico, + Farbfeld, +} + +impl From for image::ImageFormat { + fn from(format: ImageFormat) -> Self { + match format { + ImageFormat::Bmp => Self::Bmp, + ImageFormat::Ico => Self::Ico, + ImageFormat::Farbfeld => Self::Farbfeld, + } + } +} + +#[derive(Debug, Error)] +#[error("{}", .0)] +pub struct DecodingError(#[from] image::error::DecodingError); + +#[allow(clippy::missing_const_for_fn)] +fn convert_image_decoding(e: image::ImageError) -> DecodingError { + if let image::ImageError::Decoding(de) = e { + de.into() + } else { + unreachable!("No other error should be possible") + } +} + +#[derive(Debug, Error)] +pub enum LoadError { + #[error("{}", .0)] + Decoding(#[from] DecodingError), + #[error("{}", .0)] + Io(#[from] std::io::Error), +} + +fn convert_image_load_error(e: image::ImageError) -> LoadError { + match e { + image::ImageError::Decoding(de) => LoadError::Decoding(de.into()), + image::ImageError::IoError(ioe) => ioe.into(), + _ => unreachable!("No other error should be possible"), + } +} + +pub type Rgba16Texture = image::ImageBuffer, Box<[u16]>>; + +fn vec_image_to_box(vec_image: image::ImageBuffer, Vec>) -> Rgba16Texture { + let width = vec_image.width(); + let height = vec_image.height(); + let buf = vec_image.into_raw().into_boxed_slice(); + ImageBuffer::from_raw(width, height, buf).expect("image buffer is too small") +} + +/// Get the size, in bytes, of the texture +#[allow(clippy::missing_const_for_fn)] +fn texture_size(image: &Rgba16Texture) -> usize { + image.len() * mem::size_of::>() +} + +/// A texture from disk +struct TextureFile { + path: Box, + texture: Option>, +} + +impl TextureFile { + /// This doesn't load the texture + #[allow(clippy::missing_const_for_fn)] + fn new(path: impl AsRef) -> Self { + Self { + path: path.as_ref().into(), + texture: None, + } + } + + const fn is_loaded(&self) -> bool { + self.texture.is_some() + } + + fn load(&mut self) -> Result<&Rgba16Texture, LoadError> { + if self.texture.is_none() { + log::warn!("{} was not pre-loaded", self.path.to_string_lossy()); + let texture = image::open(&self.path).map_err(convert_image_load_error)?; + let texture = texture.to_rgba16(); + let texture = Arc::new(vec_image_to_box(texture)); + self.texture = Some(texture); + } + + Ok(self.texture.as_ref().expect("the texture wasn't loaded")) + } + + fn loaded_texture(&self) -> Option<&Rgba16Texture> { + self.texture.as_deref() + } + + fn is_used(&self) -> bool { + let Some(arc) = &self.texture else { return false }; + Arc::strong_count(arc) > 1 + } + + /// Unloads the texture from memory if it isn't being used + fn unload(&mut self) { + if !self.is_used() { + self.texture = None; + } + } + + /// The amount of heap memory used, in bytes. This returns 0 if the texture + /// hasn't been loaded yet. + fn allocated_size(&self) -> usize { + self.texture.as_ref().map_or(0, |t| texture_size(t)) + } +} + +enum TextureBuffer { + Memory(Arc), + Disk(TextureFile), +} + +struct Texture { + priority: Priority, + queued_priority: Arc>>, + buffer: TextureBuffer, +} + +impl Texture { + fn from_buffer(texture: Rgba16Texture) -> Self { + Self { + priority: Priority::Urgent, // indicates that it can't be unloaded + queued_priority: Arc::new(Mutex::new(None)), + buffer: TextureBuffer::Memory(Arc::new(texture)), + } + } + + fn from_path(path: impl AsRef, priority: Priority) -> Self { + Self { + priority, + queued_priority: Arc::new(Mutex::new(None)), + buffer: TextureBuffer::Disk(TextureFile::new(path)), + } + } + + const fn priority(&self) -> Priority { + self.priority + } + + fn _set_priority(buffer: &TextureBuffer, src: &mut Priority, priority: Priority) -> bool { + // memory textures and textures in use should always be urgent + if let TextureBuffer::Disk(disk) = buffer && !disk.is_used() { + *src = priority; + true + } else { + false + } + } + + fn unqueue_priority(&mut self) { + let mut queued_priority = self.queued_priority.lock(); + let unqueued_priority = queued_priority.unwrap_or(Priority::Unnecessary); + + if Self::_set_priority(&self.buffer, &mut self.priority, unqueued_priority) { + *queued_priority = None; + } + } + + fn set_priority(&mut self, priority: Priority) { + Self::_set_priority(&self.buffer, &mut self.priority, priority); + } + + fn load_texture(&mut self) -> Result<&Rgba16Texture, LoadError> { + match &mut self.buffer { + TextureBuffer::Memory(ref texture) => Ok(texture), + TextureBuffer::Disk(file) => file.load(), + } + } + + /// If the texture is loaded, return it. + fn loaded_texture(&self) -> Option<&Rgba16Texture> { + match &self.buffer { + TextureBuffer::Memory(ref texture) => Some(texture), + TextureBuffer::Disk(file) => file.loaded_texture(), + } + } + + fn unload(&mut self) { + if let TextureBuffer::Disk(file) = &mut self.buffer { + file.unload(); + } + } + + /// The amount of heap memory used for the texture, if any + fn allocated_size(&self) -> usize { + match &self.buffer { + TextureBuffer::Memory(texture) => texture_size(texture), + TextureBuffer::Disk(file) => file.allocated_size(), + } + } + + const fn is_loaded(&self) -> bool { + match &self.buffer { + TextureBuffer::Memory(_) => true, + TextureBuffer::Disk(file) => file.is_loaded(), + } + } +} + +pub struct TextureManager { + textures: DashMap, + max_size: usize, + needs_atlas_update: AtomicBool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TextureManagerConfig { + /// The initial capacity of the texture manager. This defaults to 500 textures. + pub initial_capacity: usize, + /// The maximum amount of heap usage acceptable. Defaults to 10 MiB. + pub max_size: usize, +} + +impl Default for TextureManagerConfig { + fn default() -> Self { + Self { + initial_capacity: 500, + max_size: 10 * 1024 * 1024, // 10 MiB + } + } +} + +impl TextureManager { + /// Create a new `TextureManager` with the given config options. + #[must_use] + pub fn new(config: &TextureManagerConfig) -> Self { + let textures = DashMap::with_capacity(config.initial_capacity); + + Self { + textures, + max_size: config.max_size, + needs_atlas_update: AtomicBool::new(false), + } + } + + /// Load textures into memory that will be needed soon. Unload unnecessary textures + pub fn cache_files(&self) { + let mut textures: Vec<_> = self + .textures + .iter_mut() + .map(|mut t| { + t.value_mut().unqueue_priority(); + t + }) + .collect(); + textures.sort_by_key(|t2| Reverse(t2.priority())); + + let max_size = self.max_size; + let mut total_size = 0; + + for texture in &mut textures { + drop(texture.load_texture()); + total_size += texture.allocated_size(); + if total_size > max_size && texture.priority() != Priority::Urgent { + texture.unload(); + return; + } + } + } + + /// Loads a texture from memory in the given format. + /// + /// # Errors + /// + /// This returns `Expected(DecodingError)` if the given buffer was invalid + /// for the given format. + pub fn load_from_memory( + &self, + buf: &[u8], + format: ImageFormat, + ) -> Result { + let id = TextureId::new(); + let texture = image::load_from_memory_with_format(buf, format.into()); + let texture = texture.map_err(convert_image_decoding)?; + let texture = texture.into_rgba16(); + let texture = vec_image_to_box(texture); + let texture = Texture::from_buffer(texture); + + self.textures.insert(id, texture); + self.needs_atlas_update.store(true, Ordering::Release); + + Ok(id) + } + + /// Loads a texture from disk. + /// + /// # Errors + /// + /// This returns an error if `priority` is set to [`Priority::Urgent`] but + /// there was an error in loading the file to a texture. + pub fn load_from_file( + &self, + path: impl AsRef, + priority: Priority, + ) -> Result { + let id = TextureId::new(); + let mut texture = Texture::from_path(path, priority); + + if priority == Priority::Urgent { + match texture.load_texture() { + Ok(_) => { + self.textures.insert(id, texture); + self.needs_atlas_update.store(true, Ordering::Release); + } + Err(e) => { + self.textures.insert(id, texture); + return Err(e); + } + } + } else { + self.textures.insert(id, texture); + } + + Ok(id) + } + + /// Loads a texture from disk. + /// + /// # Errors + /// + /// This returns an error if `priority` is set to [`Priority::Urgent`] but + /// there was an error in loading the file to a texture. + pub fn set_priority(&self, id: TextureId, priority: Priority) -> Result<(), LoadError> { + let mut texture = self.textures.get_mut(&id).expect("invalid texture id"); + texture.set_priority(priority); + + if !texture.is_loaded() && priority == Priority::Urgent { + let mut texture = self.textures.get_mut(&id).expect("invalid texture id"); + texture.load_texture()?; + self.needs_atlas_update.store(true, Ordering::Release); + } + + Ok(()) + } + + /// This returns `true` if a texture has been set to have an urgent + /// priority since the last time this function was called. + pub fn needs_atlas_update(&self) -> bool { + self.needs_atlas_update.fetch_and(false, Ordering::AcqRel) + } + + /// Load a texture into memory, if it hasn't been already. Then return a + /// copy of the texture. + /// + /// # Errors + /// + /// This returns an error if an error occurs in loading the texture from + /// disk, such as the file not existing, or not being a valid texture. + pub fn load_texture(&self, id: TextureId) -> Result { + self.textures + .get_mut(&id) + .expect("the TextureId was invalid") + .load_texture() + .cloned() + } +} diff --git a/tvg/Cargo.toml b/tvg/Cargo.toml new file mode 100644 index 0000000..d7e7a6b --- /dev/null +++ b/tvg/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "alligator_tvg" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +byteorder = "1" +thiserror = "1" +raise = "2" +num_enum = "0.5" diff --git a/tvg/src/colors.rs b/tvg/src/colors.rs new file mode 100644 index 0000000..10bc41c --- /dev/null +++ b/tvg/src/colors.rs @@ -0,0 +1,333 @@ +use std::io::{self, Read}; + +use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; +use num_enum::TryFromPrimitive; + +/// The color table encodes the palette for this file. +/// +/// It’s binary content is defined by the `color_encoding` field in the header. +/// For the three defined color encodings, each will yield a list of +/// `color_count` RGBA tuples. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ColorTable { + colors: Box<[C]>, +} + +impl ColorTable { + /// Read in one encoding, and convert it to another + pub fn read_from_encoding( + reader: &mut impl Read, + color_count: u32, + encoding: ColorEncoding, + ) -> io::Result { + Ok(match encoding { + ColorEncoding::Rgba8888 => (&ColorTable::::read(reader, color_count)?).into(), + ColorEncoding::Rgb565 => (&ColorTable::::read(reader, color_count)?).into(), + ColorEncoding::RgbaF32 => (&ColorTable::::read(reader, color_count)?).into(), + ColorEncoding::Custom => (&ColorTable::::read(reader, color_count)?).into(), + }) + } + + /// Parse a color table. + fn read(reader: &mut impl Read, color_count: u32) -> io::Result { + let mut colors = Vec::with_capacity(color_count as usize); + for _ in 0..color_count { + colors.push(C::parse_bytes(reader)?); + } + + let colors = colors.into_boxed_slice(); + Ok(Self { colors }) + } + + /// Returns the number of colors in the table. + fn len(&self) -> usize { + self.colors.len() + } + + /// Returns a reference to a color, or `None` if out-of-bounds. + fn get(&self, index: usize) -> Option<&C> { + self.colors.get(index) + } + + fn iter(&self) -> impl Iterator { + self.colors.iter() + } +} + +impl ColorTable {} + +impl From<&ColorTable> for ColorTable { + fn from(value: &ColorTable) -> Self { + let mut colors = Vec::with_capacity(value.len()); + + for color in value.iter() { + let r = color.red_u16(); + let g = color.green_u16(); + let b = color.blue_u16(); + let a = color.alpha_u16(); + colors.push(New::from_rgba16_lossy(r, g, b, a)); + } + + let colors = colors.into_boxed_slice(); + Self { colors } + } +} + +/// The color encoding defines which format the colors in the color table will +/// have. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] +#[repr(u8)] +pub enum ColorEncoding { + /// Each color is a 4-tuple (red, green, blue, alpha) of bytes with the + /// color channels encoded in sRGB and the alpha as linear alpha. + Rgba8888 = 0, + /// Each color is encoded as a 3-tuple (red, green, blue) with 16 bits per + /// color. + /// + /// While red and blue both use 5 bits, the green channel uses 6 bits. Red + /// uses bit range 0...4, green bits 5...10 and blue bits 11...15. This + /// color also uses the sRGB color space. + Rgb565 = 1, + /// Each color is a 4-tuple (red, green, blue, alpha) of binary32 IEEE 754 + /// floating point value with the color channels encoded in scRGB and the + /// alpha as linear alpha. A color value of 1.0 is full intensity, while a + /// value of 0.0 is zero intensity. + RgbaF32 = 2, + /// The custom color encoding is defined *undefined*. The information how + /// these colors are encoded must be implemented via external means. + Custom = 3, +} + +pub trait Color: Sized { + /// The size of the color's representation in bits + const SIZE: usize; + + /// Attempt to read the color. Returns `Err` if an error occurred while + /// attempting to read [`SIZE`] bytes from `bytes`. + fn parse_bytes(reader: &mut impl Read) -> io::Result; + + /// Convert from the RGBA16 format to this format. This may be lossy. + fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self; + + fn red_u16(&self) -> u16; + fn blue_u16(&self) -> u16; + fn green_u16(&self) -> u16; + fn alpha_u16(&self) -> u16; +} + +/// Each color value is encoded as a sequence of four bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct Rgba8888 { + /// Red color channel between 0 and 100% intensity, mapped to byte values 0 + /// to 255. + red: u8, + /// Green color channel between 0 and 100% intensity, mapped to byte values + /// 0 to 255. + green: u8, + /// Blue color channel between 0 and 100% intensity, mapped to byte values + /// 0 to 255. + blue: u8, + /// Transparency channel between 0 and 100% transparency, mapped to byte + /// values 0 to 255. + alpha: u8, +} + +impl Color for Rgba8888 { + const SIZE: usize = 4; + + fn parse_bytes(reader: &mut impl Read) -> io::Result { + Ok(Self { + red: reader.read_u8()?, + green: reader.read_u8()?, + blue: reader.read_u8()?, + alpha: reader.read_u8()?, + }) + } + + fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self { + Self { + red: (red >> 8) as u8, + green: (green >> 8) as u8, + blue: (blue >> 8) as u8, + alpha: (alpha >> 8) as u8, + } + } + + fn red_u16(&self) -> u16 { + (self.red as u16) << 8 + } + + fn green_u16(&self) -> u16 { + (self.green as u16) << 8 + } + + fn blue_u16(&self) -> u16 { + (self.blue as u16) << 8 + } + + fn alpha_u16(&self) -> u16 { + (self.alpha as u16) << 8 + } +} + +/// Each color value is encoded as a sequence of 2 bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct Rgb565 { + /// Red color channel between 0 and 100% intensity, mapped to integer + /// values 0 to 31. + red: u8, + /// Green color channel between 0 and 100% intensity, mapped to integer + /// values 0 to 63. + green: u8, + /// Blue color channel between 0 and 100% intensity, mapped to integer + /// values 0 to 31. + blue: u8, +} + +impl Color for Rgb565 { + const SIZE: usize = 2; + + fn parse_bytes(reader: &mut impl Read) -> io::Result { + let color = reader.read_u16::()?; + + let red = ((color & 0x001F) << 3) as u8; + let green = ((color & 0x07E0) >> 3) as u8; + let blue = ((color & 0xF800) >> 8) as u8; + + Ok(Self { red, blue, green }) + } + + fn from_rgba16_lossy(red: u16, green: u16, blue: u16, _a: u16) -> Self { + Self { + red: (red >> 11) as u8, + green: (green >> 10) as u8, + blue: (blue >> 11) as u8, + } + } + + fn red_u16(&self) -> u16 { + (self.red as u16) << 11 + } + + fn green_u16(&self) -> u16 { + (self.green as u16) << 11 + } + + fn blue_u16(&self) -> u16 { + (self.blue as u16) << 10 + } + + fn alpha_u16(&self) -> u16 { + 0 + } +} + +/// Each color value is encoded as a sequence of 16 bytes. +#[derive(Debug, Clone, Copy, PartialEq)] +struct RgbaF32 { + /// Red color channel, using 0.0 for 0% intensity and 1.0 for 100% + /// intensity. + red: f32, + /// Green color channel, using 0.0 for 0% intensity and 1.0 for 100% + /// intensity. + green: f32, + /// Blue color channel, using 0.0 for 0% intensity and 1.0 for 100% + /// intensity. + blue: f32, + /// Transparency channel between 0 and 100% transparency, mapped to byte + /// values 0.0 to 1.0. + alpha: f32, +} + +impl Color for RgbaF32 { + const SIZE: usize = 16; + + fn parse_bytes(reader: &mut impl Read) -> io::Result { + Ok(Self { + red: reader.read_f32::()?, + green: reader.read_f32::()?, + blue: reader.read_f32::()?, + alpha: reader.read_f32::()?, + }) + } + + fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self { + Self { + red: (red as f32) / (u16::MAX as f32), + green: (green as f32) / (u16::MAX as f32), + blue: (blue as f32) / (u16::MAX as f32), + alpha: (alpha as f32) / (u16::MAX as f32), + } + } + + fn red_u16(&self) -> u16 { + (self.red * (u16::MAX as f32)) as u16 + } + + fn green_u16(&self) -> u16 { + (self.green * (u16::MAX as f32)) as u16 + } + + fn blue_u16(&self) -> u16 { + (self.blue * (u16::MAX as f32)) as u16 + } + + fn alpha_u16(&self) -> u16 { + (self.alpha * (u16::MAX as f32)) as u16 + } +} + +/// Each color value is encoded as a sequence of 8 bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Rgba16 { + /// Red color channel between 0 and 100% intensity, mapped to a big-endian + /// 16-bit integer. + red: u16, + /// Green color channel between 0 and 100% intensity, mapped to a + /// big-endian 16-bit integer. + green: u16, + /// Blue color channel between 0 and 100% intensity, mapped to a big-endian + /// 16-bit integer. + blue: u16, + /// Transparency channel between 0 and 100% intensity, mapped to a + /// big-endian 16-bit integer. + alpha: u16, +} + +impl Color for Rgba16 { + const SIZE: usize = 8; + + fn parse_bytes(reader: &mut impl Read) -> io::Result { + Ok(Self { + red: reader.read_u16::()?, + green: reader.read_u16::()?, + blue: reader.read_u16::()?, + alpha: reader.read_u16::()?, + }) + } + + fn from_rgba16_lossy(red: u16, green: u16, blue: u16, alpha: u16) -> Self { + Self { + red, + green, + blue, + alpha, + } + } + + fn red_u16(&self) -> u16 { + self.red + } + + fn green_u16(&self) -> u16 { + self.green + } + + fn blue_u16(&self) -> u16 { + self.blue + } + + fn alpha_u16(&self) -> u16 { + self.alpha + } +} diff --git a/tvg/src/commands.rs b/tvg/src/commands.rs new file mode 100644 index 0000000..f316a53 --- /dev/null +++ b/tvg/src/commands.rs @@ -0,0 +1,613 @@ +use std::io::{self, Read}; + +use byteorder::ReadBytesExt; +use raise::yeet; + +use crate::{header::TvgHeader, path::Path, read_unit, read_varuint, Decode, Point, TvgError}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rectangle { + /// Horizontal distance of the left side to the origin. + x: f64, + /// Vertical distance of the upper side to the origin. + y: f64, + /// Horizontal extent of the rectangle. + width: f64, + /// Vertical extent of the rectangle. + height: f64, +} + +impl Decode for Rectangle { + fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self { + x: read_unit(reader, header)?, + y: read_unit(reader, header)?, + width: read_unit(reader, header)?, + height: read_unit(reader, header)?, + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Line { + /// Start point of the line. + start: Point, + /// End point of the line. + end: Point, +} + +impl Decode for Line { + fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self { + start: Point::read(reader, header)?, + end: Point::read(reader, header)?, + }) + } +} + +/// A command that can be encoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +enum CommandName { + /// Determines end of file. + EndOfDocument = 0, + /// Fills an N-gon. + FillPolygon = 1, + /// Fills a set of [`Rectangle`]s. + FillRectangles = 2, + /// Fills a free-form [`Path`]. + FillPath = 3, + /// Draws a set of lines. + DrawLines = 4, + /// Draws the outline of a polygon. + DrawLineLoop = 5, + /// Draws a list of end-to-end lines. + DrawLineStrip = 6, + /// Draws a free-form [`Path`]. + DrawLinePath = 7, + /// Draws a filled polygon with an outline. + OutlineFillPolygon = 8, + /// Draws several filled [`Rectangle`]s with an outline. + OutlineFillRectangles = 9, + /// This command combines the [`FillPath`] and [`DrawLinePath`] command + /// into one. + OutlineFillPath = 10, +} + +impl TryFrom for CommandName { + type Error = TvgError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::EndOfDocument), + 1 => Ok(Self::FillPolygon), + 2 => Ok(Self::FillRectangles), + 3 => Ok(Self::FillPath), + 4 => Ok(Self::DrawLines), + 5 => Ok(Self::DrawLineLoop), + 6 => Ok(Self::DrawLineStrip), + 7 => Ok(Self::DrawLinePath), + 8 => Ok(Self::OutlineFillPolygon), + 9 => Ok(Self::OutlineFillRectangles), + 10 => Ok(Self::OutlineFillPath), + v => Err(TvgError::InvalidCommand(v)), + } + } +} + +/// The type of style the command uses as a primary style. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum StyleKind { + /// The shape is uniformly colored with a single color. + FlatColored = 0, + /// The shape is colored with a linear gradient. + LinearGradient = 1, + /// The shape is colored with a radial gradient. + RadialGradient = 2, +} + +impl TryFrom for StyleKind { + type Error = TvgError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::FlatColored), + 1 => Ok(Self::LinearGradient), + 2 => Ok(Self::RadialGradient), + _ => Err(TvgError::InvalidStyleKind), + } + } +} + +/// The style kind, along with the colors and points used to render the style. +#[derive(Debug, Clone, Copy)] +pub enum Style { + /// The shape is uniformly colored with the color at `color_index` in the + /// [`ColorTable`]. + FlatColored { + /// The index in the [`ColorTable`]. + color_index: u32, + }, + /// The gradient is formed by a mental line between `point_0` and + /// `point_1`. + /// + /// The color at `point_0` is the color at `color_index_0` in the color + /// table. The color at `point_1` is the color at `color_index_1` in the + /// [`ColorTable`]. On the line, the color is interpolated between the two + /// points. Each point that is not on the line is orthogonally projected to + /// the line and the color at that point is sampled. Points that are not + /// projectable onto the line have either the color at `point_0` if they + /// are closed to `point_0` or vice versa for `point_1`. + LinearGradient { + /// The start point of the gradient. + point_0: Point, + /// The end point of the gradient. + point_1: Point, + /// The color at [`point_0`]. + color_index_0: u32, + /// The color at [`point_1`]. + color_index_1: u32, + }, + /// The gradient is formed by a mental circle with the center at `point_0` + /// and `point_1` being somewhere on the circle outline. Thus, the radius + /// of said circle is the distance between `point_0` and `point_1`. + /// + /// The color at `point_0` is the color at `color_index_0` in the color + /// table. The color on the circle outline is the color at `color_index_1` + /// in the [`ColorTable`]. If a sampled point is inside the circle, the + /// color is interpolated based on the distance to the center and the + /// radius. If the point is not in the circle itself, the color at + /// `color_index_1` is always taken. + RadialGradient { + /// The center point of the mental circle. + point_0: Point, + /// The end point of the gradient. + point_1: Point, + /// The color at `point_0`. + color_index_0: u32, + /// The color at `point_1`. + color_index_1: u32, + }, +} + +impl Style { + fn read(reader: &mut impl Read, header: &TvgHeader, kind: StyleKind) -> io::Result { + match kind { + StyleKind::FlatColored => Self::read_flat_colored(reader), + StyleKind::LinearGradient => Self::read_linear_gradient(reader, header), + StyleKind::RadialGradient => Self::read_radial_gradient(reader, header), + } + } + + fn read_flat_colored(reader: &mut impl Read) -> io::Result { + Ok(Self::FlatColored { + color_index: read_varuint(reader)?, + }) + } + + fn read_linear_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::LinearGradient { + point_0: Point::read(reader, header)?, + point_1: Point::read(reader, header)?, + color_index_0: read_varuint(reader)?, + color_index_1: read_varuint(reader)?, + }) + } + + fn read_radial_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::RadialGradient { + point_0: Point::read(reader, header)?, + point_1: Point::read(reader, header)?, + color_index_0: read_varuint(reader)?, + color_index_1: read_varuint(reader)?, + }) + } +} + +/// TinyVG files contain a sequence of draw commands that must be executed in +/// the defined order to get the final result. Each draw command adds a new 2D +/// primitive to the graphic. +#[derive(Debug, Clone)] +pub enum Command { + /// If this command is read, the TinyVG file has ended. + /// + /// This command must have prim_style_kind to be set to 0, so the last byte + /// of every TinyVG file is `0x00`. Every byte after this command is + /// considered not part of the TinyVG data and can be used for other + /// purposes like metadata or similar. + EndOfDocument, + /// Fills a polygon with N [`Point`]s. + /// + /// The number of points must be at least 3. Files that encode a lower + /// value must be discarded as ”invalid” by a conforming implementation. + /// + /// The polygon specified in polygon must be drawn using the even-odd rule. + /// That means that if for any point to be inside the polygon, a line to + /// infinity must cross an odd number of polygon segments. + FillPolygon { + /// The style that is used to fill the polygon. + fill_style: Style, + /// The points of the polygon. + polygon: Box<[Point]>, + }, + /// Fills a list of [`Rectangle`]s. + /// + /// The rectangles must be drawn first to last, which is the order they + /// appear in the file. + FillRectangles { + /// The style that is used to fill all rectangles. + fill_style: Style, + /// The list of rectangles to be filled. + rectangles: Box<[Rectangle]>, + }, + /// Fills a [`Path`]. + /// + /// For the filling, all path segments are considered a polygon each (drawn + /// with even-odd rule) that, when overlap, also perform the even odd rule. + /// This allows the user to carve out parts of the path and create + /// arbitrarily shaped surfaces. + FillPath { + /// The style that is used to fill the path. + fill_style: Style, + /// A [`Path`] with `segment_count` segments. + path: Path, + }, + /// Draws a set of [`Line`]s. + /// + /// Each line is `line_width` units wide, and at least a single display + /// pixel. This means that line_width of 0 is still visible, even though + /// only marginally. This allows very thin outlines. + DrawLines { + /// The style that is used to draw the all lines. + line_style: Style, + /// The width of the lines. + line_width: f64, + /// The list of lines. + lines: Box<[Line]>, + }, + /// Draws a polygon. + /// + /// Each line is `line_width` units wide. The lines are drawn between + /// consecutive points as well as the first and the last point. + DrawLineLoop { + /// The style that is used to draw the all lines. + line_style: Style, + /// The width of the line. + line_width: f64, + /// The points of the polygon. + points: Box<[Point]>, + }, + /// Draws a list of consecutive lines. + /// + /// The lines are drawn between consecutive points, but contrary to + /// [`DrawLineLoop`], the first and the last point are not connected. + DrawLineStrip { + /// The style that is used to draw the all rectangles. + line_style: Style, + /// The width of the line. + line_width: f64, + /// The points of the line strip. + points: Box<[Point]>, + }, + /// Draws a [`Path`]. + /// + /// The outline of the path is `line_width` units wide. + DrawLinePath { + /// The style that is used to draw the path. + line_style: Style, + /// The width of the line. + line_width: f64, + /// A path with `segment_count` segments. + path: Path, + }, + /// Fills a polygon and draws an outline at the same time. + /// + /// This command is a combination of [`FillPolygon`] and [`DrawLineLoop`]. + /// It first performs a [`FillPolygon`] with the `fill_style`, then + /// performs [`DrawLineLoop`] with `line_style` and `line_width`. + /// + /// The outline commands use a reduced number of elements. The maximum + /// number of points is 64. + OutlineFillPolygon { + /// The style that is used to fill the polygon. + fill_style: Style, + /// The style that is used to draw the outline of the polygon. + line_style: Style, + /// The width of the line. + line_width: f64, + /// The set of points of this polygon. + points: Box<[Point]>, + }, + /// Fills and outlines a list of [`Rectangle`]s. + /// + /// For each rectangle, it is first filled, then its outline is drawn, then + /// the next rectangle is drawn. + /// + /// The outline commands use a reduced number of elements, the maximum + /// number of points is 64. + OutlineFillRectangles { + /// The style that is used to fill the rectangles. + fill_style: Style, + /// The style that is used to draw the outline of the rectangles. + line_style: Style, + /// The width of the line. + line_width: f64, + /// The list of rectangles to be drawn. + rectangles: Box<[Rectangle]>, + }, + /// Fills a path and draws an outline at the same time. + /// + /// This command is a combination of [`FillPath`] and [`DrawLinePath`]. It + /// first performs a [`FillPath`] with the `fill_style`, then performs + /// [`DrawLinePath`] with `line_style` and `line_width`. + OutlineFillPath { + /// The style that is used to fill the path. + fill_style: Style, + /// The style that is used to draw the outline of the path. + line_style: Style, + /// The width of the line. + line_width: f64, + /// The path that should be drawn. + path: Path, + }, +} + +/// The header is different for outline commands, so we use this as a helper +struct OutlineHeader { + count: u32, + fill_style: Style, + line_style: Style, + line_width: f64, +} + +impl OutlineHeader { + fn read( + reader: &mut impl Read, + header: &TvgHeader, + prim_style_kind: StyleKind, + ) -> Result { + // the count and secondary style kind are stores in the same byte + let byte = reader.read_u8()?; + let count = (byte & 0b0011_1111) as u32 + 1; + let sec_style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?; + + let fill_style = Style::read(reader, header, prim_style_kind)?; + let line_style = Style::read(reader, header, sec_style_kind)?; + + let line_width = read_unit(reader, header)?; + + Ok(Self { + count, + fill_style, + line_style, + line_width, + }) + } +} + +impl Command { + pub fn read(reader: &mut impl Read, header: &TvgHeader) -> Result { + // the command name and primary style kind are stores in the same byte + let byte = reader.read_u8()?; + let command = CommandName::try_from(byte & 0b0011_1111)?; + let style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?; + + match command { + CommandName::EndOfDocument => Self::end_of_document(style_kind), + CommandName::FillPolygon => Self::read_fill_polygon(reader, header, style_kind), + CommandName::FillRectangles => Self::read_fill_rectangles(reader, header, style_kind), + CommandName::FillPath => Self::read_fill_path(reader, header, style_kind), + CommandName::DrawLines => Self::read_draw_lines(reader, header, style_kind), + CommandName::DrawLineLoop => Self::read_draw_line_loop(reader, header, style_kind), + CommandName::DrawLineStrip => Self::read_draw_line_strip(reader, header, style_kind), + CommandName::DrawLinePath => Self::read_draw_line_path(reader, header, style_kind), + CommandName::OutlineFillPolygon => { + Self::read_outline_fill_polygon(reader, header, style_kind) + } + CommandName::OutlineFillRectangles => { + Self::read_outline_fill_rectangles(reader, header, style_kind) + } + CommandName::OutlineFillPath => { + Self::read_outline_fill_path(reader, header, style_kind) + } + } + } + + pub fn is_end_of_document(&self) -> bool { + matches!(self, Self::EndOfDocument) + } + + fn read_command_header( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> io::Result<(u32, Style)> { + // every command adds one to the count + let count = read_varuint(reader)? + 1; + let style = Style::read(reader, header, style_kind)?; + + Ok((count, style)) + } + + fn end_of_document(style_kind: StyleKind) -> Result { + if style_kind != StyleKind::FlatColored { + Err(TvgError::InvalidEndOfDocument(style_kind as u8)) + } else { + Ok(Self::EndOfDocument) + } + } + + fn read_fill_polygon( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let (point_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; + if point_count < 3 { + yeet!(TvgError::InvalidPolygon(point_count)); + } + + let polygon = Point::read_multiple(reader, header, point_count)?; + + Ok(Self::FillPolygon { + fill_style, + polygon, + }) + } + + fn read_fill_rectangles( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let (rectangle_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; + let rectangles = Rectangle::read_multiple(reader, header, rectangle_count)?; + + Ok(Self::FillRectangles { + fill_style, + rectangles, + }) + } + + fn read_fill_path( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let (segment_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; + let path = Path::read(reader, header, segment_count)?; + + Ok(Self::FillPath { fill_style, path }) + } + + fn read_draw_lines( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let (line_count, line_style) = Self::read_command_header(reader, header, style_kind)?; + let line_width = read_unit(reader, header)?; + let lines = Line::read_multiple(reader, header, line_count)?; + + Ok(Self::DrawLines { + line_style, + line_width, + lines, + }) + } + + fn read_draw_line_loop( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?; + let line_width = read_unit(reader, header)?; + let points = Point::read_multiple(reader, header, point_count)?; + + Ok(Self::DrawLineLoop { + line_style, + line_width, + points, + }) + } + + fn read_draw_line_strip( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?; + let line_width = read_unit(reader, header)?; + let points = Point::read_multiple(reader, header, point_count)?; + + Ok(Self::DrawLineStrip { + line_style, + line_width, + points, + }) + } + + fn read_draw_line_path( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let (segment_count, line_style) = Self::read_command_header(reader, header, style_kind)?; + let line_width = read_unit(reader, header)?; + let path = Path::read(reader, header, segment_count)?; + + Ok(Self::DrawLinePath { + line_style, + line_width, + path, + }) + } + + fn read_outline_fill_polygon( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let OutlineHeader { + count: segment_count, + fill_style, + line_style, + line_width, + } = OutlineHeader::read(reader, header, style_kind)?; + + let points = Point::read_multiple(reader, header, segment_count)?; + + Ok(Self::OutlineFillPolygon { + fill_style, + line_style, + line_width, + points, + }) + } + + fn read_outline_fill_rectangles( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let OutlineHeader { + count: rect_count, + fill_style, + line_style, + line_width, + } = OutlineHeader::read(reader, header, style_kind)?; + + let rectangles = Rectangle::read_multiple(reader, header, rect_count)?; + + Ok(Self::OutlineFillRectangles { + fill_style, + line_style, + line_width, + rectangles, + }) + } + + fn read_outline_fill_path( + reader: &mut impl Read, + header: &TvgHeader, + style_kind: StyleKind, + ) -> Result { + let OutlineHeader { + count: segment_count, + fill_style, + line_style, + line_width, + } = OutlineHeader::read(reader, header, style_kind)?; + + let path = Path::read(reader, header, segment_count)?; + + Ok(Self::OutlineFillPath { + fill_style, + line_style, + line_width, + path, + }) + } +} diff --git a/tvg/src/header.rs b/tvg/src/header.rs new file mode 100644 index 0000000..b3be494 --- /dev/null +++ b/tvg/src/header.rs @@ -0,0 +1,149 @@ +use std::io::{self, Read}; + +use byteorder::{LittleEndian, ReadBytesExt}; +use num_enum::TryFromPrimitive; +use raise::yeet; + +use crate::colors::ColorEncoding; +use crate::read_varuint; +use crate::TvgError; + +const MAGIC: [u8; 2] = [0x72, 0x56]; +pub const SUPPORTED_VERSION: u8 = 1; + +/// The coordinate range defines how many bits a Unit value uses. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] +#[repr(u8)] +pub enum CoordinateRange { + /// Each [`Unit`] takes up 16 bits. + #[default] + Default = 0, + /// Each [`Unit`] takes up 8 bits. + Reduced = 1, + /// Each [`Unit`] takes up 32 bits. + Enhanced = 2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] +#[repr(u8)] +pub enum Scale { + _1 = 0, + _2 = 1, + _4 = 2, + _8 = 3, + _16 = 4, + _32 = 5, + _64 = 6, + _128 = 7, + _256 = 8, + _512 = 9, + _1024 = 10, + _2048 = 11, + _4096 = 12, + _8192 = 13, + _16384 = 14, + _32768 = 15, +} + +/// Each TVG file starts with a header defining some global values for the file +/// like scale and image size. This is a representation of the header, but not +/// necessarily an exact representation of the bits of a TVG header. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TvgHeader { + /// Must always be [0x72, 0x56] + magic: [u8; 2], + /// Must be 1. For future versions, this field might decide how the rest of + /// the format looks like. + version: u8, + /// Defines the number of fraction bits in a Unit value. + scale: Scale, + /// Defines the type of color information that is used in the + /// [`ColorTable`]. + color_encoding: ColorEncoding, + /// Defines the number of total bits in a Unit value and thus the overall + /// precision of the file. + coordinate_range: CoordinateRange, + /// Encodes the maximum width of the output file in *display units*. + /// + /// A value of 0 indicates that the image has the maximum possible width. + width: u32, + /// Encodes the maximum height of the output file in *display units*. + /// + /// A value of 0 indicates that the image has the maximum possible height. + height: u32, + /// The number of colors in the color table. + color_count: u32, +} + +impl TvgHeader { + pub fn read(reader: &mut R) -> Result { + // magic number is used as a first line defense against invalid data + let magic = [reader.read_u8()?, reader.read_u8()?]; + if magic != MAGIC { + yeet!(TvgError::InvalidFile); + } + + // the version of tvg being used + let version = reader.read_u8()?; + if version != SUPPORTED_VERSION { + yeet!(TvgError::UnsupportedVersion(version)) + } + + // scale, color_encoding, and coordinate_range are stored in one byte + let byte = reader.read_u8()?; + let scale = Scale::try_from_primitive(byte & 0b0000_1111).expect("invalid scale"); + let color_encoding = ColorEncoding::try_from_primitive((byte & 0b0011_0000) >> 4) + .expect("invalid color encoding"); + let coordinate_range = CoordinateRange::try_from_primitive((byte & 0b1100_0000) >> 6) + .expect("invalid coordinate range"); + + // width and height depend on the coordinate range + let width = read_unsigned_unit(coordinate_range, reader)?; + let height = read_unsigned_unit(coordinate_range, reader)?; + + let color_count = read_varuint(reader)?; + + Ok(Self { + magic, + version, + scale, + color_encoding, + coordinate_range, + width, + height, + color_count, + }) + } + + /// Defines the number of total bits in a Unit value and thus the overall + /// precision of the file. + pub fn coordinate_range(&self) -> CoordinateRange { + self.coordinate_range + } + + /// Defines the type of color information that is used in the [`ColorTable`]. + pub fn color_encoding(&self) -> ColorEncoding { + self.color_encoding + } + + /// Defines the number of fraction bits in a Unit value. + pub fn scale(&self) -> Scale { + self.scale + } + + /// The number of colors in the [`ColorTable`]. + pub fn color_count(&self) -> u32 { + self.color_count + } +} + +fn read_unsigned_unit( + coordinate_range: CoordinateRange, + bytes: &mut R, +) -> io::Result { + Ok(match coordinate_range { + CoordinateRange::Reduced => bytes.read_u8()? as u32, + CoordinateRange::Default => bytes.read_u16::()? as u32, + CoordinateRange::Enhanced => bytes.read_u32::()?, + }) +} diff --git a/tvg/src/lib.rs b/tvg/src/lib.rs new file mode 100644 index 0000000..5cbe33c --- /dev/null +++ b/tvg/src/lib.rs @@ -0,0 +1,151 @@ +use std::io::{self, Read}; + +use byteorder::{LittleEndian, ReadBytesExt}; +use colors::{Color, ColorTable}; +use commands::Command; +use header::{CoordinateRange, Scale, TvgHeader}; +use thiserror::Error; + +mod colors; +mod commands; +mod header; +mod path; + +pub use colors::Rgba16; +pub use header::SUPPORTED_VERSION; + +pub struct TvgFile { + header: TvgHeader, + color_table: ColorTable, + commands: Box<[Command]>, +} + +impl TvgFile { + pub fn read_from(reader: &mut impl Read) -> Result { + let header = TvgHeader::read(reader)?; + let color_table = + ColorTable::read_from_encoding(reader, header.color_count(), header.color_encoding())?; + + let mut commands = Vec::new(); + loop { + let command = Command::read(reader, &header)?; + commands.push(command.clone()); + + if command.is_end_of_document() { + break; + } + } + + Ok(Self { + header, + color_table, + commands: commands.into_boxed_slice(), + }) + } +} + +#[derive(Debug, Error)] +pub enum TvgError { + #[error("Not a valid TVG file. The magic number was invalid.")] + InvalidFile, + #[error("Expected version 1, but found version {}", .0)] + UnsupportedVersion(u8), + #[error("Found a coordinate range with an index of 3, which is invalid")] + InvalidCoordinateRange, + #[error("Found a command with an index of {}, which is invalid", .0)] + InvalidCommand(u8), + #[error("Found a style kind with an index of 3, which is invalid")] + InvalidStyleKind, + #[error("Polygons must have at least 3 points, found {}", .0)] + InvalidPolygon(u32), + #[error("The end of the document must have a `prim_style_kind` value of 0. Found {}", .0)] + InvalidEndOfDocument(u8), + #[error("{}", .0)] + IoError(#[from] io::Error), +} + +trait Decode: Sized { + fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result; + + fn read_multiple( + reader: &mut impl Read, + header: &TvgHeader, + count: u32, + ) -> io::Result> { + let mut vec = Vec::with_capacity(count as usize); + for _ in 0..count { + vec.push(Self::read(reader, header)?); + } + + Ok(vec.into_boxed_slice()) + } +} + +/// The unit is the common type for both positions and sizes in the vector +/// graphic. It is encoded as a signed integer with a configurable amount of +/// bits (see [`CoordinateRange`]) and fractional bits. +fn read_unit(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + let value = match header.coordinate_range() { + CoordinateRange::Reduced => reader.read_i8()? as i32, + CoordinateRange::Default => reader.read_i16::()? as i32, + CoordinateRange::Enhanced => reader.read_i32::()?, + }; + + let fractional = match header.scale() { + Scale::_1 => 1.0, + Scale::_2 => 2.0, + Scale::_4 => 4.0, + Scale::_8 => 8.0, + Scale::_16 => 16.0, + Scale::_32 => 32.0, + Scale::_64 => 64.0, + Scale::_128 => 128.0, + Scale::_256 => 256.0, + Scale::_512 => 512.0, + Scale::_1024 => 1024.0, + Scale::_2048 => 2048.0, + Scale::_4096 => 4096.0, + Scale::_8192 => 8192.0, + Scale::_16384 => 16_384.0, + Scale::_32768 => 32_768.0, + }; + + Ok((value as f64) / fractional) +} + +/// A X and Y coordinate pair. +#[derive(Debug, Clone, Copy)] +pub struct Point { + /// Horizontal distance of the point to the origin. + x: f64, + /// Vertical distance of the point to the origin. + y: f64, +} + +impl Decode for Point { + fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self { + x: read_unit(reader, header)?, + y: read_unit(reader, header)?, + }) + } +} + +fn read_varuint(reader: &mut impl Read) -> io::Result { + let mut count = 0; + let mut result = 0; + + loop { + let byte = reader.read_u8()? as u32; + let value = (byte & 0x7F) << (7 * count); + result |= value; + + if (byte & 0x80) == 0 { + break; + } + + count += 1; + } + + Ok(result) +} diff --git a/tvg/src/path.rs b/tvg/src/path.rs new file mode 100644 index 0000000..d2bf4fb --- /dev/null +++ b/tvg/src/path.rs @@ -0,0 +1,294 @@ +use std::io::{self, Read}; + +use byteorder::ReadBytesExt; +use num_enum::TryFromPrimitive; + +use crate::{header::TvgHeader, read_unit, read_varuint, Decode, Point}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] +#[repr(u8)] +enum Sweep { + Right = 0, + Left = 1, +} + +/// An instruction to move a hypothetical "pen". +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] +#[repr(u8)] +enum InstructionKind { + /// A straight line is drawn from the current point to a new point. + Line = 0, + /// A straight horizontal line is drawn from the current point to a new x + /// coordinate. + HorizontalLine = 1, + /// A straight vertical line is drawn from the current point to a new y + /// coordiante. + VerticalLine = 2, + /// A cubic Bézier curve is drawn from the current point to a new point. + CubicBezier = 3, + /// A circle segment is drawn from current point to a new point. + ArcCircle = 4, + /// An ellipse segment is drawn from current point to a new point. + ArcEllipse = 5, + /// The path is closed, and a straight line is drawn to the starting point. + ClosePath = 6, + /// A quadratic Bézier curve is drawn from the current point to a new point. + QuadraticBezier = 7, +} + +#[derive(Debug, Clone, Copy)] +enum InstructionData { + /// The line instruction draws a straight line to the position. + Line { + /// The end point of the line. + position: Point, + }, + /// The horizontal line instruction draws a straight horizontal line to a + /// given x coordinate. + HorizontalLine { + /// The new x coordinate. + x: f64, + }, + /// The vertical line instruction draws a straight vertical line to a given + /// y coordinate. + VerticalLine { + /// The new y coordinate. + y: f64, + }, + /// The cubic bezier instruction draws a Bézier curve with two control + /// points. + /// + /// The curve is drawn between the current location and `point_1` with + /// `control_0` being the first control point and `control_1` being the + /// second one. + CubicBezier { + /// The first control point. + control_0: Point, + /// The second control point. + control_1: Point, + /// The end point of the Bézier curve. + point_1: Point, + }, + /// Draws a circle segment between the current and the target point. + /// + /// The `radius` field determines the radius of the circle. If the distance + /// between the current point and `target` is larger than `radius`, the + /// distance is used as the radius. + ArcCircle { + /// If `true`, the large portion of the circle segment is drawn. + large_arc: bool, + /// Determines if the circle segment is left- or right bending. + sweep: Sweep, + /// The radius of the circle. + radius: f64, + /// The end point of the circle segment. + target: Point, + }, + /// Draws an ellipse segment between the current and the target point. + /// + /// The `radius_x` and `radius_y` fields determine the both radii of the + /// ellipse. If the distance between the current point and target is not + /// enough to fit any ellipse segment between the two points, `radius_x` + /// and `radius_y` are scaled uniformly so that it fits exactly. + ArcEllipse { + /// If `true`, the large portion of the ellipse segment is drawn. + large_arc: bool, + /// Determines if the ellipse segment is left- or right bending. + sweep: Sweep, + /// The radius of the ellipse segment in the horizontal direction. + radius_x: f64, + /// The radius of the ellipse segment in the vertical direction. + radius_y: f64, + /// The rotation of the ellipse in mathematical negative direction, in + /// degrees. + rotation: f64, + /// The end point of the ellipse segment. + target: Point, + }, + /// A straight line is drawn to the start location of the current segment. + /// This instruction doesn’t have additional data encoded. + ClosePath, + /// The quadratic bezier instruction draws a Bézier curve with a single + /// control point. + /// + /// The curve is drawn between the current location and `point_1` with + /// control being the control point. + QuadraticBezier { + /// The control point. + control: Point, + /// The end point of the Bézier curve. + target: Point, + }, +} + +impl InstructionData { + fn read(reader: &mut impl Read, header: &TvgHeader, kind: InstructionKind) -> io::Result { + match kind { + InstructionKind::Line => Self::read_line(reader, header), + InstructionKind::HorizontalLine => Self::read_horizontal_line(reader, header), + InstructionKind::VerticalLine => Self::read_vertical_line(reader, header), + InstructionKind::CubicBezier => Self::read_cubic_bezier(reader, header), + InstructionKind::ArcCircle => Self::read_arc_circle(reader, header), + InstructionKind::ArcEllipse => Self::read_arc_ellipse(reader, header), + InstructionKind::ClosePath => Ok(Self::ClosePath), + InstructionKind::QuadraticBezier => Self::read_quadratic_bezier(reader, header), + } + } + + fn read_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::Line { + position: Point::read(reader, header)?, + }) + } + + fn read_horizontal_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::HorizontalLine { + x: read_unit(reader, header)?, + }) + } + + fn read_vertical_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::VerticalLine { + y: read_unit(reader, header)?, + }) + } + + fn read_cubic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::CubicBezier { + control_0: Point::read(reader, header)?, + control_1: Point::read(reader, header)?, + point_1: Point::read(reader, header)?, + }) + } + + fn read_arc_header(reader: &mut impl Read, header: &TvgHeader) -> io::Result<(bool, Sweep)> { + // large_arc and sweep are stored in the same byte + let byte = reader.read_u8()?; + let large_arc = (byte & 1) != 0; + let sweep = match byte & 2 { + 0 => Sweep::Left, + _ => Sweep::Right, + }; + + Ok((large_arc, sweep)) + } + + fn read_arc_circle(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + let (large_arc, sweep) = Self::read_arc_header(reader, header)?; + let radius = read_unit(reader, header)?; + let target = Point::read(reader, header)?; + + Ok(Self::ArcCircle { + large_arc, + sweep, + radius, + target, + }) + } + + fn read_arc_ellipse(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + let (large_arc, sweep) = Self::read_arc_header(reader, header)?; + let radius_x = read_unit(reader, header)?; + let radius_y = read_unit(reader, header)?; + let rotation = read_unit(reader, header)?; + let target = Point::read(reader, header)?; + + Ok(Self::ArcEllipse { + large_arc, + sweep, + radius_x, + radius_y, + rotation, + target, + }) + } + + fn read_quadratic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + Ok(Self::QuadraticBezier { + control: Point::read(reader, header)?, + target: Point::read(reader, header)?, + }) + } +} + +#[derive(Debug, Clone)] +struct Instruction { + /// The width of the line the "pen" makes, if it makes one at all. + line_width: Option, + /// The arguments to the instruction. + data: InstructionData, +} + +impl Instruction { + fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { + let byte = reader.read_u8()?; + let instruction_kind = + InstructionKind::try_from_primitive(byte & 0b0000_0111).expect("invalid instruction"); + let has_line_width = (byte & 0b0001_0000) != 0; + + let line_width = has_line_width + .then(|| read_unit(reader, header)) + .transpose()?; + let data = InstructionData::read(reader, header, instruction_kind)?; + + Ok(Self { line_width, data }) + } +} + +#[derive(Debug, Clone)] +struct Segment { + /// The starting point of the segment. + start: Point, + /// The list of instructions for tha segment. + instructions: Box<[Instruction]>, +} + +impl Segment { + fn read(reader: &mut impl Read, header: &TvgHeader, segment_length: u32) -> io::Result { + let start = Point::read(reader, header)?; + + let mut instructions = Vec::with_capacity(segment_length as usize); + for _ in 0..segment_length { + instructions.push(Instruction::read(reader, header)?) + } + + Ok(Segment { + start, + instructions: instructions.into_boxed_slice(), + }) + } +} + +/// Paths describe instructions to create complex 2D graphics. +/// +/// Each path segment generates a shape by moving a ”pen” around. The path this +/// ”pen” takes is the outline of our segment. Each segment, the ”pen” starts +/// at a defined position and is moved by instructions. Each instruction will +/// leave the ”pen” at a new position. The line drawn by our ”pen” is the +/// outline of the shape. +#[derive(Debug, Clone)] +pub struct Path { + segments: Box<[Segment]>, +} + +impl Path { + pub(crate) fn read( + reader: &mut impl Read, + header: &TvgHeader, + segment_count: u32, + ) -> io::Result { + let mut segment_lengths = Vec::with_capacity(segment_count as usize); + for _ in 0..segment_count { + segment_lengths.push(read_varuint(reader)? + 1); + } + + let mut segments = Vec::with_capacity(segment_count as usize); + for segment_length in segment_lengths { + segments.push(Segment::read(reader, header, segment_length)?); + } + + Ok(Self { + segments: segments.into_boxed_slice(), + }) + } +} diff --git a/tvg/tests/examples/tvg/everything-32.tvg b/tvg/tests/examples/tvg/everything-32.tvg new file mode 100644 index 0000000..7ea4bdd Binary files /dev/null and b/tvg/tests/examples/tvg/everything-32.tvg differ diff --git a/tvg/tests/examples/tvg/everything.tvg b/tvg/tests/examples/tvg/everything.tvg new file mode 100644 index 0000000..b211c53 Binary files /dev/null and b/tvg/tests/examples/tvg/everything.tvg differ diff --git a/tvg/tests/examples/tvg/shield-16.tvg b/tvg/tests/examples/tvg/shield-16.tvg new file mode 100644 index 0000000..aacd3ea Binary files /dev/null and b/tvg/tests/examples/tvg/shield-16.tvg differ diff --git a/tvg/tests/examples/tvg/shield-32.tvg b/tvg/tests/examples/tvg/shield-32.tvg new file mode 100644 index 0000000..a2abc92 Binary files /dev/null and b/tvg/tests/examples/tvg/shield-32.tvg differ diff --git a/tvg/tests/examples/tvg/shield-8.tvg b/tvg/tests/examples/tvg/shield-8.tvg new file mode 100644 index 0000000..57033be Binary files /dev/null and b/tvg/tests/examples/tvg/shield-8.tvg differ diff --git a/tvg/tests/parse.rs b/tvg/tests/parse.rs new file mode 100644 index 0000000..eb6e801 --- /dev/null +++ b/tvg/tests/parse.rs @@ -0,0 +1,14 @@ +use std::fs::File; + +use alligator_tvg::{Rgba16, TvgFile}; + +#[test] +fn main() { + for entry in std::fs::read_dir("tests/examples/tvg").unwrap() { + let entry = entry.unwrap().file_name(); + let entry = entry.to_string_lossy(); + let mut file = File::open(format!("./tests/examples/tvg/{}", &entry)).unwrap(); + let _: TvgFile = TvgFile::read_from(&mut file).unwrap(); + println!("{:?} succeeded!", entry); + } +} -- cgit v1.2.3