diff --git a/Cargo.toml b/Cargo.toml index 3de8844b..e1f40e96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ debug = true [dependencies] fnv = "1.0.7" rgb = "0.8.50" +bytemuck = { version = "1.16", features = ["derive"] } imgref = "1.11.0" bitflags = "2.6.0" rustybuzz = "0.20.0" @@ -31,6 +32,7 @@ image = { version = "0.25.0", optional = true, default-features = false } serde = { version = "1.0", optional = true, features = ["derive", "rc"] } glow = { version = "0.15.0", default-features = false } log = "0.4" +wgpu = { version = "23", optional = true, default-features = false, features = ["wgsl"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] glutin = { version = "0.31", optional = true, default-features = false } @@ -44,12 +46,14 @@ web_sys = { version = "0.3", package = "web-sys", features = [ wasm-bindgen = "0.2" [features] -default = ["image-loading"] +default = ["image-loading", "wgpu"] +#default = ["image-loading"] image-loading = ["image"] debug_inspector = [] +wgpu = ["dep:wgpu"] [dev-dependencies] -winit = { version = "0.29", default-features = false } +winit = { version = "0.29.1" } euclid = "0.22.3" rand = "0.8" svg = "0.14.0" @@ -63,6 +67,8 @@ image = { version = "0.25.0", default-features = false, features = [ cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "e00109d77f06d5a2e3057865eda3f530bc40a046" } swash = "=0.1.17" # keep this in sync with cosmic-text lazy_static = "1.4.0" +spin_on = "0.1" +wgpu = { version = "23" } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] glutin = "0.31.0" @@ -78,6 +84,7 @@ console_error_panic_hook = "0.1.5" instant = { version = "0.1", features = ["wasm-bindgen", "now"] } resource = { version = "0.5.0", features = ["force-static"] } getrandom = { version = "0.2.2", features = ["js"] } +wgpu = { version = "23", features = ["webgl"] } [[example]] name = "book_example_1_1" diff --git a/book/src/1_getting_started/2_rendering.rs b/book/src/1_getting_started/2_rendering.rs index 1f624cf2..f43efc65 100644 --- a/book/src/1_getting_started/2_rendering.rs +++ b/book/src/1_getting_started/2_rendering.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU32; use femtovg::renderer::OpenGl; -use femtovg::{Canvas, Color, Renderer}; +use femtovg::{Canvas, Color}; use glutin::surface::Surface; use glutin::{context::PossiblyCurrentContext, display::Display}; use glutin_winit::DisplayBuilder; @@ -71,11 +71,11 @@ fn create_window(event_loop: &EventLoop<()>) -> (PossiblyCurrentContext, Display ) } -fn render( +fn render( context: &PossiblyCurrentContext, surface: &Surface, window: &Window, - canvas: &mut Canvas, + canvas: &mut Canvas, ) { // Make sure the canvas has the right size: let size = window.inner_size(); @@ -85,7 +85,7 @@ fn render( canvas.clear_rect(30, 30, 30, 30, Color::rgbf(1., 0., 0.)); // Tell renderer to execute all drawing commands - canvas.flush(); + canvas.flush_to_surface(&()); // Display what we've just rendered surface.swap_buffers(context).expect("Could not swap buffers"); diff --git a/book/src/1_getting_started/3_event_loop.rs b/book/src/1_getting_started/3_event_loop.rs index e1d41e3a..d8078cb5 100644 --- a/book/src/1_getting_started/3_event_loop.rs +++ b/book/src/1_getting_started/3_event_loop.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU32; use femtovg::renderer::OpenGl; -use femtovg::{Canvas, Color, Renderer}; +use femtovg::{Canvas, Color}; use glutin::surface::Surface; use glutin::{context::PossiblyCurrentContext, display::Display}; use glutin_winit::DisplayBuilder; @@ -88,11 +88,11 @@ fn create_window(event_loop: &EventLoop<()>) -> (PossiblyCurrentContext, Display ) } -fn render( +fn render( context: &PossiblyCurrentContext, surface: &Surface, window: &Window, - canvas: &mut Canvas, + canvas: &mut Canvas, square_position: PhysicalPosition, ) { // Make sure the canvas has the right size: @@ -111,7 +111,7 @@ fn render( ); // Tell renderer to execute all drawing commands - canvas.flush(); + canvas.flush_to_surface(&()); // Display what we've just rendered surface.swap_buffers(context).expect("Could not swap buffers"); } diff --git a/examples/breakout.rs b/examples/breakout.rs index d29e6631..79f19614 100644 --- a/examples/breakout.rs +++ b/examples/breakout.rs @@ -1,4 +1,6 @@ -use femtovg::{renderer::OpenGl, Align, Baseline, Color, FontId, ImageFlags, ImageId, Paint, Path}; +use std::sync::Arc; + +use femtovg::{Align, Baseline, Canvas, Color, FontId, ImageFlags, ImageId, Paint, Path, Renderer}; use instant::Instant; use rand::{ distributions::{Distribution, Standard}, @@ -12,6 +14,7 @@ use winit::{ }; mod helpers; +use helpers::WindowSurface; fn main() { #[cfg(not(target_arch = "wasm32"))] @@ -20,10 +23,6 @@ fn main() { helpers::start(); } -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; - -type Canvas = femtovg::Canvas; type Point = euclid::default::Point2D; type Vector = euclid::default::Vector2D; type Size = euclid::default::Size2D; @@ -93,7 +92,7 @@ struct Game { } impl Game { - fn new(canvas: &mut Canvas, levels: Vec>>) -> Self { + fn new(canvas: &mut Canvas, levels: Vec>>) -> Self { let logo_image_id = canvas .load_image_mem( &resource!("examples/assets/rust-logo.png"), @@ -497,7 +496,7 @@ impl Game { } } - fn draw(&self, canvas: &mut Canvas) { + fn draw(&self, canvas: &mut Canvas) { // draw background let step_size_x = self.size.width / 50.0; @@ -528,7 +527,7 @@ impl Game { } } - fn draw_title_screen(&self, canvas: &mut Canvas) { + fn draw_title_screen(&self, canvas: &mut Canvas) { // curtain let mut path = Path::new(); path.rect(0.0, 0.0, self.size.width, self.size.height); @@ -564,7 +563,7 @@ impl Game { let _ = canvas.fill_text(self.size.width / 2.0, (self.size.height / 2.0) + 40.0, text, &paint); } - fn draw_game(&self, canvas: &mut Canvas) { + fn draw_game(&self, canvas: &mut Canvas) { // Paddle let side_size = 15.0; @@ -666,29 +665,29 @@ impl Game { let _ = canvas.fill_text(20.0, 25.0, format!("Score: {}", self.score), &paint); } - fn draw_round_info(&self, canvas: &mut Canvas) { + fn draw_round_info(&self, canvas: &mut Canvas) { let heading = format!("ROUND {}", self.current_level + 1); self.draw_generic_info(canvas, &heading, ""); } - fn draw_paused(&self, canvas: &mut Canvas) { + fn draw_paused(&self, canvas: &mut Canvas) { self.draw_generic_info(canvas, "PAUSE", "Click anywhere to resume. Press ESC to exit"); } - fn draw_game_over(&self, canvas: &mut Canvas) { + fn draw_game_over(&self, canvas: &mut Canvas) { let score = format!("Score: {}", self.score); self.draw_generic_info(canvas, "Game Over", &score); } - fn draw_win(&self, canvas: &mut Canvas) { + fn draw_win(&self, canvas: &mut Canvas) { let score = format!("Final score: {}", self.score); self.draw_generic_info(canvas, "All cleared!", &score); } - fn draw_bricks(&self, canvas: &mut Canvas) { + fn draw_bricks(&self, canvas: &mut Canvas) { // Bricks for brick in &self.bricks { if brick.destroyed { @@ -699,7 +698,7 @@ impl Game { } } - fn draw_generic_info(&self, canvas: &mut Canvas, heading: &str, subtext: &str) { + fn draw_generic_info(&self, canvas: &mut Canvas, heading: &str, subtext: &str) { self.draw_bricks(canvas); // curtain @@ -775,7 +774,7 @@ struct Powerup { } impl Powerup { - fn draw(&self, canvas: &mut Canvas, fonts: &Fonts) { + fn draw(&self, canvas: &mut Canvas, fonts: &Fonts) { let mut path = Path::new(); path.rounded_rect( self.rect.origin.x, @@ -847,7 +846,7 @@ impl Brick { } } - fn draw(&self, canvas: &mut Canvas) { + fn draw(&self, canvas: &mut Canvas) { if self.destroyed { return; } @@ -897,13 +896,7 @@ enum Cmd { B(u8), // Brick Id } -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { let level1 = vec![ vec![ Cmd::Spac, @@ -1278,11 +1271,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { WindowEvent::Resized(physical_size) => { #[cfg(not(target_arch = "wasm32"))] - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); game.size = Size::new(physical_size.width as f32, physical_size.height as f32); } WindowEvent::CloseRequested => event_loop_window_target.exit(), @@ -1299,9 +1288,7 @@ fn run( game.update(dt); game.draw(&mut canvas); - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); } _ => (), }, diff --git a/examples/demo.rs b/examples/demo.rs index ed4af6dc..e911f696 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -1,4 +1,4 @@ -use std::f32::consts::PI; +use std::{f32::consts::PI, sync::Arc}; use femtovg::{ Align, Baseline, Canvas, Color, FillRule, FontId, ImageFlags, ImageId, LineCap, LineJoin, Paint, Path, Renderer, @@ -15,6 +15,7 @@ use winit::{ mod helpers; use helpers::PerfGraph; +use helpers::WindowSurface; fn main() { #[cfg(not(target_arch = "wasm32"))] @@ -23,9 +24,6 @@ fn main() { helpers::start(); } -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; - pub fn quantize(a: f32, d: f32) -> f32 { (a / d + 0.5).trunc() * d } @@ -36,13 +34,7 @@ struct Fonts { icons: FontId, } -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { let fonts = Fonts { regular: canvas .add_font_mem(&resource!("examples/assets/Roboto-Regular.ttf")) @@ -113,11 +105,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { #[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(physical_size) => { - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); } WindowEvent::CursorMoved { device_id: _, position, .. @@ -184,7 +172,6 @@ fn run( canvas.set_size(size.width, size.height, dpi_factor as f32); canvas.clear_rect(0, 0, size.width, size.height, Color::rgbf(0.3, 0.3, 0.32)); - let height = size.height as f32; let width = size.width as f32; @@ -192,6 +179,33 @@ fn run( let rel_mousex = pt.0; let rel_mousey = pt.1; + //canvas.clear_rect(10, 10, 40, 40, Color::rgb(255, 0, 0)); + //canvas.clear_rect(150, 50, 40, 40, Color::rgb(0, 255, 0)); + + /* + { + let mut p = femtovg::Path::new(); + p.rect(10., 10., 40., 40.); + let paint = femtovg::Paint::color(femtovg::Color::rgb(0, 255, 0)); + canvas.fill_path(&p, &paint); + } + + { + let mut p = femtovg::Path::new(); + p.rounded_rect(150., 50., 40., 40., 20.); + let paint = femtovg::Paint::color(femtovg::Color::rgb(0, 0, 255)); + // canvas.fill_path(&p, &paint); + canvas.stroke_path(&p, &paint); + } + */ + /* + { + let mut p = femtovg::Path::new(); + p.rect(200., 50., 140., 140.); + let paint = femtovg::Paint::image(images[1], 200., 50., 140., 140., 0., 1.0); + canvas.fill_path(&p, &paint); + } */ + draw_graph(&mut canvas, 0.0, height / 2.0, width, height / 2.0, t); draw_eyes( @@ -305,9 +319,7 @@ fn run( perf.render(canvas, 5.0, 5.0); }); - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); } WindowEvent::CloseRequested => event_loop_window_target.exit(), _ => (), diff --git a/examples/external_text.rs b/examples/external_text.rs index a4b82361..9d9e71a2 100644 --- a/examples/external_text.rs +++ b/examples/external_text.rs @@ -2,18 +2,17 @@ mod helpers; use cosmic_text::{Attrs, Buffer, CacheKey, FontSystem, Metrics, SubpixelBin}; use femtovg::{ - renderer::OpenGl, Atlas, Canvas, Color, DrawCommand, ErrorKind, GlyphDrawCommands, ImageFlags, ImageId, - ImageSource, Paint, Quad, Renderer, + Atlas, Canvas, Color, DrawCommand, ErrorKind, GlyphDrawCommands, ImageFlags, ImageId, ImageSource, Paint, Quad, + Renderer, }; -use std::collections::HashMap; +use helpers::WindowSurface; +use std::{collections::HashMap, sync::Arc}; use winit::{ event::{Event, WindowEvent}, event_loop::EventLoop, window::Window, }; -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; use imgref::{Img, ImgRef}; use rgb::RGBA8; use swash::scale::image::Content; @@ -224,13 +223,7 @@ lazy_static::lazy_static! { static ref FONT_SYSTEM: FontSystem = FontSystem::new(); } -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { let mut buffer = Buffer::new(&FONT_SYSTEM, Metrics::new(20, 25)); let mut cache = RenderCache::default(); buffer.set_text(LOREM_TEXT, Attrs::new()); @@ -241,11 +234,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { #[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(physical_size) => { - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); } WindowEvent::CloseRequested => event_loop_window_target.exit(), WindowEvent::RedrawRequested { .. } => { @@ -261,9 +250,7 @@ fn run( .unwrap(); canvas.draw_glyph_commands(cmds, &Paint::color(Color::black()), 1.0); - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); } _ => (), }, diff --git a/examples/gradients.rs b/examples/gradients.rs index 4badf115..38d9d074 100644 --- a/examples/gradients.rs +++ b/examples/gradients.rs @@ -1,4 +1,6 @@ -use femtovg::{renderer::OpenGl, Canvas, Color, Paint, Path, Renderer}; +use std::sync::Arc; + +use femtovg::{Canvas, Color, Paint, Path, Renderer}; use instant::Instant; use resource::resource; use winit::{ @@ -8,7 +10,7 @@ use winit::{ }; mod helpers; -use helpers::PerfGraph; +use helpers::{PerfGraph, WindowSurface}; fn main() { #[cfg(not(target_arch = "wasm32"))] @@ -17,19 +19,10 @@ fn main() { helpers::start(); } -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; - #[cfg(target_arch = "wasm32")] use winit::window::Window; -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { canvas .add_font_mem(&resource!("examples/assets/Roboto-Regular.ttf")) .expect("Cannot add font"); @@ -47,11 +40,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { #[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(physical_size) => { - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); } WindowEvent::CloseRequested => event_loop_window_target.exit(), WindowEvent::RedrawRequested { .. } => { @@ -73,9 +62,7 @@ fn run( perf.render(&mut canvas, 5.0, 5.0); canvas.restore(); - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); } _ => (), }, diff --git a/examples/helpers/mod.rs b/examples/helpers/mod.rs index 4199b03b..e089a75e 100644 --- a/examples/helpers/mod.rs +++ b/examples/helpers/mod.rs @@ -1,144 +1,20 @@ -#[cfg(not(target_arch = "wasm32"))] -use std::num::NonZeroU32; - use super::run; -use femtovg::{renderer::OpenGl, Canvas}; -#[cfg(not(target_arch = "wasm32"))] -use glutin::{ - config::ConfigTemplateBuilder, - context::{ContextApi, ContextAttributesBuilder}, - display::GetGlDisplay, - prelude::*, - surface::{SurfaceAttributesBuilder, WindowSurface}, -}; -#[cfg(not(target_arch = "wasm32"))] -use glutin_winit::DisplayBuilder; -#[cfg(not(target_arch = "wasm32"))] -use raw_window_handle::HasRawWindowHandle; -use winit::{event_loop::EventLoop, window::WindowBuilder}; - mod perf_graph; pub use perf_graph::PerfGraph; -pub fn start( - #[cfg(not(target_arch = "wasm32"))] width: u32, - #[cfg(not(target_arch = "wasm32"))] height: u32, - #[cfg(not(target_arch = "wasm32"))] title: &'static str, - #[cfg(not(target_arch = "wasm32"))] resizeable: bool, -) { - // This provides better error messages in debug mode. - // It's disabled in release mode so it doesn't bloat up the file size. - #[cfg(all(debug_assertions, target_arch = "wasm32"))] - console_error_panic_hook::set_once(); - - let event_loop = EventLoop::new().unwrap(); - - #[cfg(not(target_arch = "wasm32"))] - let (canvas, window, context, surface) = { - let window_builder = WindowBuilder::new() - .with_inner_size(winit::dpi::PhysicalSize::new(width, height)) - .with_resizable(resizeable) - .with_title(title); - - let template = ConfigTemplateBuilder::new().with_alpha_size(8); - - let display_builder = DisplayBuilder::new().with_window_builder(Some(window_builder)); - - let (window, gl_config) = display_builder - .build(&event_loop, template, |configs| { - // Find the config with the maximum number of samples, so our triangle will - // be smooth. - configs - .reduce(|accum, config| { - let transparency_check = config.supports_transparency().unwrap_or(false) - & !accum.supports_transparency().unwrap_or(false); - - if transparency_check || config.num_samples() < accum.num_samples() { - config - } else { - accum - } - }) - .unwrap() - }) - .unwrap(); - - let window = window.unwrap(); - - let raw_window_handle = Some(window.raw_window_handle()); - - let gl_display = gl_config.display(); - - let context_attributes = ContextAttributesBuilder::new().build(raw_window_handle); - let fallback_context_attributes = ContextAttributesBuilder::new() - .with_context_api(ContextApi::Gles(None)) - .build(raw_window_handle); - let mut not_current_gl_context = Some(unsafe { - gl_display - .create_context(&gl_config, &context_attributes) - .unwrap_or_else(|_| { - gl_display - .create_context(&gl_config, &fallback_context_attributes) - .expect("failed to create context") - }) - }); - - let (width, height): (u32, u32) = window.inner_size().into(); - let raw_window_handle = window.raw_window_handle(); - let attrs = SurfaceAttributesBuilder::::new().build( - raw_window_handle, - NonZeroU32::new(width).unwrap(), - NonZeroU32::new(height).unwrap(), - ); - - let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() }; - - let gl_context = not_current_gl_context.take().unwrap().make_current(&surface).unwrap(); - - let renderer = unsafe { OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s).cast()) } - .expect("Cannot create renderer"); - - let mut canvas = Canvas::new(renderer).expect("Cannot create canvas"); - canvas.set_size(width, height, window.scale_factor() as f32); - - (canvas, window, gl_context, surface) - }; - - #[cfg(target_arch = "wasm32")] - let (canvas, window) = { - use wasm_bindgen::JsCast; - - let canvas = web_sys::window() - .unwrap() - .document() - .unwrap() - .get_element_by_id("canvas") - .unwrap() - .dyn_into::() - .unwrap(); - - use winit::platform::web::WindowBuilderExtWebSys; - - let renderer = OpenGl::new_from_html_canvas(&canvas).expect("Cannot create renderer"); - - let window = WindowBuilder::new() - .with_canvas(Some(canvas)) - .build(&event_loop) - .unwrap(); - - let canvas = Canvas::new(renderer).expect("Cannot create canvas"); +pub trait WindowSurface { + type Renderer: femtovg::Renderer + 'static; + fn resize(&mut self, width: u32, height: u32); + fn present(&self, canvas: &mut femtovg::Canvas); +} - (canvas, window) - }; +#[cfg(not(feature = "wgpu"))] +mod opengl; +#[cfg(not(feature = "wgpu"))] +pub use opengl::start_opengl as start; - run( - canvas, - event_loop, - #[cfg(not(target_arch = "wasm32"))] - context, - #[cfg(not(target_arch = "wasm32"))] - surface, - window, - ); -} +#[cfg(feature = "wgpu")] +mod wgpu; +#[cfg(feature = "wgpu")] +pub use wgpu::start_wgpu as start; diff --git a/examples/helpers/opengl.rs b/examples/helpers/opengl.rs new file mode 100644 index 00000000..b11a3bbb --- /dev/null +++ b/examples/helpers/opengl.rs @@ -0,0 +1,167 @@ +#[cfg(not(target_arch = "wasm32"))] +use std::num::NonZeroU32; +use std::sync::Arc; + +use super::{run, WindowSurface}; + +use femtovg::{renderer::OpenGl, Canvas}; +#[cfg(not(target_arch = "wasm32"))] +use glutin::{ + config::ConfigTemplateBuilder, + context::{ContextApi, ContextAttributesBuilder}, + display::GetGlDisplay, + prelude::*, + surface::SurfaceAttributesBuilder, +}; +#[cfg(not(target_arch = "wasm32"))] +use glutin_winit::DisplayBuilder; +#[cfg(not(target_arch = "wasm32"))] +use raw_window_handle::HasRawWindowHandle; +use winit::{event_loop::EventLoop, window::WindowBuilder}; + +pub struct DemoSurface { + #[cfg(not(target_arch = "wasm32"))] + context: glutin::context::PossiblyCurrentContext, + #[cfg(not(target_arch = "wasm32"))] + surface: glutin::surface::Surface, +} + +impl WindowSurface for DemoSurface { + type Renderer = OpenGl; + + fn resize(&mut self, width: u32, height: u32) { + #[cfg(not(target_arch = "wasm32"))] + { + self.surface + .resize(&self.context, width.try_into().unwrap(), height.try_into().unwrap()); + } + } + fn present(&self, canvas: &mut femtovg::Canvas) { + canvas.flush_to_surface(&()); + #[cfg(not(target_arch = "wasm32"))] + { + self.surface.swap_buffers(&self.context).unwrap(); + } + } +} + +pub fn start_opengl( + #[cfg(not(target_arch = "wasm32"))] width: u32, + #[cfg(not(target_arch = "wasm32"))] height: u32, + #[cfg(not(target_arch = "wasm32"))] title: &'static str, + #[cfg(not(target_arch = "wasm32"))] resizeable: bool, +) { + // This provides better error messages in debug mode. + // It's disabled in release mode so it doesn't bloat up the file size. + #[cfg(all(debug_assertions, target_arch = "wasm32"))] + console_error_panic_hook::set_once(); + + let event_loop = EventLoop::new().unwrap(); + + #[cfg(not(target_arch = "wasm32"))] + let (canvas, window, context, surface) = { + let window_builder = WindowBuilder::new() + .with_inner_size(winit::dpi::PhysicalSize::new(width, height)) + .with_resizable(resizeable) + .with_title(title); + + let template = ConfigTemplateBuilder::new().with_alpha_size(8); + + let display_builder = DisplayBuilder::new().with_window_builder(Some(window_builder)); + + let (window, gl_config) = display_builder + .build(&event_loop, template, |configs| { + // Find the config with the maximum number of samples, so our triangle will + // be smooth. + configs + .reduce(|accum, config| { + let transparency_check = config.supports_transparency().unwrap_or(false) + & !accum.supports_transparency().unwrap_or(false); + + if transparency_check || config.num_samples() < accum.num_samples() { + config + } else { + accum + } + }) + .unwrap() + }) + .unwrap(); + + let window = window.unwrap(); + + let raw_window_handle = Some(window.raw_window_handle()); + + let gl_display = gl_config.display(); + + let context_attributes = ContextAttributesBuilder::new().build(raw_window_handle); + let fallback_context_attributes = ContextAttributesBuilder::new() + .with_context_api(ContextApi::Gles(None)) + .build(raw_window_handle); + let mut not_current_gl_context = Some(unsafe { + gl_display + .create_context(&gl_config, &context_attributes) + .unwrap_or_else(|_| { + gl_display + .create_context(&gl_config, &fallback_context_attributes) + .expect("failed to create context") + }) + }); + + let (width, height): (u32, u32) = window.inner_size().into(); + let raw_window_handle = window.raw_window_handle(); + let attrs = SurfaceAttributesBuilder::::new().build( + raw_window_handle, + NonZeroU32::new(width).unwrap(), + NonZeroU32::new(height).unwrap(), + ); + + let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() }; + + let gl_context = not_current_gl_context.take().unwrap().make_current(&surface).unwrap(); + + let renderer = unsafe { OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s).cast()) } + .expect("Cannot create renderer"); + + let mut canvas = Canvas::new(renderer).expect("Cannot create canvas"); + canvas.set_size(width, height, window.scale_factor() as f32); + + (canvas, window, gl_context, surface) + }; + + #[cfg(target_arch = "wasm32")] + let (canvas, window) = { + use wasm_bindgen::JsCast; + + let canvas = web_sys::window() + .unwrap() + .document() + .unwrap() + .get_element_by_id("canvas") + .unwrap() + .dyn_into::() + .unwrap(); + + use winit::platform::web::WindowBuilderExtWebSys; + + let renderer = OpenGl::new_from_html_canvas(&canvas).expect("Cannot create renderer"); + + let window = WindowBuilder::new() + .with_canvas(Some(canvas)) + .build(&event_loop) + .unwrap(); + + let canvas = Canvas::new(renderer).expect("Cannot create canvas"); + + (canvas, window) + }; + + let demo_surface = DemoSurface { + #[cfg(not(target_arch = "wasm32"))] + context, + #[cfg(not(target_arch = "wasm32"))] + surface, + }; + + run(canvas, event_loop, demo_surface, Arc::new(window)); +} diff --git a/examples/helpers/wgpu.rs b/examples/helpers/wgpu.rs new file mode 100644 index 00000000..e7f5f909 --- /dev/null +++ b/examples/helpers/wgpu.rs @@ -0,0 +1,114 @@ +use std::sync::Arc; + +use femtovg::{renderer::WGPURenderer, Canvas}; +use winit::{event_loop::EventLoop, window::WindowBuilder}; + +use super::{run, WindowSurface}; + +pub struct DemoSurface { + device: Arc, + surface_config: wgpu::SurfaceConfiguration, + surface: wgpu::Surface<'static>, +} + +impl WindowSurface for DemoSurface { + type Renderer = femtovg::renderer::WGPURenderer; + + fn resize(&mut self, width: u32, height: u32) { + self.surface_config.width = width.max(1); + self.surface_config.height = height.max(1); + self.surface.configure(&self.device, &self.surface_config); + } + + fn present(&self, canvas: &mut femtovg::Canvas) { + let frame = self + .surface + .get_current_texture() + .expect("unable to get next texture from swapchain"); + + canvas.flush_to_surface(&frame.texture); + + frame.present(); + } +} + +pub fn start_wgpu( + #[cfg(not(target_arch = "wasm32"))] width: u32, + #[cfg(not(target_arch = "wasm32"))] height: u32, + #[cfg(not(target_arch = "wasm32"))] title: &'static str, + #[cfg(not(target_arch = "wasm32"))] resizeable: bool, +) { + let event_loop = EventLoop::new().unwrap(); + + let window_builder = WindowBuilder::new() + .with_inner_size(winit::dpi::PhysicalSize::new(width, height)) + .with_resizable(resizeable) + .with_title(title); + + let window = Arc::new(window_builder.build(&event_loop).unwrap()); + + let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); + let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); + let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); + + let instance = spin_on::spin_on(async { + wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor { + backends, + flags: wgpu::InstanceFlags::from_build_config().with_env(), + dx12_shader_compiler, + gles_minor_version, + }) + .await + }); + + let surface = instance.create_surface(window.clone()).unwrap(); + let adapter = spin_on::spin_on(async { + wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface)) + .await + .expect("Failed to find an appropriate adapter") + }); + + // Create the logical device and command queue + let (device, queue) = spin_on::spin_on(async { + adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + required_features: wgpu::Features::empty(), + // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain. + required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()), + memory_hints: wgpu::MemoryHints::MemoryUsage, + }, + None, + ) + .await + .expect("Failed to create device") + }); + + let mut surface_config = surface.get_default_config(&adapter, width, height).unwrap(); + + let swapchain_capabilities = surface.get_capabilities(&adapter); + let swapchain_format = swapchain_capabilities + .formats + .iter() + .find(|f| !f.is_srgb()) + .copied() + .unwrap_or_else(|| swapchain_capabilities.formats[0]); + surface_config.format = swapchain_format; + surface.configure(&device, &surface_config); + + let device = Arc::new(device); + + let demo_surface = DemoSurface { + device: device.clone(), + surface_config, + surface, + }; + + let renderer = WGPURenderer::new(device, Arc::new(queue)); + + let mut canvas = Canvas::new(renderer).expect("Cannot create canvas"); + canvas.set_size(width, height, window.scale_factor() as f32); + + run(canvas, event_loop, demo_surface, window); +} diff --git a/examples/paint_filter.rs b/examples/paint_filter.rs index 49320bde..613516af 100644 --- a/examples/paint_filter.rs +++ b/examples/paint_filter.rs @@ -1,7 +1,10 @@ +use std::sync::Arc; + /** * Shows how to use `Canvas::filter_image()` to apply a blur filter. */ -use femtovg::{renderer::OpenGl, Canvas, Color, ImageFlags, Paint, Path}; +use femtovg::{Canvas, Color, ImageFlags, Paint, Path}; +use helpers::WindowSurface; use instant::Instant; use resource::resource; use winit::{ @@ -19,19 +22,10 @@ fn main() { helpers::start(); } -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; - #[cfg(target_arch = "wasm32")] use winit::window::Window; -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { let image_id = canvas .load_image_mem(&resource!("examples/assets/rust-logo.png"), ImageFlags::empty()) .unwrap(); @@ -46,11 +40,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { #[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(physical_size) => { - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); } WindowEvent::CloseRequested => event_loop_window_target.exit(), WindowEvent::RedrawRequested { .. } => { @@ -111,9 +101,7 @@ fn run( canvas.restore(); - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); if let Some(img) = filtered_image { canvas.delete_image(img) diff --git a/examples/paint_image.rs b/examples/paint_image.rs index 179202c5..e16ebee1 100644 --- a/examples/paint_image.rs +++ b/examples/paint_image.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + /** * Shows how to work with `Paint::image()` to fill paths. * The image is rendered independently of the shape of the path, @@ -6,7 +8,8 @@ * `Canvas::path_bbox()` and use it to set the cx, cy, width, height values * in `Paint::image()` as shown in this example. */ -use femtovg::{renderer::OpenGl, Canvas, Color, ImageFlags, Paint, Path, PixelFormat, RenderTarget}; +use femtovg::{Canvas, Color, ImageFlags, Paint, Path, PixelFormat, RenderTarget}; +use helpers::WindowSurface; use instant::Instant; use winit::{ event::{ElementState, Event, WindowEvent}, @@ -23,9 +26,6 @@ fn main() { helpers::start(); } -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; - #[cfg(target_arch = "wasm32")] use winit::window::Window; @@ -35,13 +35,7 @@ enum Shape { Polar, } -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { // Prepare the image, in this case a grid. let grid_size: usize = 16; let image_id = canvas @@ -101,11 +95,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { #[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(physical_size) => { - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); } WindowEvent::CloseRequested => event_loop_window_target.exit(), WindowEvent::ModifiersChanged(modifiers) => { @@ -209,9 +199,7 @@ fn run( canvas.restore(); - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); } _ => (), }, diff --git a/examples/svg.rs b/examples/svg.rs index 79f046d0..580db7ae 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -1,4 +1,6 @@ -use femtovg::{renderer::OpenGl, Canvas, Color, FillRule, ImageFlags, Paint, Path}; +use std::sync::Arc; + +use femtovg::{Canvas, Color, FillRule, ImageFlags, Paint, Path}; use instant::Instant; use resource::resource; use usvg::TreeParsing; @@ -9,7 +11,7 @@ use winit::{ }; mod helpers; -use helpers::PerfGraph; +use helpers::{PerfGraph, WindowSurface}; fn main() { #[cfg(not(target_arch = "wasm32"))] @@ -18,19 +20,10 @@ fn main() { helpers::start(); } -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; - #[cfg(target_arch = "wasm32")] use winit::window::Window; -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { canvas .add_font_mem(&resource!("examples/assets/Roboto-Light.ttf")) .expect("Cannot add font"); @@ -72,11 +65,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { #[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(physical_size) => { - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); } WindowEvent::MouseInput { button: MouseButton::Left, @@ -168,9 +157,7 @@ fn run( perf.render(&mut canvas, 5.0, 5.0); canvas.restore(); - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); } _ => (), }, diff --git a/examples/text.rs b/examples/text.rs index c6b72857..d05562e9 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -1,4 +1,6 @@ -use femtovg::{renderer::OpenGl, Align, Baseline, Canvas, Color, FontId, ImageFlags, ImageId, Paint, Path, Renderer}; +use std::sync::Arc; + +use femtovg::{Align, Baseline, Canvas, Color, FontId, ImageFlags, ImageId, Paint, Path, Renderer}; use instant::Instant; use resource::resource; use winit::{ @@ -9,7 +11,7 @@ use winit::{ }; mod helpers; -use helpers::PerfGraph; +use helpers::{PerfGraph, WindowSurface}; struct Fonts { sans: FontId, @@ -24,19 +26,10 @@ fn main() { helpers::start(); } -#[cfg(not(target_arch = "wasm32"))] -use glutin::prelude::*; - #[cfg(target_arch = "wasm32")] use winit::window::Window; -fn run( - mut canvas: Canvas, - el: EventLoop<()>, - #[cfg(not(target_arch = "wasm32"))] context: glutin::context::PossiblyCurrentContext, - #[cfg(not(target_arch = "wasm32"))] surface: glutin::surface::Surface, - window: Window, -) { +fn run(mut canvas: Canvas, el: EventLoop<()>, mut surface: W, window: Arc) { let fonts = Fonts { sans: canvas .add_font_mem(&resource!("examples/assets/Roboto-Regular.ttf")) @@ -84,11 +77,7 @@ fn run( Event::WindowEvent { ref event, .. } => match event { #[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(physical_size) => { - surface.resize( - &context, - physical_size.width.try_into().unwrap(), - physical_size.height.try_into().unwrap(), - ); + surface.resize(physical_size.width, physical_size.height); } WindowEvent::CloseRequested => event_loop_window_target.exit(), WindowEvent::KeyboardInput { @@ -203,9 +192,7 @@ fn run( canvas.restore(); } - canvas.flush(); - #[cfg(not(target_arch = "wasm32"))] - surface.swap_buffers(&context).unwrap(); + surface.present(&mut canvas); } _ => (), }, diff --git a/src/error.rs b/src/error.rs index 3de19c59..2eb05719 100644 --- a/src/error.rs +++ b/src/error.rs @@ -40,6 +40,8 @@ pub enum ErrorKind { ImageUpdateWithDifferentFormat, /// The specified image format is not supported. UnsupportedImageFormat, + /// The requested operation is not supported (for example screenshot by wgpu renderer). + UnsupportedOperation, } impl Display for ErrorKind { diff --git a/src/lib.rs b/src/lib.rs index 5f195df6..595670c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub use color::Color; pub mod renderer; pub use renderer::{RenderTarget, Renderer}; -use renderer::{Command, CommandType, Drawable, Params, ShaderType, Vertex}; +use renderer::{Command, CommandType, Drawable, Params, ShaderType, SurfacelessRenderer, Vertex}; pub(crate) mod geometry; pub use geometry::Transform2D; @@ -370,15 +370,32 @@ where /// Clears the rectangle area defined by left upper corner (x,y), width and height with the provided color. pub fn clear_rect(&mut self, x: u32, y: u32, width: u32, height: u32, color: Color) { - let cmd = Command::new(CommandType::ClearRect { - x, - y, - width, - height, - color, - }); + let mut cmd = Command::new(CommandType::ClearRect { color }); + cmd.composite_operation = self.state().composite_operation; + + let x0 = x as f32; + let y0 = y as f32; + let x1 = x0 + width as f32; + let y1 = y0 + height as f32; + let (p0, p1) = (x0, y0); + let (p2, p3) = (x1, y0); + let (p4, p5) = (x1, y1); + let (p6, p7) = (x0, y1); + + let verts = [ + Vertex::new(p0, p1, 0.0, 0.0), + Vertex::new(p4, p5, 0.0, 0.0), + Vertex::new(p2, p3, 0.0, 0.0), + Vertex::new(p0, p1, 0.0, 0.0), + Vertex::new(p6, p7, 0.0, 0.0), + Vertex::new(p4, p5, 0.0, 0.0), + ]; + + cmd.triangles_verts = Some((self.verts.len(), verts.len())); self.append_cmd(cmd); + + self.verts.extend_from_slice(&verts); } /// Returns the width of the current render target. @@ -397,12 +414,21 @@ where } } + /// Returns a screenshot of the current canvas. + pub fn screenshot(&mut self) -> Result, ErrorKind> { + self.renderer.screenshot() + } + /// Tells the renderer to execute all drawing commands and clears the current internal state /// /// Call this at the end of each frame. - pub fn flush(&mut self) { - self.renderer - .render(&mut self.images, &self.verts, std::mem::take(&mut self.commands)); + pub fn flush_to_surface(&mut self, surface: &T::Surface) { + self.renderer.render( + surface, + &mut self.images, + &self.verts, + std::mem::take(&mut self.commands), + ); self.verts.clear(); self.gradients .release_old_gradients(&mut self.images, &mut self.renderer); @@ -411,12 +437,6 @@ where } } - /// Returns a screenshot of the current canvas. - pub fn screenshot(&mut self) -> Result, ErrorKind> { - self.flush(); - self.renderer.screenshot() - } - // State Handling /// Pushes and saves the current render state into a state stack. @@ -1470,6 +1490,25 @@ where } } +impl Canvas +where + T: SurfacelessRenderer, +{ + /// Tells the renderer to execute all drawing commands and clears the current internal state + /// + /// Call this at the end of each frame. + pub fn flush(&mut self) { + self.renderer + .render_surfaceless(&mut self.images, &self.verts, std::mem::take(&mut self.commands)); + self.verts.clear(); + self.gradients + .release_old_gradients(&mut self.images, &mut self.renderer); + if let Some(atlas) = self.ephemeral_glyph_atlas.take() { + atlas.clear(self); + } + } +} + impl Drop for Canvas { fn drop(&mut self) { self.images.clear(&mut self.renderer); @@ -1495,11 +1534,13 @@ pub struct RecordingRenderer { impl Renderer for RecordingRenderer { type Image = DummyImage; type NativeTexture = (); + type Surface = (); fn set_size(&mut self, _width: u32, _height: u32, _dpi: f32) {} fn render( &mut self, + _surface: &Self::Surface, _images: &mut ImageStore, _verts: &[renderer::Vertex], commands: Vec, @@ -1567,7 +1608,7 @@ fn test_image_blit_fast_path() { .unwrap(); let paint = Paint::image(image, 0., 0., 30., 30., 0., 0.).with_anti_alias(false); canvas.fill_path(&path, &paint); - canvas.flush(); + canvas.flush_to_surface(&()); let commands = recorded_commands.borrow(); let mut commands = commands.iter(); diff --git a/src/paint.rs b/src/paint.rs index a1ccf6f3..5c6a3fe4 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -218,7 +218,7 @@ impl PaintFlavor { } } -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub enum GlyphTexture { #[default] None, @@ -226,6 +226,15 @@ pub enum GlyphTexture { ColorTexture(ImageId), } +impl GlyphTexture { + pub(crate) fn image_id(&self) -> Option { + match self { + GlyphTexture::None => None, + GlyphTexture::AlphaMask(image_id) | GlyphTexture::ColorTexture(image_id) => Some(*image_id), + } + } +} + #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct StrokeSettings { diff --git a/src/renderer.rs b/src/renderer.rs index 9d9fad8c..653c3308 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -11,6 +11,11 @@ use crate::{ mod opengl; pub use opengl::OpenGl; +#[cfg(feature = "wgpu")] +mod wgpu; +#[cfg(feature = "wgpu")] +pub use wgpu::WGPURenderer; + mod void; pub use void::Void; @@ -31,14 +36,6 @@ pub enum CommandType { SetRenderTarget(RenderTarget), /// Clear a rectangle with the specified color. ClearRect { - /// X-coordinate of the rectangle. - x: u32, - /// Y-coordinate of the rectangle. - y: u32, - /// Width of the rectangle. - width: u32, - /// Height of the rectangle. - height: u32, /// Color to fill the rectangle with. color: Color, }, @@ -124,11 +121,20 @@ pub trait Renderer { /// Associated native texture type. type NativeTexture; + /// Associated surface type. + type Surface; + /// Set the size of the renderer. fn set_size(&mut self, width: u32, height: u32, dpi: f32); /// Render the specified commands. - fn render(&mut self, images: &mut ImageStore, verts: &[Vertex], commands: Vec); + fn render( + &mut self, + surface: &Self::Surface, + images: &mut ImageStore, + verts: &[Vertex], + commands: Vec, + ); /// Allocate a new image with the specified image info. fn alloc_image(&mut self, info: ImageInfo) -> Result; @@ -157,8 +163,16 @@ pub trait Renderer { fn screenshot(&mut self) -> Result, ErrorKind>; } +/// Marker trait for renderers that don't have a surface. +pub trait SurfacelessRenderer: Renderer { + /// Render the specified commands. + fn render_surfaceless(&mut self, images: &mut ImageStore, verts: &[Vertex], commands: Vec); +} + +use bytemuck::{Pod, Zeroable}; + /// Vertex struct for specifying triangle geometry. -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default, Pod, Zeroable)] #[repr(C)] pub struct Vertex { /// X-coordinate of the vertex. @@ -208,6 +222,8 @@ pub enum ShaderType { FillColor, /// Texture copy unclipped shader. TextureCopyUnclipped, + /// Fill color shader without clipping, used for clear_rect() + FillColorUnclipped, } impl ShaderType { @@ -221,6 +237,7 @@ impl ShaderType { Self::FilterImage => 4, Self::FillColor => 5, Self::TextureCopyUnclipped => 6, + Self::FillColorUnclipped => 7, } } diff --git a/src/renderer/opengl.rs b/src/renderer/opengl.rs index 7b1cfb15..ecc84699 100644 --- a/src/renderer/opengl.rs +++ b/src/renderer/opengl.rs @@ -18,7 +18,7 @@ use crate::{ use glow::HasContext; -use super::{Command, CommandType, Params, RenderTarget, Renderer, ShaderType}; +use super::{Command, CommandType, Params, RenderTarget, Renderer, ShaderType, SurfacelessRenderer}; mod program; use program::MainProgram; @@ -468,10 +468,7 @@ impl OpenGl { self.context.bind_texture(glow::TEXTURE_2D, tex); } - let glyphtex = match glyph_tex { - GlyphTexture::None => None, - GlyphTexture::AlphaMask(id) | GlyphTexture::ColorTexture(id) => images.get(id).map(GlTexture::id), - }; + let glyphtex = glyph_tex.image_id().and_then(|id| images.get(id).map(GlTexture::id)); unsafe { self.context.active_texture(glow::TEXTURE0 + 1); @@ -687,6 +684,7 @@ impl OpenGl { impl Renderer for OpenGl { type Image = GlTexture; type NativeTexture = ::Texture; + type Surface = (); fn set_size(&mut self, width: u32, height: u32, _dpi: f32) { self.view[0] = width as f32; @@ -703,7 +701,13 @@ impl Renderer for OpenGl { Ok(image.id()) } - fn render(&mut self, images: &mut ImageStore, verts: &[Vertex], commands: Vec) { + fn render( + &mut self, + _surface: &Self::Surface, + images: &mut ImageStore, + verts: &[Vertex], + commands: Vec, + ) { self.current_program = 0; self.main_program().bind(); @@ -764,14 +768,14 @@ impl Renderer for OpenGl { ref params2, } => self.stencil_stroke(images, &cmd, params1, params2), CommandType::Triangles { ref params } => self.triangles(images, &cmd, params), - CommandType::ClearRect { - x, - y, - width, - height, - color, - } => { - self.clear_rect(x, y, width, height, color); + CommandType::ClearRect { color } => { + if let Some((start, _)) = cmd.triangles_verts { + let x = verts[start].x as _; + let y = verts[start].y as _; + let width = verts[start + 1].x as u32 - x; + let height = verts[start + 1].y as u32 - y; + self.clear_rect(x, y, width, height, color); + } } CommandType::SetRenderTarget(target) => { self.set_target(images, target); @@ -866,6 +870,12 @@ impl Renderer for OpenGl { } } +impl SurfacelessRenderer for OpenGl { + fn render_surfaceless(&mut self, images: &mut ImageStore, verts: &[Vertex], commands: Vec) { + self.render(&(), images, verts, commands) + } +} + impl Drop for OpenGl { fn drop(&mut self) { if let Some(vert_arr) = self.vert_arr { diff --git a/src/renderer/void.rs b/src/renderer/void.rs index d00aacb9..60af6574 100644 --- a/src/renderer/void.rs +++ b/src/renderer/void.rs @@ -13,10 +13,18 @@ pub struct Void; impl Renderer for Void { type Image = VoidImage; type NativeTexture = (); + type Surface = (); fn set_size(&mut self, width: u32, height: u32, dpi: f32) {} - fn render(&mut self, images: &mut ImageStore, verts: &[Vertex], commands: Vec) {} + fn render( + &mut self, + _surface: &Self::Surface, + images: &mut ImageStore, + verts: &[Vertex], + commands: Vec, + ) { + } fn alloc_image(&mut self, info: ImageInfo) -> Result { Ok(VoidImage { info }) diff --git a/src/renderer/wgpu.rs b/src/renderer/wgpu.rs new file mode 100644 index 00000000..78d03724 --- /dev/null +++ b/src/renderer/wgpu.rs @@ -0,0 +1,1536 @@ +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::Arc; + +use rgb::bytemuck; +use wgpu::util::DeviceExt; +use wgpu::PipelineCompilationOptions; + +use crate::image::ImageStore; +use crate::paint::GlyphTexture; +use crate::renderer::ShaderType; +use crate::BlendFactor; +use crate::FillRule; +use crate::ImageId; +use crate::ImageInfo; +use crate::RenderTarget; +use crate::Scissor; + +use super::Renderer; + +pub use wgpu; + +/* +#[repr(C)] +#[derive(Clone, Copy,)] +struct Vertex { + _pos: [f32; 4], + _tex_coord: [f32; 2], +} + */ + +use super::Params; +use super::Vertex; + +const UNIFORMARRAY_SIZE: usize = 14; + +#[derive(Clone, PartialEq)] +pub struct UniformArray([f32; UNIFORMARRAY_SIZE * 4]); + +impl Default for UniformArray { + fn default() -> Self { + Self([ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + ]) + } +} + +impl UniformArray { + pub fn as_slice(&self) -> &[f32] { + &self.0 + } + + pub fn set_scissor_mat(&mut self, mat: [f32; 12]) { + self.0[0..12].copy_from_slice(&mat); + } + + pub fn set_paint_mat(&mut self, mat: [f32; 12]) { + self.0[12..24].copy_from_slice(&mat); + } + + pub fn set_inner_col(&mut self, col: [f32; 4]) { + self.0[24..28].copy_from_slice(&col); + } + + pub fn set_outer_col(&mut self, col: [f32; 4]) { + self.0[28..32].copy_from_slice(&col); + } + + pub fn set_scissor_ext(&mut self, ext: [f32; 2]) { + self.0[32..34].copy_from_slice(&ext); + } + + pub fn set_scissor_scale(&mut self, scale: [f32; 2]) { + self.0[34..36].copy_from_slice(&scale); + } + + pub fn set_extent(&mut self, ext: [f32; 2]) { + self.0[36..38].copy_from_slice(&ext); + } + + pub fn set_radius(&mut self, radius: f32) { + self.0[38] = radius; + } + + pub fn set_feather(&mut self, feather: f32) { + self.0[39] = feather; + } + + pub fn set_stroke_mult(&mut self, stroke_mult: f32) { + self.0[40] = stroke_mult; + } + + pub fn set_stroke_thr(&mut self, stroke_thr: f32) { + self.0[41] = stroke_thr; + } + + pub fn set_tex_type(&mut self, tex_type: f32) { + self.0[42] = tex_type; + } + + pub fn set_shader_type(&mut self, shader_type: f32) { + self.0[43] = shader_type; + } + + pub fn set_glyph_texture_type(&mut self, glyph_texture_type: f32) { + self.0[44] = glyph_texture_type; + } + + pub fn set_image_blur_filter_direction(&mut self, direction: [f32; 2]) { + self.0[46..48].copy_from_slice(&direction); + } + + pub fn set_image_blur_filter_sigma(&mut self, sigma: f32) { + self.0[45] = sigma; + } + + pub fn set_image_blur_filter_coeff(&mut self, coeff: [f32; 3]) { + self.0[48..51].copy_from_slice(&coeff); + } +} + +impl From<&Params> for UniformArray { + fn from(params: &Params) -> Self { + let mut arr = Self::default(); + + arr.set_scissor_mat(params.scissor_mat); + arr.set_paint_mat(params.paint_mat); + arr.set_inner_col(params.inner_col); + arr.set_outer_col(params.outer_col); + arr.set_scissor_ext(params.scissor_ext); + arr.set_scissor_scale(params.scissor_scale); + arr.set_extent(params.extent); + arr.set_radius(params.radius); + arr.set_feather(params.feather); + arr.set_stroke_mult(params.stroke_mult); + arr.set_stroke_thr(params.stroke_thr); + arr.set_shader_type(params.shader_type.to_f32()); + arr.set_tex_type(params.tex_type); + arr.set_glyph_texture_type(params.glyph_texture_type as f32); + arr.set_image_blur_filter_direction(params.image_blur_filter_direction); + arr.set_image_blur_filter_sigma(params.image_blur_filter_sigma); + arr.set_image_blur_filter_coeff(params.image_blur_filter_coeff); + + arr + } +} + +pub struct Image { + texture: Rc, + info: ImageInfo, +} + +/// WGPU renderer. +pub struct WGPURenderer { + device: Arc, + queue: Arc, + + shader_module: Rc, + + screen_view: [f32; 2], + + empty_texture: Rc, + stencil_buffer: Option>, + stencil_buffer_for_textures: HashMap, Rc>, + + bind_group_layout: Rc, + viewport_bind_group_layout: Rc, + pipeline_layout: Rc, +} + +impl WGPURenderer { + /// Creates a new renderer for the device. + pub fn new(device: Arc, queue: Arc) -> Self { + let module = wgpu::include_wgsl!("wgpu/shader.wgsl"); + let shader_module = Rc::new(device.create_shader_module(module)); + + let texture_descriptor = wgpu::TextureDescriptor { + size: wgpu::Extent3d::default(), + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + label: None, + view_formats: &[], + }; + let empty_texture = Rc::new(device.create_texture(&wgpu::TextureDescriptor { + label: Some("empty"), + view_formats: &[], + ..texture_descriptor + })); + + queue.write_texture( + empty_texture.as_image_copy(), + &[255, 0, 0, 255], + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(4), + rows_per_image: None, + }, + wgpu::Extent3d::default(), + ); + + let viewport_bind_group_layout = Rc::new(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Bind Group Layout for Viewport uniform"), + 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_layout = Rc::new(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + 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::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, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + 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: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + })); + + let pipeline_layout = Rc::new(device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&viewport_bind_group_layout, &bind_group_layout], + push_constant_ranges: &[], + })); + + Self { + device, + queue, + + shader_module, + + screen_view: [0.0, 0.0], + + empty_texture, + stencil_buffer: None, + stencil_buffer_for_textures: HashMap::new(), + bind_group_layout, + viewport_bind_group_layout, + pipeline_layout, + } + } +} + +impl Renderer for WGPURenderer { + type Image = Image; + type NativeTexture = wgpu::Texture; + type Surface = wgpu::Texture; + + fn set_size(&mut self, _width: u32, _height: u32, _dpi: f32) {} + + fn render( + &mut self, + surface_texture: &Self::Surface, + images: &mut crate::image::ImageStore, + verts: &[super::Vertex], + commands: Vec, + ) { + self.screen_view[0] = surface_texture.width() as f32; + self.screen_view[1] = surface_texture.height() as f32; + + let texture_view = std::rc::Rc::new(surface_texture.create_view(&wgpu::TextureViewDescriptor::default())); + + let vertex_buffer = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Main Vertex Buffer"), + contents: bytemuck::cast_slice(verts), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + if let Some(stencil_buffer) = &self.stencil_buffer { + if stencil_buffer.size() != surface_texture.size() { + self.stencil_buffer = None; + } + } + + let stencil_buffer = self + .stencil_buffer + .get_or_insert_with(|| { + Rc::new(self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("Stencil buffer"), + size: wgpu::Extent3d { + width: surface_texture.width(), + height: surface_texture.height(), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Stencil8, + view_formats: &[], + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + })) + }) + .clone(); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + + let mut render_pass_builder = RenderPassBuilder::new( + self.device.clone(), + &mut encoder, + surface_texture.format(), + self.screen_view, + self.viewport_bind_group_layout.clone(), + &mut self.stencil_buffer_for_textures, + texture_view, + stencil_buffer.clone(), + vertex_buffer, + ); + + let mut pipeline_and_bindgroup_mapper = CommandToPipelineAndBindGroupMapper::new( + self.device.clone(), + self.empty_texture.clone(), + self.shader_module.clone(), + self.bind_group_layout.clone(), + self.pipeline_layout.clone(), + ); + + let mut current_render_target = RenderTarget::Screen; + + for command in commands { + let blend_state = wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: blend_factor(command.composite_operation.src_rgb), + dst_factor: blend_factor(command.composite_operation.dst_rgb), + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: blend_factor(command.composite_operation.src_alpha), + dst_factor: blend_factor(command.composite_operation.dst_alpha), + operation: wgpu::BlendOperation::Add, + }, + }; + + match command.cmd_type { + super::CommandType::SetRenderTarget(render_target) => { + current_render_target = render_target; + //eprintln!("SRT {:?}", render_target); + //assert!(matches!(render_target, RenderTarget::Screen)); + match render_target { + RenderTarget::Screen => { + render_pass_builder.set_render_target_screen(); + } + RenderTarget::Image(image_id) => { + render_pass_builder.set_render_target_image(images, image_id, wgpu::LoadOp::Load); + } + } + } + super::CommandType::ClearRect { color } => { + let mut params = Params::new( + images, + &Default::default(), + &crate::paint::PaintFlavor::Color(color), + &Default::default(), + &Scissor::default(), + 0., + 0., + 0., + ); + params.shader_type = ShaderType::FillColorUnclipped; + if let Some((start, count)) = command.triangles_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::Zero, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::Zero, + operation: wgpu::BlendOperation::Add, + }, + }), + wgpu::PrimitiveTopology::TriangleList, + StencilTest::Disabled, // ### clear stencil mask + None, + ¶ms, + images, + None, + Default::default(), + ); + + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + super::CommandType::ConvexFill { ref params } => { + for drawable in &command.drawables { + if let Some((start, count)) = drawable.fill_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleList, + StencilTest::Disabled, + Some(wgpu::Face::Back), + params, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + render_pass_builder.draw(start as u32..(start + count) as u32); + } + + if let Some((start, count)) = drawable.stroke_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleStrip, + StencilTest::Disabled, + Some(wgpu::Face::Back), + params, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + } + super::CommandType::ConcaveFill { + ref stencil_params, + ref fill_params, + } => { + if command.drawables.iter().any(|drawable| drawable.fill_verts.is_some()) { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + None, + wgpu::PrimitiveTopology::TriangleList, + StencilTest::Enabled { + stencil_state: wgpu::StencilState { + front: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Always, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::IncrementWrap, + }, + back: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Always, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::DecrementWrap, + }, + read_mask: !0, + write_mask: !0, + }, + stencil_reference: 0, + }, + None, + stencil_params, + images, + None, + GlyphTexture::None, + ); + + for drawable in &command.drawables { + if let Some((start, count)) = drawable.fill_verts { + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + } + + if command.drawables.iter().any(|drawable| drawable.stroke_verts.is_some()) { + for drawable in &command.drawables { + // draw fringes + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleStrip, + StencilTest::Enabled { + stencil_state: wgpu::StencilState { + front: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Equal, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::Keep, + }, + back: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Equal, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::Keep, + }, + read_mask: match command.fill_rule { + FillRule::NonZero => 0xff, + FillRule::EvenOdd => 0x1, + }, + write_mask: match command.fill_rule { + FillRule::NonZero => 0xff, + FillRule::EvenOdd => 0x1, + }, + }, + stencil_reference: 0, + }, + Some(wgpu::Face::Back), + fill_params, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + + if let Some((start, count)) = drawable.stroke_verts { + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + } + + if let Some((start, count)) = command.triangles_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleStrip, + StencilTest::Enabled { + stencil_state: wgpu::StencilState { + front: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::NotEqual, + fail_op: wgpu::StencilOperation::Zero, + depth_fail_op: wgpu::StencilOperation::Zero, + pass_op: wgpu::StencilOperation::Zero, + }, + back: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::NotEqual, + fail_op: wgpu::StencilOperation::Zero, + depth_fail_op: wgpu::StencilOperation::Zero, + pass_op: wgpu::StencilOperation::Zero, + }, + read_mask: match command.fill_rule { + FillRule::NonZero => 0xff, + FillRule::EvenOdd => 0x1, + }, + write_mask: match command.fill_rule { + FillRule::NonZero => 0xff, + FillRule::EvenOdd => 0x1, + }, + }, + stencil_reference: 0, + }, + Some(wgpu::Face::Back), + fill_params, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + super::CommandType::Stroke { params } => { + for drawable in &command.drawables { + if let Some((start, count)) = drawable.stroke_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleStrip, + StencilTest::Disabled, + Some(wgpu::Face::Back), + ¶ms, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + } + super::CommandType::StencilStroke { params1, params2 } => { + if command + .drawables + .iter() + .any(|drawable: &super::Drawable| drawable.stroke_verts.is_some()) + { + // Fill the stroke base without overlap + + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleStrip, + StencilTest::Enabled { + stencil_state: wgpu::StencilState { + front: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Equal, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::IncrementClamp, + }, + back: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Equal, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::IncrementClamp, + }, + read_mask: !0, + write_mask: !0, + }, + stencil_reference: 0, + }, + Some(wgpu::Face::Back), + ¶ms2, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + + for drawable in &command.drawables { + if let Some((start, count)) = drawable.stroke_verts { + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + + // Draw anti-aliased pixels. + + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleStrip, + StencilTest::Enabled { + stencil_state: wgpu::StencilState { + front: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Equal, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::Keep, + }, + back: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Equal, + fail_op: wgpu::StencilOperation::Keep, + depth_fail_op: wgpu::StencilOperation::Keep, + pass_op: wgpu::StencilOperation::Keep, + }, + read_mask: !0, + write_mask: !0, + }, + stencil_reference: 0, + }, + Some(wgpu::Face::Back), + ¶ms1, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + + for drawable in &command.drawables { + if let Some((start, count)) = drawable.stroke_verts { + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + + // clear stencil buffer + + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + None, + wgpu::PrimitiveTopology::TriangleStrip, + StencilTest::Enabled { + stencil_state: wgpu::StencilState { + front: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Always, + fail_op: wgpu::StencilOperation::Zero, + depth_fail_op: wgpu::StencilOperation::Zero, + pass_op: wgpu::StencilOperation::Zero, + }, + back: wgpu::StencilFaceState { + compare: wgpu::CompareFunction::Always, + fail_op: wgpu::StencilOperation::Zero, + depth_fail_op: wgpu::StencilOperation::Zero, + pass_op: wgpu::StencilOperation::Zero, + }, + read_mask: !0, + write_mask: !0, + }, + stencil_reference: 0, + }, + Some(wgpu::Face::Back), + ¶ms1, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + + for drawable in &command.drawables { + if let Some((start, count)) = drawable.stroke_verts { + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + } + } + super::CommandType::Triangles { ref params } => { + if let Some((start, count)) = command.triangles_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleList, + StencilTest::Disabled, + Some(wgpu::Face::Back), + params, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + render_pass_builder.draw(start as u32..(start + count) as u32); + } + } + super::CommandType::RenderFilteredImage { target_image, filter } => { + match filter { + crate::ImageFilter::GaussianBlur { sigma } => { + let previous_render_target = current_render_target; + + let source_image = images.get(command.image.unwrap()).unwrap(); + + let image_paint = crate::Paint::image( + command.image.unwrap(), + 0., + 0., + source_image.texture.width() as _, + source_image.texture.height() as _, + 0., + 1., + ); + + let mut blur_params = Params::new( + images, + &Default::default(), + &image_paint.flavor, + &Default::default(), + &Scissor::default(), + 0., + 0., + 0., + ); + blur_params.shader_type = ShaderType::FilterImage; + + let gauss_coeff_x = 1. / ((2. * std::f32::consts::PI).sqrt() * sigma); + let gauss_coeff_y = f32::exp(-0.5 / (sigma * sigma)); + let gauss_coeff_z = gauss_coeff_y * gauss_coeff_y; + + blur_params.image_blur_filter_coeff[0] = gauss_coeff_x; + blur_params.image_blur_filter_coeff[1] = gauss_coeff_y; + blur_params.image_blur_filter_coeff[2] = gauss_coeff_z; + + blur_params.image_blur_filter_direction = [1.0, 0.0]; + // GLES 2.0 does not allow non-constant loop indices, so limit the standard devitation to allow for a upper fixed limit + // on the number of iterations in the fragment shader. + blur_params.image_blur_filter_sigma = sigma.min(8.); + + let horizontal_blur_buffer = + Rc::new(self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("blur horizontal"), + size: wgpu::Extent3d { + width: source_image.texture.width(), + height: source_image.texture.height(), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: source_image.texture.format(), + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + })); + + render_pass_builder.set_render_target_texture( + &horizontal_blur_buffer, + None, + wgpu::LoadOp::Clear(wgpu::Color::default()), + ); + + if let Some((start, count)) = command.triangles_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleList, + StencilTest::Disabled, + Some(wgpu::Face::Back), + &blur_params, + images, + command.image.map(ImageOrTexture::Image), + command.glyph_texture, + ); + render_pass_builder.draw(start as u32..(start + count) as u32); + } + + render_pass_builder.set_render_target_image( + images, + target_image, + wgpu::LoadOp::Clear(wgpu::Color::default()), + ); + + blur_params.image_blur_filter_direction = [0.0, 1.0]; + + if let Some((start, count)) = command.triangles_verts { + pipeline_and_bindgroup_mapper.update_renderpass( + &mut render_pass_builder, + blend_state.into(), + wgpu::PrimitiveTopology::TriangleList, + StencilTest::Disabled, + Some(wgpu::Face::Back), + &blur_params, + images, + Some(ImageOrTexture::Texture(horizontal_blur_buffer)), + command.glyph_texture, + ); + render_pass_builder.draw(start as u32..(start + count) as u32); + } + + current_render_target = previous_render_target; + match current_render_target { + RenderTarget::Screen => { + render_pass_builder.set_render_target_screen(); + } + RenderTarget::Image(image_id) => { + render_pass_builder.set_render_target_image(images, image_id, wgpu::LoadOp::Load); + } + } + } + } + } + } + + // render_pass_builder.finish(); + } + + drop(render_pass_builder); + + self.queue.submit(Some(encoder.finish())); + } + + fn alloc_image(&mut self, info: crate::ImageInfo) -> Result { + Ok(Image { + texture: Rc::new(self.device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: info.width() as u32, + height: info.height() as u32, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: match info.format() { + crate::PixelFormat::Rgb8 => wgpu::TextureFormat::Rgba8Unorm, + crate::PixelFormat::Rgba8 => wgpu::TextureFormat::Rgba8Unorm, + crate::PixelFormat::Gray8 => wgpu::TextureFormat::R8Unorm, + }, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + })), + info, + }) + } + + fn create_image_from_native_texture( + &mut self, + native_texture: Self::NativeTexture, + info: crate::ImageInfo, + ) -> Result { + Ok(Image { + texture: Rc::new(native_texture), + info, + }) + } + + fn update_image( + &mut self, + image: &mut Self::Image, + data: crate::ImageSource, + x: usize, + y: usize, + ) -> Result<(), crate::ErrorKind> { + #[cfg(target_arch = "wasm32")] + if let crate::ImageSource::HtmlImageElement(htmlimage) = data { + self.queue.copy_external_image_to_texture( + &wgpu::ImageCopyExternalImage { + source: wgpu::ExternalImageSource::HTMLImageElement(htmlimage.clone()), + origin: wgpu::Origin2d::ZERO, + flip_y: false, + }, + wgpu::ImageCopyTextureTagged { + texture: &image.texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + color_space: wgpu::PredefinedColorSpace::Srgb, + premultiplied_alpha: true, + }, + wgpu::Extent3d { + width: data.dimensions().width as _, + height: data.dimensions().height as _, + depth_or_array_layers: 1, + }, + ); + } + + use rgb::ComponentBytes; + + let converted_rgba; + let (bytes, bpp) = match data { + crate::ImageSource::Rgb(img) => { + converted_rgba = img + .pixels() + .map(|rgb| rgb::Rgba { + r: rgb.r, + g: rgb.g, + b: rgb.b, + a: 255, + }) + .collect::>(); + (converted_rgba.as_bytes(), 4) + } + crate::ImageSource::Rgba(img) => (img.buf().as_bytes(), 4), + crate::ImageSource::Gray(img) => (img.buf().as_bytes(), 1), + #[cfg(target_arch = "wasm32")] + crate::ImageSource::HtmlImageElement(..) => { + unreachable!() + } + }; + + let mut target = image.texture.as_image_copy(); + target.origin.x = x as _; + target.origin.y = y as _; + + self.queue.write_texture( + target, + bytes, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(bpp * image.texture.width()), + rows_per_image: None, + }, + wgpu::Extent3d { + width: data.dimensions().width as _, + height: data.dimensions().height as _, + depth_or_array_layers: 1, + }, + ); + Ok(()) + } + + fn delete_image(&mut self, image: Self::Image, _image_id: crate::ImageId) { + self.stencil_buffer_for_textures.remove(&image.texture); + drop(image); + } + + fn screenshot(&mut self) -> Result, crate::ErrorKind> { + return Err(crate::ErrorKind::UnsupportedOperation); + } +} + +#[derive(Clone, PartialEq, Debug)] +enum StencilTest { + Disabled, + Enabled { + stencil_state: wgpu::StencilState, + stencil_reference: u32, + }, +} + +#[derive(Clone, Hash, PartialEq, Eq)] +struct PipelineState { + shader_type: ShaderType, + enable_glyph_texture: bool, + render_to_texture: bool, + color_target_state: wgpu::ColorTargetState, + primitive_topology: wgpu::PrimitiveTopology, + cull_mode: Option, + stencil_state: Option, +} + +impl PipelineState { + fn new( + color_blend: Option, + stencil_test: StencilTest, + format: wgpu::TextureFormat, + shader_type: ShaderType, + enable_glyph_texture: bool, + render_to_texture: bool, + primitive_topology: wgpu::PrimitiveTopology, + cull_mode: Option, + has_stencil_buffer: bool, + ) -> Self { + let (stencil_state, color_target_state) = match &stencil_test { + StencilTest::Enabled { stencil_state, .. } => ( + stencil_state.clone(), + wgpu::ColorTargetState { + format, + blend: color_blend, + write_mask: if color_blend.is_some() { + wgpu::ColorWrites::ALL + } else { + wgpu::ColorWrites::empty() + }, + }, + ), + StencilTest::Disabled => ( + wgpu::StencilState { + front: wgpu::StencilFaceState::IGNORE, + back: wgpu::StencilFaceState::IGNORE, + read_mask: !0, + write_mask: !0, + }, + wgpu::ColorTargetState { + format, + blend: color_blend, + write_mask: wgpu::ColorWrites::ALL, + }, + ), + }; + Self { + shader_type, + enable_glyph_texture, + render_to_texture, + color_target_state, + primitive_topology, + cull_mode, + stencil_state: has_stencil_buffer.then_some(stencil_state), + } + } + + fn materialize( + &self, + device: &wgpu::Device, + pipeline_layout: &wgpu::PipelineLayout, + shader_module: &wgpu::ShaderModule, + ) -> wgpu::RenderPipeline { + let constants = HashMap::from([ + ("shader_type".to_string(), self.shader_type.to_f32() as f64), + ( + "enable_glyph_texture".to_string(), + if self.enable_glyph_texture { 1.0 } else { 0.0 }, + ), + ( + "render_to_texture".to_string(), + if self.render_to_texture { 1.0 } else { 0. }, + ), + ]); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(pipeline_layout), + vertex: wgpu::VertexState { + module: shader_module, + entry_point: Some("vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2], + }], + compilation_options: PipelineCompilationOptions { + constants: &constants, + ..Default::default() + }, + }, + fragment: Some(wgpu::FragmentState { + module: shader_module, + entry_point: Some("fs_main"), + compilation_options: PipelineCompilationOptions { + constants: &constants, + ..Default::default() + }, + targets: &[Some(self.color_target_state.clone())], + }), + primitive: wgpu::PrimitiveState { + topology: self.primitive_topology, + front_face: if self.render_to_texture { + wgpu::FrontFace::Cw + } else { + wgpu::FrontFace::Ccw + }, + cull_mode: self.cull_mode, + ..Default::default() + }, + depth_stencil: self + .stencil_state + .as_ref() + .map(|stencil_state| wgpu::DepthStencilState { + format: wgpu::TextureFormat::Stencil8, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Always, + stencil: stencil_state.clone(), + bias: Default::default(), + }), + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }) + } +} + +#[derive(Clone, PartialEq)] +enum ImageOrTexture { + Image(ImageId), + Texture(Rc), +} + +#[derive(Clone, PartialEq)] +struct BindGroupState { + image: Option, + glyph_texture: GlyphTexture, + uniforms: UniformArray, +} + +impl BindGroupState { + fn materialize( + &self, + device: &wgpu::Device, + images: &ImageStore, + bind_group_layout: &wgpu::BindGroupLayout, + empty_texture: &Rc, + ) -> wgpu::BindGroup { + let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Fragment Uniform Buffer"), + contents: bytemuck::cast_slice(self.uniforms.as_slice()), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let (main_texture_view, main_sampler) = + RenderPassBuilder::create_texture_view_and_sampler(device, images, self.image.as_ref(), empty_texture); + let (glyph_texture_view, glyph_sampler) = RenderPassBuilder::create_texture_view_and_sampler( + device, + images, + self.glyph_texture.image_id().map(ImageOrTexture::Image).as_ref(), + empty_texture, + ); + + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buf.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&main_texture_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&main_sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView(&glyph_texture_view), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&glyph_sampler), + }, + ], + label: None, + }) + } +} + +struct RenderPassBuilder<'a> { + device: Arc, + encoder: &'a mut wgpu::CommandEncoder, + surface_view: std::rc::Rc, + surface_format: wgpu::TextureFormat, + texture_view: std::rc::Rc, + stencil_buffer: Option>, + viewport: [f32; 2], + vertex_buffer: wgpu::Buffer, + rendering_to_texture: bool, + viewport_bind_group_layout: Rc, + current_bind_group_state: Option, + rpass: Option>, + screen_stencil_buffer: Rc, + screen_view: [f32; 2], + screen_surface_format: wgpu::TextureFormat, + stencil_buffer_for_textures: &'a mut HashMap, Rc>, + viewport_bind_group: wgpu::BindGroup, +} + +impl<'a> RenderPassBuilder<'a> { + fn new( + device: Arc, + encoder: &'a mut wgpu::CommandEncoder, + screen_surface_format: wgpu::TextureFormat, + screen_view: [f32; 2], + viewport_bind_group_layout: Rc, + stencil_buffer_for_textures: &'a mut HashMap, Rc>, + texture_view: Rc, + stencil_buffer: Rc, + vertex_buffer: wgpu::Buffer, + ) -> Self { + let viewport_bind_group = Self::create_viewport_bind_group(&device, &screen_view, &viewport_bind_group_layout); + Self { + device: device.clone(), + encoder, + surface_view: texture_view.clone(), + surface_format: screen_surface_format, + texture_view, + stencil_buffer: Some(stencil_buffer.clone()), + viewport: screen_view, + vertex_buffer, + rendering_to_texture: false, + viewport_bind_group_layout, + current_bind_group_state: None, + rpass: None, + screen_stencil_buffer: stencil_buffer, + screen_view, + screen_surface_format, + stencil_buffer_for_textures, + viewport_bind_group, + } + } + + fn set_viewport(&mut self, viewport: [f32; 2]) { + if self.viewport == viewport { + return; + } + self.viewport = viewport; + self.viewport_bind_group = + Self::create_viewport_bind_group(&self.device, &self.viewport, &self.viewport_bind_group_layout); + } + + fn create_viewport_bind_group( + device: &wgpu::Device, + viewport: &[f32; 2], + viewport_bind_group_layout: &wgpu::BindGroupLayout, + ) -> wgpu::BindGroup { + let view_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Uniform Buffer for Viewport"), + contents: bytemuck::cast_slice(viewport), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: viewport_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: view_buf.as_entire_binding(), + }], + label: None, + }); + + viewport_bind_group + } + + fn create_texture_view_and_sampler( + device: &wgpu::Device, + images: &ImageStore, + image: Option<&ImageOrTexture>, + empty_texture: &Rc, + ) -> (wgpu::TextureView, wgpu::Sampler) { + let texture_and_flags = image.and_then(|image_or_texture| match image_or_texture { + ImageOrTexture::Image(image_id) => images.get(*image_id).map(|img| (img.texture.clone(), img.info.flags())), + ImageOrTexture::Texture(texture) => Some((texture.clone(), crate::ImageFlags::empty())), + }); + let texture_view = texture_and_flags + .as_ref() + .map_or_else(|| empty_texture, |(texture, _)| texture) + .create_view(&Default::default()); + + let flags = texture_and_flags.map_or(crate::ImageFlags::empty(), |(_, flags)| flags); + + let filter_mode = if flags.contains(crate::ImageFlags::NEAREST) { + wgpu::FilterMode::Nearest + } else { + wgpu::FilterMode::Linear + }; + + // ### Share + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: if flags.contains(crate::ImageFlags::REPEAT_X) { + wgpu::AddressMode::Repeat + } else { + wgpu::AddressMode::ClampToEdge + }, + address_mode_v: if flags.contains(crate::ImageFlags::REPEAT_Y) { + wgpu::AddressMode::Repeat + } else { + wgpu::AddressMode::ClampToEdge + }, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: filter_mode, + min_filter: filter_mode, + ..Default::default() + }); + (texture_view, sampler) + } + + fn set_render_target_texture( + &mut self, + texture: &wgpu::Texture, + stencil_buffer: Option>, + load: wgpu::LoadOp, + ) { + self.texture_view = std::rc::Rc::new(texture.create_view(&Default::default())); + self.set_viewport([texture.width() as f32, texture.height() as f32]); + self.stencil_buffer = stencil_buffer; + self.surface_format = texture.format(); + self.rendering_to_texture = true; + + self.recreate_render_pass(load); + } + + fn set_render_target_image( + &mut self, + images: &mut ImageStore, + image_id: ImageId, + load: wgpu::LoadOp, + ) { + let image = images.get(image_id).unwrap(); + + let stencil_buffer = self + .stencil_buffer_for_textures + .entry(image.texture.clone()) + .or_insert_with(|| { + Rc::new(self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("Stencil buffer"), + size: wgpu::Extent3d { + width: image.texture.width(), + height: image.texture.height(), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Stencil8, + view_formats: &[], + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + })) + }) + .clone(); + + self.set_render_target_texture(&image.texture.clone(), Some(stencil_buffer), load); + } + + fn set_render_target_screen(&mut self) { + self.texture_view = self.surface_view.clone(); + self.stencil_buffer = Some(self.screen_stencil_buffer.clone()); + self.set_viewport(self.screen_view); + self.surface_format = self.screen_surface_format; + self.rendering_to_texture = false; + + self.recreate_render_pass(wgpu::LoadOp::Load); + } + + fn recreate_render_pass(&mut self, load: wgpu::LoadOp) { + drop(self.rpass.take()); + let stencil_view = self + .stencil_buffer + .as_ref() + .map(|buffer| buffer.create_view(&Default::default())); + + let mut rpass = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.texture_view, + resolve_target: None, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: stencil_view + .as_ref() + .map(|view| wgpu::RenderPassDepthStencilAttachment { + view, + depth_ops: None, + stencil_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + rpass.set_viewport(0., 0., self.viewport[0], self.viewport[1], 0., 0.); + self.current_bind_group_state.take(); + rpass.set_bind_group(0, &self.viewport_bind_group, &[]); + self.rpass = Some(rpass.forget_lifetime()); + } + + fn draw(&mut self, vertices: std::ops::Range) { + self.rpass.as_mut().unwrap().draw(vertices, 0..1); + } +} + +struct CommandToPipelineAndBindGroupMapper { + device: Arc, + empty_texture: Rc, + shader_module: Rc, + + current_bind_group_state: Option, + current_bind_group: Option, + bind_group_layout: Rc, + current_pipeline_state: Option, + pipeline_cache: HashMap, + pipeline_layout: Rc, +} + +impl CommandToPipelineAndBindGroupMapper { + fn new( + device: Arc, + empty_texture: Rc, + shader_module: Rc, + bind_group_layout: Rc, + pipeline_layout: Rc, + ) -> Self { + Self { + device: device.clone(), + empty_texture, + shader_module, + current_bind_group_state: None, + current_bind_group: None, + bind_group_layout, + current_pipeline_state: None, + pipeline_cache: Default::default(), + pipeline_layout, + } + } + + fn update_renderpass<'a>( + &mut self, + render_pass_builder: &'a mut RenderPassBuilder<'_>, + color_blend: Option, + primitive_topology: wgpu::PrimitiveTopology, + stencil_test: StencilTest, + cull_mode: Option, + params: &Params, + images: &'a ImageStore, + image: Option, + glyph_texture: GlyphTexture, + ) { + let render_pass = render_pass_builder.rpass.as_mut().unwrap(); + + if let StencilTest::Enabled { stencil_reference, .. } = &stencil_test { + render_pass.set_stencil_reference(*stencil_reference); + } else { + render_pass.set_stencil_reference(0); + } + + let bind_group_state = BindGroupState { + image, + glyph_texture, + uniforms: UniformArray::from(params), + }; + + if self.current_bind_group_state != Some(bind_group_state.clone()) { + self.current_bind_group = bind_group_state + .materialize(&self.device, images, &self.bind_group_layout, &self.empty_texture) + .into(); + self.current_bind_group_state = Some(bind_group_state); + } + render_pass.set_bind_group(1, self.current_bind_group.as_ref().unwrap(), &[]); + + let pipeline_state = PipelineState::new( + color_blend, + stencil_test, + render_pass_builder.surface_format, + params.shader_type, + params.uses_glyph_texture(), + render_pass_builder.rendering_to_texture, + primitive_topology, + cull_mode, + render_pass_builder.stencil_buffer.is_some(), + ); + + if self.current_pipeline_state.as_ref() != Some(&pipeline_state) { + self.current_pipeline_state = Some(pipeline_state.clone()); + let render_pipeline = self.pipeline_cache.entry(pipeline_state.clone()).or_insert_with(|| { + pipeline_state.materialize(&self.device, &self.pipeline_layout, &self.shader_module) + }); + + render_pass.set_pipeline(&render_pipeline); + } + } +} + +fn blend_factor(factor: BlendFactor) -> wgpu::BlendFactor { + match factor { + BlendFactor::Zero => wgpu::BlendFactor::Zero, + BlendFactor::One => wgpu::BlendFactor::One, + BlendFactor::SrcColor => wgpu::BlendFactor::Src, + BlendFactor::OneMinusSrcColor => wgpu::BlendFactor::OneMinusSrc, + BlendFactor::DstColor => wgpu::BlendFactor::Dst, + BlendFactor::OneMinusDstColor => wgpu::BlendFactor::OneMinusDst, + BlendFactor::SrcAlpha => wgpu::BlendFactor::SrcAlpha, + BlendFactor::OneMinusSrcAlpha => wgpu::BlendFactor::OneMinusSrcAlpha, + BlendFactor::DstAlpha => wgpu::BlendFactor::DstAlpha, + BlendFactor::OneMinusDstAlpha => wgpu::BlendFactor::OneMinusDstAlpha, + BlendFactor::SrcAlphaSaturate => wgpu::BlendFactor::SrcAlphaSaturated, + } +} diff --git a/src/renderer/wgpu/shader.wgsl b/src/renderer/wgpu/shader.wgsl new file mode 100644 index 00000000..a660b137 --- /dev/null +++ b/src/renderer/wgpu/shader.wgsl @@ -0,0 +1,252 @@ +struct Params { + scissor_mat: mat3x4, + paint_mat: mat3x4, + inner_col: vec4, + outer_col: vec4, + scissor_ext: vec2, + scissor_scale: vec2, + extent: vec2, + radius: f32, + feather: f32, + stroke_mult: f32, + stroke_thr: f32, + tex_type: f32, + _unused_shader_type: f32, + glyph_texture_type: f32, // 0 -> no glyph rendering, 1 -> alpha mask, 2 -> color texture + image_blur_filter_sigma: f32, + image_blur_filter_direction: vec2, + image_blur_filter_coeff: vec3, +} + +override shader_type: i32; +override enable_glyph_texture: bool; +override render_to_texture: bool; + +const SHADER_TYPE_FillGradient: i32 = 0; +const SHADER_TYPE_FillImage: i32 = 1; +const SHADER_TYPE_Stencil: i32 = 2; +const SHADER_TYPE_FillImageGradient: i32 = 3; +const SHADER_TYPE_FilterImage: i32 = 4; +const SHADER_TYPE_FillColor: i32 = 5; +const SHADER_TYPE_TextureCopyUnclipped: i32 = 6; +const SHADER_TYPE_FillColorUnclipped: i32 = 7; + +@group(0) +@binding(0) +var viewSize: vec2; + +@group(1) +@binding(0) +var params: Params; + +struct Vertex { + vertex: vec2, + tcoord: vec2, +} + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) ftcoord: vec2, + @location(1) fpos: vec2, +}; + +@vertex +fn vs_main( + @location(0) vertex: vec2, + @location(1) tcoord: vec2, +) -> VertexOutput { + var result: VertexOutput; + result.ftcoord = tcoord; + result.fpos = vertex; + if (render_to_texture) { + result.position = vec4(2.0 * vertex.x / viewSize.x - 1.0, 2.0 * vertex.y / viewSize.y - 1.0, 0, 1); + } else { + result.position = vec4(2.0 * vertex.x / viewSize.x - 1.0, 1.0 - 2.0 * vertex.y / viewSize.y, 0, 1); + } + return result; +} + +@group(1) +@binding(1) +var image_texture: texture_2d; +@group(1) +@binding(2) +var image_sampler: sampler; + +@group(1) +@binding(3) +var glyph_texture: texture_2d; +@group(1) +@binding(4) +var glyph_sampler: sampler; + + +@fragment +fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { + var result: vec4; + + var strokeAlpha: f32 = 1.0; + if (shader_type != SHADER_TYPE_TextureCopyUnclipped && shader_type != SHADER_TYPE_FillColorUnclipped && shader_type != SHADER_TYPE_FilterImage) { + strokeAlpha = strokeMask(vertex, params); + if (strokeAlpha < params.stroke_thr) { + discard; + } + } + + switch (shader_type) { + case SHADER_TYPE_FillGradient: { + // Gradient + result = renderGradient(vertex, params); + } + case SHADER_TYPE_FillImageGradient: { + // Image-based Gradient; sample a texture using the gradient position. + result = renderImageGradient(vertex, params); + } + case SHADER_TYPE_FillImage: { + // Image + result = renderImage(vertex, params); + } + case SHADER_TYPE_FillColor: { + // Plain color fill + result = params.inner_col; + } + case SHADER_TYPE_TextureCopyUnclipped: { + // Plain texture copy, unclipped + return renderPlainTextureCopy(vertex, params); + } + case SHADER_TYPE_Stencil: { + // Stencil fill + result = vec4(1,1,1,1); + } + case SHADER_TYPE_FilterImage: { + // Filter Image + return renderFilteredImage(vertex, params); + } + case SHADER_TYPE_FillColorUnclipped: { + // Plain color fill + return params.inner_col; + } + default: { + result = vec4(0.0, 0.0, 1.0, 1.0); + } + } + + var scissor: f32 = scissorMask(vertex.fpos, params); + + if (enable_glyph_texture) { + // Textured tris + var mask: vec4 = textureSample(glyph_texture, glyph_sampler, vertex.ftcoord); + + if (params.glyph_texture_type == 1) { + mask = vec4(mask.x); + } else { + result = vec4(1, 1, 1, 1); + mask = vec4(mask.xyz * mask.w, mask.w); + } + + mask *= scissor; + result *= mask; + } else if (shader_type != SHADER_TYPE_Stencil && shader_type != SHADER_TYPE_FilterImage) { + // Not stencil fill + // Combine alpha + result *= strokeAlpha * scissor; + } + + return result; +} + +fn sdroundrect(pt: vec2, ext: vec2, rad: f32) -> f32 { + let ext2: vec2 = ext - vec2(rad,rad); + let d: vec2 = abs(pt) - ext2; + return min(max(d.x,d.y),0.0) + length(max(d, vec2(0.0, 0.0))) - rad; +} + +// Scissoring +fn scissorMask(p: vec2, params: Params) -> f32 { + var sc: vec2 = (abs((params.scissor_mat * vec3(p,1.0)).xy) - params.scissor_ext); + sc = vec2(0.5,0.5) - sc * params.scissor_scale; + return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0); +} + +// Stroke - from [0..1] to clipped pyramid, where the slope is 1px. +fn strokeMask(vertex: VertexOutput, params: Params) -> f32 { + return min(1.0, (1.0-abs(vertex.ftcoord.x*2.0-1.0))*params.stroke_mult) * min(1.0, vertex.ftcoord.y); + // Using this smoothstep preduces maybe better results when combined with fringe_width of 2, but it may look blurrier + // maybe this should be controlled via flag + //return smoothstep(0.0, 1.0, (1.0-abs(vertex.ftcoord.x*2.0-1.0))*params.stroke_mult) * smoothstep(0.0, 1.0, vertex.ftcoord.y); +} + +fn renderGradient(vertex: VertexOutput, params: Params) -> vec4 { + // Calculate gradient color using box gradient + let pt: vec2 = (params.paint_mat * vec3(vertex.fpos, 1.0)).xy; + + let d: f32 = clamp((sdroundrect(pt, params.extent, params.radius) + params.feather*0.5) / params.feather, 0.0, 1.0); + return mix(params.inner_col,params.outer_col,d); +} + +// Image-based Gradient; sample a texture using the gradient position. +fn renderImageGradient(vertex: VertexOutput, params: Params) -> vec4 { + // Calculate gradient color using box gradient + let pt: vec2 = (params.paint_mat * vec3(vertex.fpos, 1.0)).xy; + + let d: f32 = clamp((sdroundrect(pt, params.extent, params.radius) + params.feather*0.5) / params.feather, 0.0, 1.0); + return textureSample(image_texture, image_sampler, vec2(d, 0.0));//mix(innerCol,outerCol,d); +} + +fn renderImage(vertex: VertexOutput, params: Params) -> vec4 { + // Calculate color from texture + let pt: vec2 = (params.paint_mat * vec3(vertex.fpos, 1.0)).xy / params.extent; + + var color: vec4 = textureSample(image_texture, image_sampler, pt); + + if (params.tex_type == 1) { color = vec4(color.xyz * color.w, color.w); } + if (params.tex_type == 2) { color = vec4(color.x); } + + // Apply color tint and alpha. + color *= params.inner_col; + return color; +} + +fn renderPlainTextureCopy(vertex: VertexOutput, params: Params) -> vec4 { + var color: vec4 = textureSample(image_texture, image_sampler, vertex.ftcoord); + + if (params.tex_type == 1) { color = vec4(color.xyz * color.w, color.w); } + if (params.tex_type == 2) { color = vec4(color.x); } + // Apply color tint and alpha. + color *= params.inner_col; + return color; +} + +fn renderFilteredImage(vertex: VertexOutput, params: Params) -> vec4 { + let sampleCount: f32 = ceil(1.5 * params.image_blur_filter_sigma); + + var gaussian_coeff: vec3 = params.image_blur_filter_coeff; + + var color_sum: vec4 = textureSample(image_texture, image_sampler, vertex.fpos.xy / params.extent) * gaussian_coeff.x; + var coefficient_sum: f32 = gaussian_coeff.x; + gaussian_coeff.x *= gaussian_coeff.y; + gaussian_coeff.y *= gaussian_coeff.z; + + for (var i: f32 = 1.0; i <= 12.0; i += 1.) { + // Work around GLES 2.0 limitation of only allowing constant loop indices + // by breaking here. Sigma has an upper bound of 8, imposed on the Rust side. + if (i >= sampleCount) { + break; + } + color_sum += textureSample(image_texture, image_sampler, (vertex.fpos.xy - i * params.image_blur_filter_direction) / params.extent) * gaussian_coeff.x; + color_sum += textureSample(image_texture, image_sampler, (vertex.fpos.xy + i * params.image_blur_filter_direction) / params.extent) * gaussian_coeff.x; + coefficient_sum += 2.0 * gaussian_coeff.x; + + // Compute the coefficients incrementally: + // https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-40-incremental-computation-gaussian + gaussian_coeff.x *= gaussian_coeff.y; + gaussian_coeff.y *= gaussian_coeff.z; + } + + var color: vec4 = color_sum / coefficient_sum; + + if (params.tex_type == 1) { color = vec4(color.xyz * color.w, color.w); } + if (params.tex_type == 2) { color = vec4(color.x); } + + return color; +}