diff --git a/Cargo.toml b/Cargo.toml index 7e02d566e..0199a4708 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ lyon_tessellation = "1.0.1" image = { version = "0.24.6", optional = true, default-features = false } cosmic-text = { version = "0.8" } alot = "0.1" +ahash = "0.8.3" [dev-dependencies] image = { features = ["png"] } diff --git a/README.md b/README.md new file mode 100644 index 000000000..d6b9100cc --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Kludgine (Redux) + +This branch is a rewrite of Kludgine, aiming to be a lightweight, efficient 2d +rendering option for `wgpu`-based applications. diff --git a/examples/shapes.rs b/examples/shapes.rs index 5d4b135b8..92fb0f8e2 100644 --- a/examples/shapes.rs +++ b/examples/shapes.rs @@ -3,8 +3,8 @@ use std::time::Duration; use appit::RunningWindow; use kludgine::app::WindowBehavior; use kludgine::math::{Dips, Pixels, Point, Rect, Size}; -use kludgine::Color; -use kludgine::{PathBuilder, PreparedGraphic}; +use kludgine::shapes::PathBuilder; +use kludgine::{Color, PreparedGraphic}; fn main() { Test::run(); diff --git a/examples/texture.rs b/examples/texture.rs index ec82a96c8..7e5d66365 100644 --- a/examples/texture.rs +++ b/examples/texture.rs @@ -3,8 +3,7 @@ use std::time::Duration; use appit::RunningWindow; use kludgine::app::WindowBehavior; use kludgine::math::{Dips, Point, Rect, Size}; -use kludgine::PreparedGraphic; -use kludgine::Texture; +use kludgine::{PreparedGraphic, Texture}; fn main() { Test::run(); diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..db443f878 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ +unstable_features = true +use_field_init_shorthand = true +imports_granularity = "Module" +group_imports = "StdExternalCrate" +format_code_in_doc_comments = true +reorder_impl_items = true diff --git a/src/app.rs b/src/app.rs index f533caeb0..ef3f09b5c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,8 +3,9 @@ use std::sync::{Arc, OnceLock}; use appit::{RunningWindow, WindowBehavior as _}; -use crate::shapes::PushConstants; -use crate::{Color, Graphics, Kludgine, Renderer, Rendering, RenderingGraphics}; +use crate::pipeline::PushConstants; +use crate::render::{Renderer, Rendering}; +use crate::{Color, Graphics, Kludgine, RenderingGraphics}; fn shared_wgpu() -> Arc { static SHARED_WGPU: OnceLock> = OnceLock::new(); @@ -13,6 +14,7 @@ fn shared_wgpu() -> Arc { pub trait WindowBehavior: Sized + 'static { type Context: Send + 'static; + fn initialize( window: &mut RunningWindow, graphics: &mut Graphics<'_>, @@ -28,14 +30,17 @@ pub trait WindowBehavior: Sized + 'static { graphics: &mut RenderingGraphics<'_, 'pass>, ) -> bool; + #[must_use] fn power_preference() -> wgpu::PowerPreference { wgpu::PowerPreference::default() } + #[must_use] fn limits(adapter_limits: wgpu::Limits) -> wgpu::Limits { wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits) } + #[must_use] fn clear_color() -> Option { Some(Color::BLACK) } @@ -61,7 +66,7 @@ struct KludgineWindow { device: wgpu::Device, queue: wgpu::Queue, _adapter: wgpu::Adapter, - _wgpu: Arc, + wgpu: Arc, } impl appit::WindowBehavior for KludgineWindow @@ -70,7 +75,11 @@ where { type Context = (Arc, T::Context); + #[allow(unsafe_code)] fn initialize(window: &mut RunningWindow, (wgpu, context): Self::Context) -> Self { + // SAFETY: This function is only invoked once the window has been + // created, and cannot be invoked after the underlying window has been + // destroyed. let surface = unsafe { wgpu.create_surface(window.winit()).unwrap() }; let adapter = pollster::block_on(wgpu.request_adapter(&wgpu::RequestAdapterOptions { power_preference: T::power_preference(), @@ -79,7 +88,9 @@ where })) .unwrap(); let mut limits = T::limits(adapter.limits()); - limits.max_push_constant_size = size_of::() as u32; + limits.max_push_constant_size = size_of::() + .try_into() + .expect("should fit :)"); let (device, queue) = pollster::block_on(adapter.request_device( &wgpu::DeviceDescriptor { label: None, @@ -98,7 +109,7 @@ where &queue, swapchain_format, window.inner_size().into(), - window.scale() as f32, + lossy_f64_to_f32(window.scale()), ); let mut graphics = Graphics::new(&mut state, &device, &queue); @@ -116,7 +127,7 @@ where let behavior = T::initialize(window, &mut graphics, context); Self { - _wgpu: wgpu, + wgpu, kludgine: state, _adapter: adapter, behavior, @@ -127,6 +138,7 @@ where } } + #[allow(unsafe_code)] fn redraw(&mut self, window: &mut RunningWindow) { let frame = loop { match self.surface.get_current_texture() { @@ -140,9 +152,9 @@ where return; } wgpu::SurfaceError::Lost => { - println!("Lost surface, reconfiguring"); - self.surface = - unsafe { self._wgpu.create_surface(window.winit()).unwrap() }; + // SAFETY: redraw is only called while the event loop + // and window are still alive. + self.surface = unsafe { self.wgpu.create_surface(window.winit()).unwrap() }; self.surface.configure(&self.device, &self.config); } wgpu::SurfaceError::OutOfMemory => { @@ -200,7 +212,7 @@ where self.surface.configure(&self.device, &self.config); self.kludgine.resize( window.inner_size().into(), - window.scale() as f32, + lossy_f64_to_f32(window.scale()), &self.queue, ); // TODO pass onto kludgine @@ -257,3 +269,15 @@ where { CallbackWindow::run_with(render_fn) } + +/// Performs `value as f32`. +/// +/// This function exists solely because of clippy. The truncation of f64 -> f32 +/// isn't as severe as truncation of integer types, but it's lumped into the +/// same lint. I don't want to disable the truncation lint, and I don't want +/// functions that need to do this operation to not be checking for integer +/// truncation. +#[allow(clippy::cast_possible_truncation)] // truncation desired +fn lossy_f64_to_f32(value: f64) -> f32 { + value as f32 +} diff --git a/src/atlas.rs b/src/atlas.rs index 828e1522a..c25bfa4b8 100644 --- a/src/atlas.rs +++ b/src/atlas.rs @@ -6,9 +6,8 @@ use alot::{LotId, Lots}; use crate::math::{Rect, Size, ToFloat, UPixels}; use crate::pack::{TextureAllocation, TexturePacker}; -use crate::sealed; -use crate::shapes::{PreparedGraphic, Vertex}; -use crate::{Graphics, Texture, TextureSource, WgpuDeviceAndQueue}; +use crate::pipeline::{PreparedGraphic, Vertex}; +use crate::{sealed, Graphics, Texture, TextureSource, WgpuDeviceAndQueue}; #[derive(Debug, Clone)] pub struct TextureCollection { @@ -23,13 +22,13 @@ struct Data { } impl TextureCollection { - pub fn new( + pub(crate) fn new_generic( initial_size: Size, minimum_column_width: u16, format: wgpu::TextureFormat, graphics: &impl WgpuDeviceAndQueue, ) -> Self { - let texture = Texture::new( + let texture = Texture::new_generic( graphics, initial_size, format, @@ -45,6 +44,16 @@ impl TextureCollection { } } + #[must_use] + pub fn new( + initial_size: Size, + minimum_column_width: u16, + format: wgpu::TextureFormat, + graphics: &Graphics<'_>, + ) -> Self { + Self::new_generic(initial_size, minimum_column_width, format, graphics) + } + pub fn push_texture( &mut self, data: &[u8], diff --git a/src/lib.rs b/src/lib.rs index 5faf7940f..1a3f26800 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,21 +1,26 @@ +#![doc = include_str!("../README.md")] +// This crate uses unsafe, but attempts to minimize its usage. All functions +// that utilize unsafe must explicitly enable it. +#![deny(unsafe_code)] +#![warn( + // missing_docs, + clippy::pedantic +)] +#![allow(clippy::module_name_repetitions)] + use std::borrow::Cow; -use std::collections::{hash_map, HashMap}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; -use std::mem::size_of; -use std::ops::{Add, Div, Neg, Range}; +use std::ops::{Add, Deref, DerefMut, Div, Neg}; use std::sync::Arc; use bytemuck::{Pod, Zeroable}; use wgpu::util::DeviceExt; use crate::buffer::Buffer; -use crate::math::{ - Dips, Pixels, Point, Ratio, Rect, ScreenTransformation, Size, ToFloat, UPixels, Zero, -}; -use crate::shapes::{ - PushConstants, Vertex, FLAG_ROTATE, FLAG_SCALE, FLAG_TEXTURED, FLAG_TRANSLATE, -}; +use crate::math::{Point, Rect, Size, ToFloat, UPixels}; +use crate::pipeline::{Uniforms, Vertex}; +use crate::shapes::PathBuilder; #[cfg(feature = "app")] pub mod app; @@ -23,91 +28,45 @@ mod atlas; mod buffer; pub mod math; mod pack; +mod pipeline; +mod pod; +pub mod render; mod sealed; -mod shapes; - -pub use shapes::{Path, PathBuilder, PreparedGraphic, ShaderScalable, Shape}; +pub mod shapes; pub use atlas::{CollectedTexture, TextureCollection}; +pub use pipeline::{PreparedGraphic, ShaderScalable}; pub struct Kludgine { default_bindings: wgpu::BindGroup, - shapes_pipeline: wgpu::RenderPipeline, - _shapes_shader: wgpu::ShaderModule, + pipeline: wgpu::RenderPipeline, + _shader: wgpu::ShaderModule, binding_layout: wgpu::BindGroupLayout, sampler: wgpu::Sampler, uniforms: Buffer, - fonts: cosmic_text::FontSystem, - cache: cosmic_text::SwashCache, + pub fonts: cosmic_text::FontSystem, + pub swash_cache: cosmic_text::SwashCache, text_atlas: TextureCollection, } impl Kludgine { + #[must_use] pub fn new( device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat, - initial_size: Size, + initial_size: Size, scale: f32, ) -> Self { let uniforms = Buffer::new( - &[Uniforms { - ortho: ScreenTransformation::ortho( - 0., - 0., - initial_size.width.into(), - initial_size.height.into(), - -1.0, - 1.0, - ) - .into_array(), - scale: Ratio::from_f32(scale), - _padding: [0; 3], - }], + &[Uniforms::new(initial_size, scale)], wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, device, ); - let binding_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - 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, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::VERTEX | 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: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); + let binding_layout = pipeline::bind_group_layout(device); - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[&binding_layout], - push_constant_ranges: &[wgpu::PushConstantRange { - stages: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - range: 0..size_of::() as u32, - }], - }); + let pipeline_layout = pipeline::layout(device, &binding_layout); let empty_texture = device.create_texture(&wgpu::TextureDescriptor { label: None, @@ -125,103 +84,25 @@ impl Kludgine { }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); - let shapes_bindings = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &binding_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &uniforms.wgpu, - offset: 0, - size: None, - }), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView( - &empty_texture.create_view(&wgpu::TextureViewDescriptor::default()), - ), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&sampler), - }, - ], - }); + let default_bindings = pipeline::bind_group( + device, + &binding_layout, + &uniforms.wgpu, + &empty_texture.create_view(&wgpu::TextureViewDescriptor::default()), + &sampler, + ); - let shapes_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: None, - source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shapes.wgsl"))), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), }); - let shapes_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("shapes"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shapes_shader, - entry_point: "vs_main", - buffers: &[wgpu::VertexBufferLayout { - array_stride: size_of::>() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - format: wgpu::VertexFormat::Sint32x2, - offset: 0, - shader_location: 0, - }, - wgpu::VertexAttribute { - format: wgpu::VertexFormat::Uint32x2, - offset: 8, - shader_location: 1, - }, - wgpu::VertexAttribute { - format: wgpu::VertexFormat::Uint32, - offset: 16, - shader_location: 2, - }, - ], - }], - }, - fragment: Some(wgpu::FragmentState { - module: &shapes_shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }), - - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); + let pipeline = pipeline::new(device, &pipeline_layout, &shader, format); let fonts = cosmic_text::FontSystem::new(); Self { - text_atlas: TextureCollection::new( + text_atlas: TextureCollection::new_generic( Size::new(512, 512), 128, wgpu::TextureFormat::Bgra8Unorm, @@ -234,41 +115,26 @@ impl Kludgine { }, ), - default_bindings: shapes_bindings, - shapes_pipeline, - _shapes_shader: shapes_shader, + default_bindings, + pipeline, + _shader: shader, sampler, uniforms, binding_layout, fonts, - cache: cosmic_text::SwashCache::new(), + swash_cache: cosmic_text::SwashCache::new(), } } - pub fn resize(&self, new_size: Size, new_scale: f32, queue: &wgpu::Queue) { - self.uniforms.update( - 0, - &[Uniforms { - ortho: ScreenTransformation::ortho( - 0., - 0., - new_size.width.into(), - new_size.height.into(), - -1.0, - 1.0, - ) - .into_array(), - scale: Ratio::from_f32(new_scale), - _padding: [0; 3], - }], - queue, - ); + pub fn resize(&self, new_size: Size, new_scale: f32, queue: &wgpu::Queue) { + self.uniforms + .update(0, &[Uniforms::new(new_size, new_scale)], queue); } } -pub trait WgpuDeviceAndQueue { +trait WgpuDeviceAndQueue { fn device(&self) -> &wgpu::Device; fn queue(&self) -> &wgpu::Queue; fn binding_layout(&self) -> &wgpu::BindGroupLayout; @@ -335,16 +201,28 @@ pub struct Graphics<'gfx> { } impl<'gfx> Graphics<'gfx> { + #[must_use] pub const fn device(&self) -> &'gfx wgpu::Device { self.device } + #[must_use] pub const fn queue(&self) -> &'gfx wgpu::Queue { self.queue } +} + +impl Deref for Graphics<'_> { + type Target = Kludgine; - pub fn font_system(&mut self) -> &mut cosmic_text::FontSystem { - &mut self.kludgine.fonts + fn deref(&self) -> &Self::Target { + self.kludgine + } +} + +impl DerefMut for Graphics<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.kludgine } } @@ -385,14 +263,18 @@ impl<'gfx, 'pass> RenderingGraphics<'gfx, 'pass> { pipeline_is_active: false, } } + + #[must_use] pub const fn device(&self) -> &'gfx wgpu::Device { self.device } + #[must_use] pub const fn queue(&self) -> &'gfx wgpu::Queue { self.queue } + #[must_use] pub fn render_pass(&mut self) -> &mut wgpu::RenderPass<'pass> { // When we expose the render pass, we can't guarantee we're the current pipeline anymore. self.pipeline_is_active = false; @@ -404,7 +286,7 @@ impl<'gfx, 'pass> RenderingGraphics<'gfx, 'pass> { false } else { self.pipeline_is_active = true; - self.pass.set_pipeline(&self.state.shapes_pipeline); + self.pass.set_pipeline(&self.state.pipeline); true } } @@ -415,49 +297,67 @@ impl<'gfx, 'pass> RenderingGraphics<'gfx, 'pass> { pub struct Color(u32); impl Color { + #[must_use] pub const fn new(red: u8, green: u8, blue: u8, alpha: u8) -> Self { Self((red as u32) << 24 | (green as u32) << 16 | (blue as u32) << 8 | alpha as u32) } + /// Returns a new color by converting each component from its `0.0..=1.0` + /// range into a `0..=255` range. + #[must_use] + #[allow(clippy::cast_possible_truncation)] // truncation desired + #[allow(clippy::cast_sign_loss)] // sign loss is truncated pub fn new_f32(red: f32, green: f32, blue: f32, alpha: f32) -> Self { Self::new( - (red * 255.).round() as u8, - (green * 255.).round() as u8, - (blue * 255.).round() as u8, - (alpha * 255.).round() as u8, + (red.max(0.) * 255.).round() as u8, + (green.max(0.) * 255.).round() as u8, + (blue.max(0.) * 255.).round() as u8, + (alpha.max(0.) * 255.).round() as u8, ) } + #[must_use] + #[allow(clippy::cast_possible_truncation)] // truncation desired pub const fn red(&self) -> u8 { (self.0 >> 24) as u8 } + #[must_use] pub fn red_f32(&self) -> f32 { - self.red() as f32 / 255. + f32::from(self.red()) / 255. } + #[must_use] + #[allow(clippy::cast_possible_truncation)] // truncation desired pub const fn green(&self) -> u8 { (self.0 >> 16) as u8 } + #[must_use] pub fn green_f32(&self) -> f32 { - self.green() as f32 / 255. + f32::from(self.green()) / 255. } + #[must_use] + #[allow(clippy::cast_possible_truncation)] // truncation desired pub const fn blue(&self) -> u8 { (self.0 >> 8) as u8 } + #[must_use] pub fn blue_f32(&self) -> f32 { - self.blue() as f32 / 255. + f32::from(self.blue()) / 255. } + #[must_use] + #[allow(clippy::cast_possible_truncation)] // truncation desired pub const fn alpha(&self) -> u8 { self.0 as u8 } + #[must_use] pub fn alpha_f32(&self) -> f32 { - self.alpha() as f32 / 255. + f32::from(self.alpha()) / 255. } } @@ -470,10 +370,10 @@ impl Debug for Color { impl From for wgpu::Color { fn from(color: Color) -> Self { Self { - r: color.red_f32() as f64, - g: color.green_f32() as f64, - b: color.blue_f32() as f64, - a: color.alpha_f32() as f64, + r: f64::from(color.red_f32()), + g: f64::from(color.green_f32()), + b: f64::from(color.blue_f32()), + a: f64::from(color.alpha_f32()), } } } @@ -786,14 +686,6 @@ impl Color { pub const YELLOWGREEN: Self = Self::new(154, 205, 50, 255); } -#[derive(Pod, Zeroable, Copy, Clone)] -#[repr(C)] -struct Uniforms { - ortho: [f32; 16], - scale: Ratio, - _padding: [u32; 3], -} - #[derive(Debug)] pub struct Texture { id: sealed::TextureId, @@ -803,7 +695,7 @@ pub struct Texture { } impl Texture { - pub fn new( + pub(crate) fn new_generic( graphics: &impl WgpuDeviceAndQueue, size: Size, format: wgpu::TextureFormat, @@ -820,7 +712,13 @@ impl Texture { view_formats: &[], }); let view = wgpu.create_view(&wgpu::TextureViewDescriptor::default()); - let bind_group = Arc::new(create_bind_group(graphics, &view)); + let bind_group = Arc::new(pipeline::bind_group( + graphics.device(), + graphics.binding_layout(), + graphics.uniforms(), + &view, + graphics.sampler(), + )); Self { id: sealed::TextureId::new_unique_id(), wgpu, @@ -829,8 +727,19 @@ impl Texture { } } + #[must_use] + pub fn new( + graphics: &Graphics<'_>, + size: Size, + format: wgpu::TextureFormat, + usage: wgpu::TextureUsages, + ) -> Self { + Self::new_generic(graphics, size, format, usage) + } + + #[must_use] pub fn new_with_data( - graphics: &impl WgpuDeviceAndQueue, + graphics: &Graphics<'_>, size: Size, format: wgpu::TextureFormat, usage: wgpu::TextureUsages, @@ -851,7 +760,13 @@ impl Texture { data, ); let view = wgpu.create_view(&wgpu::TextureViewDescriptor::default()); - let bind_group = Arc::new(create_bind_group(graphics, &view)); + let bind_group = Arc::new(pipeline::bind_group( + graphics.device(), + graphics.binding_layout(), + graphics.uniforms(), + &view, + graphics.sampler(), + )); Self { id: sealed::TextureId::new_unique_id(), wgpu, @@ -860,6 +775,7 @@ impl Texture { } } + #[must_use] pub fn create_render_pass<'gfx>( &'gfx self, encoder: &'gfx mut wgpu::CommandEncoder, @@ -883,6 +799,7 @@ impl Texture { pass } + #[must_use] #[cfg(feature = "image")] pub fn from_image(image: &image::DynamicImage, graphics: &Graphics<'_>) -> Self { let image = image.to_rgba8(); @@ -895,6 +812,7 @@ impl Texture { ) } + #[must_use] pub fn prepare_sized( &self, size: Size, @@ -915,6 +833,7 @@ impl Texture { self.prepare(Rect::new(Point::default(), size), graphics) } + #[must_use] pub fn prepare(&self, dest: Rect, graphics: &Graphics<'_>) -> PreparedGraphic where Unit: Add @@ -930,6 +849,7 @@ impl Texture { self.prepare_partial(self.size().into(), dest, graphics) } + #[must_use] pub fn prepare_partial( &self, source: Rect, @@ -964,9 +884,7 @@ impl Texture { .prepare(self, graphics) } - // pub fn read_into(&self, destination: &mut Vec, device: &wgpu::) { - // dev - // } + #[must_use] pub fn size(&self) -> Size { Size { width: UPixels(self.wgpu.width()), @@ -974,41 +892,12 @@ impl Texture { } } + #[must_use] pub fn format(&self) -> wgpu::TextureFormat { self.wgpu.format() } } -fn create_bind_group( - graphics: &impl WgpuDeviceAndQueue, - view: &wgpu::TextureView, -) -> wgpu::BindGroup { - graphics - .device() - .create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: graphics.binding_layout(), - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: graphics.uniforms(), - offset: 0, - size: None, - }), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(graphics.sampler()), - }, - ], - }) -} - // pub struct PreparedTexture { // shape: PreparedShape, // } @@ -1046,245 +935,3 @@ impl sealed::TextureSource for Texture { self.id } } - -pub struct Renderer<'render, 'gfx> { - graphics: &'render mut Graphics<'gfx>, - data: &'render mut Rendering, -} - -#[derive(Debug)] -struct Command { - indices: Range, - constants: PushConstants, - texture: Option, -} - -#[derive(Eq, PartialEq, Debug, Clone, Copy)] -struct VertexId(Vertex); - -impl Hash for VertexId { - fn hash(&self, state: &mut H) { - bytemuck::bytes_of(&self.0).hash(state); - } -} - -impl Renderer<'_, '_> { - pub fn draw_shape( - &mut self, - shape: &Shape, - origin: Point, - rotation_rads: Option, - scale: Option, - ) where - Unit: Into + Zero + Copy, - Unit: ShaderScalable, - { - self.inner_draw( - shape, - Option::<&Texture>::None, - origin, - rotation_rads, - scale, - ); - } - - pub fn draw_textured_shape( - &mut self, - shape: &Shape, - texture: &impl TextureSource, - origin: Point, - rotation_rads: Option, - scale: Option, - ) where - Unit: Into + Zero + Copy, - Unit: ShaderScalable, - { - self.inner_draw(shape, Some(texture), origin, rotation_rads, scale); - } - - fn inner_draw( - &mut self, - shape: &Shape, - texture: Option<&impl TextureSource>, - origin: Point, - rotation_rads: Option, - scale: Option, - ) where - Unit: Into + Zero + Copy, - Unit: ShaderScalable, - { - // Merge the vertices into the graphics - let mut vertex_map = Vec::with_capacity(shape.vertices.len()); - for vertex in shape.vertices.iter().copied() { - let vertex = Vertex { - location: Point { - x: vertex.location.x.into(), - y: vertex.location.y.into(), - }, - texture: vertex.texture, - color: vertex.color, - }; - let index = *self - .data - .vertex_index_by_id - .entry(VertexId(vertex)) - .or_insert_with(|| { - let index = self - .data - .vertices - .len() - .try_into() - .expect("too many vertices being drawn"); - self.data.vertices.push(vertex); - index - }); - vertex_map.push(index); - } - - let first_index_drawn = self.data.indices.len(); - for &vertex_index in &shape.indices { - self.data - .indices - .push(vertex_map[usize::from(vertex_index)]); - } - - let mut flags = Unit::flags(); - assert_eq!(TEXTURED, texture.is_some()); - let texture = if let Some(texture) = texture { - flags |= FLAG_TEXTURED; - let id = texture.id(); - if let hash_map::Entry::Vacant(entry) = self.data.textures.entry(id) { - entry.insert(texture.bind_group(self.graphics)); - } - Some(id) - } else { - None - }; - let scale = scale.map_or(1., |scale| { - flags |= FLAG_SCALE; - scale - }); - let rotation = rotation_rads.map_or(0., |scale| { - flags |= FLAG_ROTATE; - scale - }); - if !origin.is_zero() { - flags |= FLAG_TRANSLATE; - } - - self.data.commands.push(Command { - indices: first_index_drawn - .try_into() - .expect("too many drawn verticies") - ..self - .data - .indices - .len() - .try_into() - .expect("too many drawn verticies"), - constants: PushConstants { - flags, - scale, - rotation, - translation: Point { - x: origin.x.into(), - y: origin.y.into(), - }, - }, - texture, - }); - } -} - -impl Drop for Renderer<'_, '_> { - fn drop(&mut self) { - if self.data.indices.is_empty() { - self.data.buffers = None; - } else { - self.data.buffers = Some(RenderingBuffers { - vertex: Buffer::new( - &self.data.vertices, - wgpu::BufferUsages::VERTEX, - self.graphics.device, - ), - index: Buffer::new( - &self.data.indices, - wgpu::BufferUsages::INDEX, - self.graphics.device, - ), - }); - } - } -} - -#[derive(Default, Debug)] -pub struct Rendering { - buffers: Option, - vertices: Vec>, - vertex_index_by_id: HashMap, - indices: Vec, - textures: HashMap>, - commands: Vec, -} - -#[derive(Debug)] -struct RenderingBuffers { - vertex: Buffer>, - index: Buffer, -} - -impl Rendering { - pub fn new_frame<'rendering, 'gfx>( - &'rendering mut self, - graphics: &'rendering mut Graphics<'gfx>, - ) -> Renderer<'rendering, 'gfx> { - self.commands.clear(); - self.indices.clear(); - self.textures.clear(); - self.vertex_index_by_id.clear(); - self.vertices.clear(); - Renderer { - graphics, - data: self, - } - } - - pub fn render<'pass>(&'pass self, graphics: &mut RenderingGraphics<'_, 'pass>) { - if let Some(buffers) = &self.buffers { - let mut current_texture_id = None; - let mut needs_texture_binding = graphics.active_pipeline_if_needed(); - - graphics - .pass - .set_vertex_buffer(0, buffers.vertex.as_slice()); - graphics - .pass - .set_index_buffer(buffers.index.as_slice(), wgpu::IndexFormat::Uint16); - - for command in &self.commands { - if let Some(texture_id) = &command.texture { - if current_texture_id != Some(*texture_id) { - current_texture_id = Some(*texture_id); - graphics.pass.set_bind_group( - 0, - self.textures.get(texture_id).expect("texture missing"), - &[], - ); - } - } else if needs_texture_binding { - needs_texture_binding = false; - graphics - .pass - .set_bind_group(0, &graphics.state.default_bindings, &[]); - } - - graphics.pass.set_push_constants( - wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - 0, - bytemuck::bytes_of(&command.constants), - ); - graphics.pass.draw_indexed(command.indices.clone(), 0, 0..1); - } - } - } -} diff --git a/src/math.rs b/src/math.rs index 0f3454ce3..113dc7964 100644 --- a/src/math.rs +++ b/src/math.rs @@ -1,8 +1,9 @@ -use bytemuck::{Pod, Zeroable}; use std::fmt; use std::num::TryFromIntError; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use bytemuck::{Pod, Zeroable}; + pub trait ToFloat { type Float; fn into_float(self) -> Self::Float; @@ -12,11 +13,15 @@ pub trait ToFloat { impl ToFloat for u32 { type Float = f32; + #[allow(clippy::cast_precision_loss)] // precision loss desired to best approximate the value fn into_float(self) -> Self::Float { self as f32 } + #[allow(clippy::cast_possible_truncation)] // truncation desired + #[allow(clippy::cast_sign_loss)] // sign loss is asserted fn from_float(float: Self::Float) -> Self { + assert!(float.is_sign_positive()); float as u32 } } @@ -28,28 +33,15 @@ macro_rules! define_integer_type { pub struct $name(pub $inner); impl $name { + #[must_use] pub const fn div(self, rhs: $inner) -> Self { Self(self.0 / rhs) } - - pub const fn into_f32(self) -> f32 { - self.0 as f32 - } - - pub const fn into_f64(self) -> f64 { - self.0 as f64 - } - } - - impl From<$name> for f64 { - fn from(value: $name) -> Self { - value.into_f64() - } } impl From<$name> for f32 { fn from(value: $name) -> Self { - value.into_f32() + value.into_float() } } @@ -230,12 +222,13 @@ impl TryFrom for UPixels { } impl Dips { - pub const INCH: Self = Dips(2540); pub const CM: Self = Dips(1000); + pub const INCH: Self = Dips(2540); pub const MM: Self = Self::CM.div(10); } impl From for Dips { + #[allow(clippy::cast_possible_truncation)] // truncation desired fn from(cm: f32) -> Self { Dips((cm * 1000.) as i32) } @@ -244,6 +237,7 @@ impl From for Dips { impl ToFloat for Dips { type Float = f32; + #[allow(clippy::cast_precision_loss)] // precision loss desired to best approximate the value fn into_float(self) -> Self::Float { self.0 as f32 / 1000. } @@ -260,6 +254,7 @@ impl fmt::Debug for Dips { } impl From for Pixels { + #[allow(clippy::cast_possible_truncation)] // truncation desired fn from(pixels: f32) -> Self { Pixels(pixels as i32) } @@ -268,6 +263,7 @@ impl From for Pixels { impl ToFloat for Pixels { type Float = f32; + #[allow(clippy::cast_precision_loss)] // precision loss desired to best approximate the value fn into_float(self) -> Self::Float { self.0 as f32 } @@ -284,14 +280,21 @@ impl fmt::Debug for Pixels { } impl From for UPixels { + #[allow(clippy::cast_possible_truncation)] // truncation desired + #[allow(clippy::cast_sign_loss)] // sign loss is handled fn from(pixels: f32) -> Self { - Self(pixels as u32) + if pixels < 0. { + Self(0) + } else { + Self(pixels as u32) + } } } impl ToFloat for UPixels { type Float = f32; + #[allow(clippy::cast_precision_loss)] // precision loss desired to best approximate the value fn into_float(self) -> Self::Float { self.0 as f32 } @@ -353,11 +356,11 @@ where } } -impl From> for Size { +impl From> for Size { fn from(value: appit::winit::dpi::PhysicalSize) -> Self { Self { - width: Pixels(value.width.try_into().expect("width too large")), - height: Pixels(value.height.try_into().expect("height too large")), + width: value.width.try_into().expect("width too large"), + height: value.height.try_into().expect("height too large"), } } } @@ -457,15 +460,6 @@ where } } -unsafe impl bytemuck::Pod for Point {} -unsafe impl bytemuck::Zeroable for Point {} -unsafe impl bytemuck::Pod for Point {} -unsafe impl bytemuck::Zeroable for Point {} -unsafe impl bytemuck::Pod for Point {} -unsafe impl bytemuck::Zeroable for Point {} -unsafe impl bytemuck::Pod for Point {} -unsafe impl bytemuck::Zeroable for Point {} - #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub struct Size { pub width: Unit, @@ -688,18 +682,21 @@ pub(crate) struct Ratio { } impl Ratio { + #[allow(clippy::cast_possible_truncation)] // truncation desired + #[allow(clippy::cast_sign_loss)] // negative scales are handled pub fn from_f32(scale: f32) -> Self { + let scale = scale.max(0.); let mut best = Ratio { div_by: 0, mul_by: 0, }; let mut best_diff = f32::MAX; for div_by in 1..=u16::MAX { - let mul_by = (div_by as f32 * scale) as u16; - let test = Ratio { div_by, mul_by }; - let delta = (test.into_f32() - scale).abs(); + let mul_by = (f32::from(div_by) * scale) as u16; + let ratio = Ratio { div_by, mul_by }; + let delta = (ratio.into_f32() - scale).abs(); if delta < best_diff { - best = test; + best = ratio; best_diff = delta; if delta < 0.00001 { break; @@ -711,7 +708,7 @@ impl Ratio { } pub fn into_f32(self) -> f32 { - self.mul_by as f32 / self.div_by as f32 + f32::from(self.mul_by) / f32::from(self.div_by) } } @@ -814,44 +811,3 @@ fn scale_factor_from_f32() { } ); } - -#[derive(Clone, Copy, Debug)] -pub(crate) struct ScreenTransformation([f32; 16]); - -impl ScreenTransformation { - pub fn ortho(left: f32, top: f32, right: f32, bottom: f32, near: f32, far: f32) -> Self { - let tx = -((right + left) / (right - left)); - let ty = -((top + bottom) / (top - bottom)); - let tz = -((far + near) / (far - near)); - - // I never thought I'd write this as real code - Self([ - // Row one - 2. / (right - left), - 0., - 0., - 0., - // Row two - 0., - 2. / (top - bottom), - 0., - 0., - // Row three - 0., - 0., - -2. / (far - near), - 0., - // Row four - tx, - ty, - tz, - 1., - ]) - } -} - -impl ScreenTransformation { - pub fn into_array(self) -> [f32; 16] { - self.0 - } -} diff --git a/src/pack.rs b/src/pack.rs index f036484e1..2c3a83e6c 100644 --- a/src/pack.rs +++ b/src/pack.rs @@ -12,7 +12,7 @@ //! end up liking what I wrote here. The only downside of using etagere once //! that PR is merged is needing euclid conversions, which is really not a good //! reason to write your own packing algorithm. - +#![allow(clippy::similar_names)] // shelf and self are indeed similar, but I'm not changing the name. use crate::math::{Point, Rect, Size, UPixels}; #[derive(Debug)] @@ -32,6 +32,7 @@ impl TexturePacker { columns: Vec::new(), } } + pub fn allocate(&mut self, area: Size) -> Option { self.allocate_area(Size { width: area.width.0.try_into().expect("area allocated too large"), @@ -188,7 +189,9 @@ impl Column { let shelf = &mut self.shelves[usize::from(*shelf_index)]; if shelf.height < area.height { continue; - } else if shelf.height / 2 > area.height { + } + + if shelf.height / 2 > area.height { // We want to avoid allocating into a shelf when we leave over // 50% of the space empty. break; diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 000000000..62660648e --- /dev/null +++ b/src/pipeline.rs @@ -0,0 +1,346 @@ +use std::mem::size_of; +use std::sync::Arc; + +use bytemuck::{Pod, Zeroable}; + +use crate::buffer::Buffer; +use crate::math::{Dips, Pixels, Point, Ratio, Size, UPixels, Zero}; +use crate::{sealed, Color, RenderingGraphics}; + +#[derive(Pod, Zeroable, Copy, Clone)] +#[repr(C)] +pub(crate) struct Uniforms { + ortho: [f32; 16], + scale: Ratio, + _padding: [u32; 3], +} + +impl Uniforms { + pub fn new(size: Size, scale: f32) -> Self { + Self { + ortho: ScreenTransformation::ortho( + 0., + 0., + size.width.into(), + size.height.into(), + -1.0, + 1.0, + ) + .into_array(), + scale: Ratio::from_f32(scale), + _padding: [0; 3], + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(C)] +pub struct Vertex { + pub location: Point, + pub texture: Point, + pub color: Color, +} + +#[test] +fn vertex_align() { + assert_eq!(std::mem::size_of::>(), 20); +} + +pub(crate) const FLAG_DIPS: u32 = 1 << 0; +pub(crate) const FLAG_SCALE: u32 = 1 << 1; +pub(crate) const FLAG_ROTATE: u32 = 1 << 2; +pub(crate) const FLAG_TRANSLATE: u32 = 1 << 3; +pub(crate) const FLAG_TEXTURED: u32 = 1 << 4; + +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub(crate) struct PushConstants { + pub flags: u32, + pub scale: f32, + pub rotation: f32, + pub translation: Point, +} + +#[derive(Debug)] +pub struct PreparedGraphic { + pub(crate) texture_binding: Option>, + pub(crate) vertices: Buffer>, + pub(crate) indices: Buffer, +} + +impl PreparedGraphic +where + Unit: Default + Into + ShaderScalable + Zero, + Vertex: Pod, +{ + pub fn render<'pass>( + &'pass self, + origin: Point, + scale: Option, + rotation: Option, + graphics: &mut RenderingGraphics<'_, 'pass>, + ) { + graphics.active_pipeline_if_needed(); + + graphics.pass.set_bind_group( + 0, + self.texture_binding + .as_deref() + .unwrap_or(&graphics.state.default_bindings), + &[], + ); + + graphics.pass.set_vertex_buffer(0, self.vertices.as_slice()); + graphics + .pass + .set_index_buffer(self.indices.as_slice(), wgpu::IndexFormat::Uint16); + let mut flags = Unit::flags(); + if self.texture_binding.is_some() { + flags |= FLAG_TEXTURED; + } + let scale = scale.map_or(1., |scale| { + flags |= FLAG_SCALE; + scale + }); + let rotation = rotation.map_or(0., |scale| { + flags |= FLAG_ROTATE; + scale + }); + if !origin.is_zero() { + flags |= FLAG_TRANSLATE; + } + + graphics.pass.set_push_constants( + wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + 0, + bytemuck::bytes_of(&PushConstants { + flags, + scale, + rotation, + translation: Point { + x: origin.x.into(), + y: origin.y.into(), + }, + }), + ); + graphics.pass.draw_indexed( + 0..self + .indices + .len() + .try_into() + .expect("too many drawn verticies"), + 0, + 0..1, + ); + } +} + +pub trait ShaderScalable: sealed::ShaderScalableSealed {} + +impl ShaderScalable for Pixels {} + +impl ShaderScalable for Dips {} + +impl sealed::ShaderScalableSealed for Pixels { + fn flags() -> u32 { + 0 + } +} + +impl sealed::ShaderScalableSealed for Dips { + fn flags() -> u32 { + FLAG_DIPS + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct ScreenTransformation([f32; 16]); + +impl ScreenTransformation { + pub fn ortho(left: f32, top: f32, right: f32, bottom: f32, near: f32, far: f32) -> Self { + let tx = -((right + left) / (right - left)); + let ty = -((top + bottom) / (top - bottom)); + let tz = -((far + near) / (far - near)); + + // I never thought I'd write this as real code + Self([ + // Row one + 2. / (right - left), + 0., + 0., + 0., + // Row two + 0., + 2. / (top - bottom), + 0., + 0., + // Row three + 0., + 0., + -2. / (far - near), + 0., + // Row four + tx, + ty, + tz, + 1., + ]) + } +} + +impl ScreenTransformation { + pub fn into_array(self) -> [f32; 16] { + self.0 + } +} + +pub fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + 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, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX | 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: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }) +} + +pub fn layout( + device: &wgpu::Device, + binding_layout: &wgpu::BindGroupLayout, +) -> wgpu::PipelineLayout { + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[binding_layout], + push_constant_ranges: &[wgpu::PushConstantRange { + stages: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + range: 0..size_of::() + .try_into() + .expect("should fit :)"), + }], + }) +} + +pub(crate) fn bind_group( + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, + uniforms: &wgpu::Buffer, + texture: &wgpu::TextureView, + sampler: &wgpu::Sampler, +) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: uniforms, + offset: 0, + size: None, + }), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(texture), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(sampler), + }, + ], + }) +} + +pub fn new( + device: &wgpu::Device, + pipeline_layout: &wgpu::PipelineLayout, + shader: &wgpu::ShaderModule, + format: wgpu::TextureFormat, +) -> wgpu::RenderPipeline { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(pipeline_layout), + vertex: wgpu::VertexState { + module: shader, + entry_point: "vertex", + buffers: &[wgpu::VertexBufferLayout { + array_stride: size_of::>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + format: wgpu::VertexFormat::Sint32x2, + offset: 0, + shader_location: 0, + }, + wgpu::VertexAttribute { + format: wgpu::VertexFormat::Uint32x2, + offset: 8, + shader_location: 1, + }, + wgpu::VertexAttribute { + format: wgpu::VertexFormat::Uint32, + offset: 16, + shader_location: 2, + }, + ], + }], + }, + fragment: Some(wgpu::FragmentState { + module: shader, + entry_point: "fragment", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) +} diff --git a/src/pod.rs b/src/pod.rs new file mode 100644 index 000000000..116c44fc0 --- /dev/null +++ b/src/pod.rs @@ -0,0 +1,28 @@ +//! Unsafe [`bytemuck::Pod`] implementations. +//! +//! # Safety +//! +//! Bytemuck prevents deriving `Pod` on any type that contains generics, because +//! it can't ensure that the generic types are tagged `repr(c)`. These +//! implementations are all safe because the types being wrapped all are +//! `repr(c)` and only contain u32/f32/i32. +#![allow(unsafe_code)] + +use crate::math::{Dips, Pixels, Point}; +use crate::pipeline::Vertex; + +unsafe impl bytemuck::Pod for Point {} +unsafe impl bytemuck::Zeroable for Point {} +unsafe impl bytemuck::Pod for Point {} +unsafe impl bytemuck::Zeroable for Point {} +unsafe impl bytemuck::Pod for Point {} +unsafe impl bytemuck::Zeroable for Point {} +unsafe impl bytemuck::Pod for Point {} +unsafe impl bytemuck::Zeroable for Point {} + +unsafe impl bytemuck::Pod for Vertex {} +unsafe impl bytemuck::Zeroable for Vertex {} +unsafe impl bytemuck::Pod for Vertex {} +unsafe impl bytemuck::Zeroable for Vertex {} +unsafe impl bytemuck::Pod for Vertex {} +unsafe impl bytemuck::Zeroable for Vertex {} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 000000000..fc1bde5c9 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,256 @@ +use std::collections::hash_map; +use std::hash; +use std::ops::Range; +use std::sync::Arc; + +use ahash::AHashMap; + +use crate::buffer::Buffer; +use crate::math::{Point, Zero}; +use crate::pipeline::{ + PushConstants, ShaderScalable, Vertex, FLAG_ROTATE, FLAG_SCALE, FLAG_TEXTURED, FLAG_TRANSLATE, +}; +use crate::shapes::Shape; +use crate::{sealed, Graphics, RenderingGraphics, Texture, TextureSource}; + +pub struct Renderer<'render, 'gfx> { + graphics: &'render mut Graphics<'gfx>, + data: &'render mut Rendering, +} + +#[derive(Debug)] +struct Command { + indices: Range, + constants: PushConstants, + texture: Option, +} + +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +struct VertexId(Vertex); + +impl hash::Hash for VertexId { + fn hash(&self, state: &mut H) { + bytemuck::bytes_of(&self.0).hash(state); + } +} + +impl Renderer<'_, '_> { + pub fn draw_shape( + &mut self, + shape: &Shape, + origin: Point, + rotation_rads: Option, + scale: Option, + ) where + Unit: Into + Zero + Copy, + Unit: ShaderScalable, + { + self.inner_draw( + shape, + Option::<&Texture>::None, + origin, + rotation_rads, + scale, + ); + } + + pub fn draw_textured_shape( + &mut self, + shape: &Shape, + texture: &impl TextureSource, + origin: Point, + rotation_rads: Option, + scale: Option, + ) where + Unit: Into + Zero + Copy, + Unit: ShaderScalable, + { + self.inner_draw(shape, Some(texture), origin, rotation_rads, scale); + } + + fn inner_draw( + &mut self, + shape: &Shape, + texture: Option<&impl TextureSource>, + origin: Point, + rotation_rads: Option, + scale: Option, + ) where + Unit: Into + Zero + Copy, + Unit: ShaderScalable, + { + // Merge the vertices into the graphics + let mut vertex_map = Vec::with_capacity(shape.vertices.len()); + for vertex in shape.vertices.iter().copied() { + let vertex = Vertex { + location: Point { + x: vertex.location.x.into(), + y: vertex.location.y.into(), + }, + texture: vertex.texture, + color: vertex.color, + }; + let index = *self + .data + .vertex_index_by_id + .entry(VertexId(vertex)) + .or_insert_with(|| { + let index = self + .data + .vertices + .len() + .try_into() + .expect("too many drawn verticies"); + self.data.vertices.push(vertex); + index + }); + vertex_map.push(index); + } + + let first_index_drawn = self.data.indices.len(); + for &vertex_index in &shape.indices { + self.data + .indices + .push(vertex_map[usize::from(vertex_index)]); + } + + let mut flags = Unit::flags(); + assert_eq!(TEXTURED, texture.is_some()); + let texture = if let Some(texture) = texture { + flags |= FLAG_TEXTURED; + let id = texture.id(); + if let hash_map::Entry::Vacant(entry) = self.data.textures.entry(id) { + entry.insert(texture.bind_group(self.graphics)); + } + Some(id) + } else { + None + }; + let scale = scale.map_or(1., |scale| { + flags |= FLAG_SCALE; + scale + }); + let rotation = rotation_rads.map_or(0., |scale| { + flags |= FLAG_ROTATE; + scale + }); + if !origin.is_zero() { + flags |= FLAG_TRANSLATE; + } + + self.data.commands.push(Command { + indices: first_index_drawn + .try_into() + .expect("too many drawn verticies") + ..self + .data + .indices + .len() + .try_into() + .expect("too many drawn verticies"), + constants: PushConstants { + flags, + scale, + rotation, + translation: Point { + x: origin.x.into(), + y: origin.y.into(), + }, + }, + texture, + }); + } +} + +impl Drop for Renderer<'_, '_> { + fn drop(&mut self) { + if self.data.indices.is_empty() { + self.data.buffers = None; + } else { + self.data.buffers = Some(RenderingBuffers { + vertex: Buffer::new( + &self.data.vertices, + wgpu::BufferUsages::VERTEX, + self.graphics.device, + ), + index: Buffer::new( + &self.data.indices, + wgpu::BufferUsages::INDEX, + self.graphics.device, + ), + }); + } + } +} + +#[derive(Default, Debug)] +pub struct Rendering { + buffers: Option, + vertices: Vec>, + vertex_index_by_id: AHashMap, + indices: Vec, + textures: AHashMap>, + commands: Vec, +} + +#[derive(Debug)] +struct RenderingBuffers { + vertex: Buffer>, + index: Buffer, +} + +impl Rendering { + pub fn new_frame<'rendering, 'gfx>( + &'rendering mut self, + graphics: &'rendering mut Graphics<'gfx>, + ) -> Renderer<'rendering, 'gfx> { + self.commands.clear(); + self.indices.clear(); + self.textures.clear(); + self.vertex_index_by_id.clear(); + self.vertices.clear(); + Renderer { + graphics, + data: self, + } + } + + pub fn render<'pass>(&'pass self, graphics: &mut RenderingGraphics<'_, 'pass>) { + if let Some(buffers) = &self.buffers { + let mut current_texture_id = None; + let mut needs_texture_binding = graphics.active_pipeline_if_needed(); + + graphics + .pass + .set_vertex_buffer(0, buffers.vertex.as_slice()); + graphics + .pass + .set_index_buffer(buffers.index.as_slice(), wgpu::IndexFormat::Uint16); + + for command in &self.commands { + if let Some(texture_id) = &command.texture { + if current_texture_id != Some(*texture_id) { + current_texture_id = Some(*texture_id); + graphics.pass.set_bind_group( + 0, + self.textures.get(texture_id).expect("texture missing"), + &[], + ); + } + } else if needs_texture_binding { + needs_texture_binding = false; + graphics + .pass + .set_bind_group(0, &graphics.state.default_bindings, &[]); + } + + graphics.pass.set_push_constants( + wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + 0, + bytemuck::bytes_of(&command.constants), + ); + graphics.pass.draw_indexed(command.indices.clone(), 0, 0..1); + } + } + } +} diff --git a/src/shapes.wgsl b/src/shader.wgsl similarity index 83% rename from src/shapes.wgsl rename to src/shader.wgsl index af04f7983..e02a6782f 100644 --- a/src/shapes.wgsl +++ b/src/shader.wgsl @@ -16,8 +16,8 @@ struct VertexInput { struct VertexOutput { @builtin(position) position: vec4, - @location(0) uv: vec2, - @location(1) color: u32, + @location(0) color: vec4, + @location(1) uv: vec2, } struct Uniforms { @@ -52,8 +52,17 @@ fn dips_to_pixels(value: i32, ratio: Ratio) -> i32 { return int_scale(value, ratio) * i32(96) / i32(2540); } +fn int_to_rgba(color: u32) -> vec4 { + let r = color >> u32(24); + let g = (color >> u32(16)) & u32(0xFF); + let b = (color >> u32(8)) & u32(0xFF); + let a = color & u32(0xFF); + + return vec4(f32(r) / 255.0, f32(g) / 255.0, f32(b) / 255.0, f32(a) / 255.0); +} + @vertex -fn vs_main(input: VertexInput) -> VertexOutput { +fn vertex(input: VertexInput) -> VertexOutput { let flag_dips = u32(1); let flag_scale = flag_dips << u32(1); let flag_rotation = flag_dips << u32(2); @@ -95,14 +104,14 @@ fn vs_main(input: VertexInput) -> VertexOutput { } } outval.position = uniforms.ortho * vec4(position, 0., 1.0); - outval.color = input.color; + outval.color = int_to_rgba(input.color); outval.uv = vec2(input.uv) / vec2(textureDimensions(r_texture)); return outval; } struct FragmentInput { - @location(0) uv: vec2, - @location(1) color: u32, + @location(0) color: vec4, + @location(1) uv: vec2, } @group(0) @@ -113,17 +122,12 @@ var r_texture: texture_2d; var r_sampler: sampler; @fragment -fn fs_main(fragment: FragmentInput) -> @location(0) vec4 { +fn fragment(fragment: FragmentInput) -> @location(0) vec4 { let flag_textured = u32(1) << u32(4); if (pc.flags & flag_textured) != u32(0) { - return textureSample(r_texture, r_sampler, fragment.uv); + return textureSample(r_texture, r_sampler, fragment.uv) * fragment.color; } - let r = fragment.color >> u32(24); - let g = (fragment.color >> u32(16)) & u32(0xFF); - let b = (fragment.color >> u32(8)) & u32(0xFF); - let a = fragment.color & u32(0xFF); - - return vec4(f32(r) / 255.0, f32(g) / 255.0, f32(b) / 255.0, f32(a) / 255.0); + return fragment.color; } \ No newline at end of file diff --git a/src/shapes.rs b/src/shapes.rs index d9f260e32..2ca450495 100644 --- a/src/shapes.rs +++ b/src/shapes.rs @@ -1,18 +1,16 @@ -use std::marker::PhantomData; use std::ops::Add; -use std::sync::Arc; -use bytemuck::{Pod, Zeroable}; use lyon_tessellation::{ FillGeometryBuilder, FillOptions, FillTessellator, FillVertex, FillVertexConstructor, GeometryBuilder, GeometryBuilderError, StrokeGeometryBuilder, StrokeVertex, StrokeVertexConstructor, VertexId, }; -use wgpu::{BufferUsages, ShaderStages}; +use wgpu::BufferUsages; use crate::buffer::Buffer; -use crate::math::{Dips, Pixels, Point, Rect, ToFloat, UPixels, Zero}; -use crate::{sealed, Color, Graphics, RenderingGraphics, TextureSource}; +use crate::math::{Point, Rect, ToFloat, UPixels}; +use crate::pipeline::Vertex; +use crate::{Color, Graphics, PreparedGraphic, TextureSource}; #[derive(Debug, Clone, PartialEq)] pub struct Shape { @@ -30,6 +28,7 @@ impl Shape { } impl Shape { + #[must_use] pub fn prepare(&self, graphics: &Graphics<'_>) -> PreparedGraphic where Vertex: bytemuck::Pod, @@ -48,7 +47,6 @@ impl Shape { vertices, indices, texture_binding: None, - _unit: PhantomData, } } } @@ -76,113 +74,10 @@ impl Shape { vertices, indices, texture_binding: Some(texture.bind_group(graphics)), - _unit: PhantomData, } } } -pub(crate) const FLAG_DIPS: u32 = 1 << 0; -pub(crate) const FLAG_SCALE: u32 = 1 << 1; -pub(crate) const FLAG_ROTATE: u32 = 1 << 2; -pub(crate) const FLAG_TRANSLATE: u32 = 1 << 3; -pub(crate) const FLAG_TEXTURED: u32 = 1 << 4; - -#[derive(Debug, Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub(crate) struct PushConstants { - pub flags: u32, - pub scale: f32, - pub rotation: f32, - pub translation: Point, -} - -#[derive(Debug)] -pub struct PreparedGraphic { - texture_binding: Option>, - vertices: Buffer>, - indices: Buffer, - _unit: PhantomData, -} - -impl PreparedGraphic -where - Unit: Default + Into + ShaderScalable + Zero, - Vertex: Pod, -{ - pub fn render<'pass>( - &'pass self, - origin: Point, - scale: Option, - rotation: Option, - graphics: &mut RenderingGraphics<'_, 'pass>, - ) { - graphics.active_pipeline_if_needed(); - - graphics.pass.set_bind_group( - 0, - self.texture_binding - .as_deref() - .unwrap_or(&graphics.state.default_bindings), - &[], - ); - - graphics.pass.set_vertex_buffer(0, self.vertices.as_slice()); - graphics - .pass - .set_index_buffer(self.indices.as_slice(), wgpu::IndexFormat::Uint16); - let mut flags = Unit::flags(); - if self.texture_binding.is_some() { - flags |= FLAG_TEXTURED; - } - let scale = scale.map_or(1., |scale| { - flags |= FLAG_SCALE; - scale - }); - let rotation = rotation.map_or(0., |scale| { - flags |= FLAG_ROTATE; - scale - }); - if !origin.is_zero() { - flags |= FLAG_TRANSLATE; - } - - graphics.pass.set_push_constants( - ShaderStages::VERTEX | ShaderStages::FRAGMENT, - 0, - bytemuck::bytes_of(&PushConstants { - flags, - scale, - rotation, - translation: Point { - x: origin.x.into(), - y: origin.y.into(), - }, - }), - ); - graphics - .pass - .draw_indexed(0..self.indices.len() as u32, 0, 0..1); - } -} - -pub trait ShaderScalable: sealed::ShaderScalableSealed {} - -impl ShaderScalable for Pixels {} - -impl ShaderScalable for Dips {} - -impl sealed::ShaderScalableSealed for Pixels { - fn flags() -> u32 { - 0 - } -} - -impl sealed::ShaderScalableSealed for Dips { - fn flags() -> u32 { - FLAG_DIPS - } -} - struct ShapeBuilder { shape: Shape, default_color: Color, @@ -222,7 +117,13 @@ where attributes: &[f32], ) -> Result { let vertex = self.new_vertex(position, attributes); - let new_id = VertexId(self.shape.vertices.len() as u32); + let new_id = VertexId( + self.shape + .vertices + .len() + .try_into() + .map_err(|_| GeometryBuilderError::TooManyVertices)?, + ); self.shape.vertices.push(vertex); if self.shape.vertices.len() > u16::MAX as usize { return Err(GeometryBuilderError::TooManyVertices); @@ -308,9 +209,15 @@ where fn end_geometry(&mut self) {} fn add_triangle(&mut self, a: VertexId, b: VertexId, c: VertexId) { - self.shape.indices.push(a.0 as u16); - self.shape.indices.push(b.0 as u16); - self.shape.indices.push(c.0 as u16); + self.shape + .indices + .push(a.0.try_into().expect("checked in new_vertex")); + self.shape + .indices + .push(b.0.try_into().expect("checked in new_vertex")); + self.shape + .indices + .push(c.0.try_into().expect("checked in new_vertex")); } fn abort_geometry(&mut self) { @@ -319,26 +226,6 @@ where } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[repr(C)] -pub struct Vertex { - pub location: Point, - pub texture: Point, - pub color: Color, -} - -#[test] -fn vertex_align() { - assert_eq!(std::mem::size_of::>(), 20); -} - -unsafe impl bytemuck::Pod for Vertex {} -unsafe impl bytemuck::Zeroable for Vertex {} -unsafe impl bytemuck::Pod for Vertex {} -unsafe impl bytemuck::Zeroable for Vertex {} -unsafe impl bytemuck::Pod for Vertex {} -unsafe impl bytemuck::Zeroable for Vertex {} - /// A point on a [`Path`]. pub type Endpoint = Point; /// A control point used to create curves. @@ -439,6 +326,7 @@ where builder.build() } + #[must_use] pub fn fill(&self, color: Color) -> Shape { let lyon_path = self.as_lyon(); let mut shape_builder = ShapeBuilder::new(color);