From 5156447346b256e60d96324982daac11a8883a31 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Sun, 5 Mar 2023 11:33:30 +0000 Subject: [PATCH] Make the `with_winit` example run on android (#273) --- README.md | 38 +- examples/headless/src/main.rs | 11 +- examples/scenes/src/lib.rs | 13 +- examples/scenes/src/svg.rs | 106 ++++-- examples/scenes/src/test_scenes.rs | 29 +- examples/with_bevy/src/main.rs | 12 +- examples/with_winit/Cargo.toml | 17 +- examples/with_winit/src/hot_reload.rs | 26 +- examples/with_winit/src/lib.rs | 488 +++++++++++++++++++++++++ examples/with_winit/src/main.rs | 298 +-------------- examples/with_winit/src/multi_touch.rs | 289 +++++++++++++++ src/lib.rs | 38 +- src/render.rs | 1 - src/util.rs | 17 +- 14 files changed, 995 insertions(+), 388 deletions(-) create mode 100644 examples/with_winit/src/lib.rs create mode 100644 examples/with_winit/src/multi_touch.rs diff --git a/README.md b/README.md index 159a5b0..8b0f680 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ It is used as the rendering backend for [Xilem], a UI toolkit. Quickstart to run an example program: ```shell -cargo run -p with_winit +cargo run -p with_winit ``` ## Integrations @@ -72,21 +72,49 @@ This currently draws to a [`wgpu`] `Texture` using `vello`, then uses that textu cargo run -p with_bevy ``` +## Platforms + +We aim to target all environments which can support WebGPU with the [default limits](https://www.w3.org/TR/webgpu/#limits). +We defer to [`wgpu`] for this support. +Other platforms are more tricky, and may require special building/running procedures. + ### Web Because Vello relies heavily on compute shaders, we rely on the emerging WebGPU standard to run on the web. Until browser support becomes widespread, it will probably be necessary to use development browser versions (e.g. Chrome Canary) and explicitly enable WebGPU. -Note: Other examples use the `-p` shorthand, but `cargo-run-wasm` requires the full `--package` to be specified - The following command builds and runs a web version of the [winit demo](#winit). -This uses [`cargo-run-wasm`](https://github.com/rukai/cargo-run-wasm) to build the example for web, and host a local server for it: +This uses [`cargo-run-wasm`](https://github.com/rukai/cargo-run-wasm) to build the example for web, and host a local server for it + +Other examples use the `-p` shorthand, but `cargo-run-wasm` requires the full `--package` to be specified ```shell cargo run_wasm --package with_winit ``` -The web is not currently a primary target for vello, and WebGPU implementations are incomplete, so you might run into issues running this example. +> **Warning** +> The web is not currently a primary target for vello, and WebGPU implementations are incomplete, so you might run into issues running this example. + +### Android + +The [`with_winit`](#winit) example supports running on Android, using [cargo apk](https://crates.io/crates/cargo-apk). + +``` +cargo apk run -p with_winit +``` + +> **Note** +> cargo apk doesn't support running in release mode without configuration. +> See [their crates page docs](https://crates.io/crates/cargo-apk) (around `package.metadata.android.signing.`). +> +> See also [cargo-apk#16](https://github.com/rust-mobile/cargo-apk/issues/16). +> To run in release mode, you must add the following to `examples/with_winit/Cargo.toml` (changing `$HOME` to your home directory): + +``` +[package.metadata.android.signing.release] +path = "$HOME/.android/debug.keystore" +keystore_password = "android" +``` ## Community diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs index c145d06..cfe4f2b 100644 --- a/examples/headless/src/main.rs +++ b/examples/headless/src/main.rs @@ -11,7 +11,7 @@ use vello::{ block_on_wgpu, kurbo::{Affine, Vec2}, util::RenderContext, - Scene, SceneBuilder, SceneFragment, + RendererOptions, Scene, SceneBuilder, SceneFragment, }; use wgpu::{ BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer, @@ -86,8 +86,13 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { let device_handle = &mut context.devices[device_id]; let device = &device_handle.device; let queue = &device_handle.queue; - let mut renderer = vello::Renderer::new(&device) - .or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?; + let mut renderer = vello::Renderer::new( + &device, + &RendererOptions { + surface_format: None, + }, + ) + .or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?; let mut fragment = SceneFragment::new(); let mut builder = SceneBuilder::for_fragment(&mut fragment); let example_scene = &mut scenes.scenes[index]; diff --git a/examples/scenes/src/lib.rs b/examples/scenes/src/lib.rs index ea84dc6..242cbe1 100644 --- a/examples/scenes/src/lib.rs +++ b/examples/scenes/src/lib.rs @@ -1,6 +1,5 @@ pub mod download; mod simple_text; -#[cfg(not(target_arch = "wasm32"))] mod svg; mod test_scenes; use std::path::PathBuf; @@ -9,7 +8,6 @@ use anyhow::{anyhow, Result}; use clap::{Args, Subcommand}; use download::Download; pub use simple_text::SimpleText; -#[cfg(not(target_arch = "wasm32"))] pub use svg::{default_scene, scene_from_files}; pub use test_scenes::test_scenes; @@ -66,16 +64,19 @@ enum Command { impl Arguments { pub fn select_scene_set( &self, - command: impl FnOnce() -> clap::Command, + #[allow(unused)] command: impl FnOnce() -> clap::Command, ) -> Result> { if let Some(command) = &self.command { command.action()?; Ok(None) } else { - // There is no file access on WASM - #[cfg(target_arch = "wasm32")] + // There is no file access on WASM, and on Android we haven't set up the assets + // directory. + // TODO: Upload the assets directory on Android + // Therefore, only render the `test_scenes` (including one SVG example) + #[cfg(any(target_arch = "wasm32", target_os = "android"))] return Ok(Some(test_scenes())); - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] if self.test_scenes { Ok(test_scenes()) } else if let Some(svgs) = &self.svgs { diff --git a/examples/scenes/src/svg.rs b/examples/scenes/src/svg.rs index 9f4f5c0..d28c881 100644 --- a/examples/scenes/src/svg.rs +++ b/examples/scenes/src/svg.rs @@ -8,7 +8,7 @@ use anyhow::{Ok, Result}; use vello::{kurbo::Vec2, SceneBuilder, SceneFragment}; use vello_svg::usvg; -use crate::{ExampleScene, SceneSet}; +use crate::{ExampleScene, SceneParams, SceneSet}; pub fn scene_from_files(files: &[PathBuf]) -> Result { scene_from_files_inner(files, || ()) @@ -73,32 +73,92 @@ fn example_scene_of(file: PathBuf) -> ExampleScene { .file_stem() .map(|it| it.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".to_string()); - let name_stored = name.clone(); - let mut cached_scene = None; ExampleScene { - function: Box::new(move |builder, params| { - let (scene_frag, resolution) = cached_scene.get_or_insert_with(|| { - let start = Instant::now(); - let contents = std::fs::read_to_string(&file).expect("failed to read svg file"); - let svg = usvg::Tree::from_str(&contents, &usvg::Options::default()) - .expect("failed to parse svg file"); - eprintln!( - "Parsing SVG {name_stored} took {:?} (file `{file:?}`", - start.elapsed() - ); - let mut new_scene = SceneFragment::new(); - let mut builder = SceneBuilder::for_fragment(&mut new_scene); - vello_svg::render_tree(&mut builder, &svg); - let resolution = Vec2::new(svg.size.width(), svg.size.height()); - // TODO: Handle svg.view_box - (new_scene, resolution) - }); - builder.append(&scene_frag, None); - params.resolution = Some(*resolution); - }), + function: Box::new(svg_function_of(name.clone(), move || { + let contents = std::fs::read_to_string(&file).expect("failed to read svg file"); + contents + })), config: crate::SceneConfig { animated: false, name, }, } } + +pub fn svg_function_of>( + name: String, + contents: impl FnOnce() -> R + Send + 'static, +) -> impl FnMut(&mut SceneBuilder, &mut SceneParams) { + fn render_svg_contents(name: &str, contents: &str) -> (SceneFragment, Vec2) { + let start = Instant::now(); + let svg = usvg::Tree::from_str(&contents, &usvg::Options::default()) + .expect("failed to parse svg file"); + let mut new_scene = SceneFragment::new(); + let mut builder = SceneBuilder::for_fragment(&mut new_scene); + vello_svg::render_tree(&mut builder, &svg); + let resolution = Vec2::new(svg.size.width(), svg.size.height()); + eprintln!("Rendered svg {name} in {:?}", start.elapsed()); + (new_scene, resolution) + } + let mut cached_scene = None; + #[cfg(not(target_arch = "wasm32"))] + let (tx, rx) = std::sync::mpsc::channel(); + #[cfg(not(target_arch = "wasm32"))] + let mut tx = Some(tx); + #[cfg(not(target_arch = "wasm32"))] + let mut has_started_parse = false; + let mut contents = Some(contents); + move |builder, params| { + if let Some((scene_frag, resolution)) = cached_scene.as_mut() { + builder.append(&scene_frag, None); + params.resolution = Some(*resolution); + return; + } + #[cfg(target_arch = "wasm32")] + { + let contents = contents.take().unwrap(); + let contents = contents(); + let (scene_frag, resolution) = render_svg_contents(&name, contents.as_ref()); + builder.append(&scene_frag, None); + params.resolution = Some(resolution); + cached_scene = Some((scene_frag, resolution)) + } + #[cfg(not(target_arch = "wasm32"))] + { + let mut timeout = std::time::Duration::from_millis(10); + if !has_started_parse { + has_started_parse = true; + // Prefer jank over loading screen for first time + timeout = std::time::Duration::from_millis(400); + let tx = tx.take().unwrap(); + let contents = contents.take().unwrap(); + let name = name.clone(); + std::thread::spawn(move || { + let contents = contents(); + tx.send(render_svg_contents(&name, contents.as_ref())) + .unwrap(); + }); + } + let recv = rx.recv_timeout(timeout); + use std::sync::mpsc::RecvTimeoutError; + match recv { + Result::Ok((scene_frag, resolution)) => { + builder.append(&scene_frag, None); + params.resolution = Some(resolution); + cached_scene = Some((scene_frag, resolution)) + } + Err(RecvTimeoutError::Timeout) => params.text.add( + builder, + None, + 48., + None, + vello::kurbo::Affine::translate((110.0, 600.0)), + &format!("Loading {name}"), + ), + Err(RecvTimeoutError::Disconnected) => { + panic!() + } + } + }; + } +} diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 46ede47..c2dec2a 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -34,7 +34,7 @@ pub fn test_scenes() -> SceneSet { scene!(labyrinth), scene!(base_color_test: animated), ]; - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", target_os = "android"))] scenes.push(ExampleScene { config: SceneConfig { animated: false, @@ -246,28 +246,13 @@ fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { } } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", target_os = "android"))] fn included_tiger() -> impl FnMut(&mut SceneBuilder, &mut SceneParams) { - use vello::kurbo::Vec2; - use vello_svg::usvg; - let mut cached_scene = None; - move |builder, params| { - let (scene_frag, resolution) = cached_scene.get_or_insert_with(|| { - let contents = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../assets/Ghostscript_Tiger.svg" - )); - let svg = usvg::Tree::from_str(&contents, &usvg::Options::default()) - .expect("failed to parse svg file"); - let mut new_scene = SceneFragment::new(); - let mut builder = SceneBuilder::for_fragment(&mut new_scene); - vello_svg::render_tree(&mut builder, &svg); - let resolution = Vec2::new(svg.size.width(), svg.size.height()); - (new_scene, resolution) - }); - builder.append(&scene_frag, None); - params.resolution = Some(*resolution); - } + let contents = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../assets/Ghostscript_Tiger.svg" + )); + crate::svg::svg_function_of("Ghostscript Tiger".to_string(), move || contents) } // Support functions diff --git a/examples/with_bevy/src/main.rs b/examples/with_bevy/src/main.rs index f98ce73..6fcc25e 100644 --- a/examples/with_bevy/src/main.rs +++ b/examples/with_bevy/src/main.rs @@ -1,7 +1,7 @@ use bevy::render::RenderSet; use vello::kurbo::{Affine, Point, Rect}; use vello::peniko::{Color, Fill, Gradient, Stroke}; -use vello::{Renderer, Scene, SceneBuilder, SceneFragment}; +use vello::{Renderer, RendererOptions, Scene, SceneBuilder, SceneFragment}; use bevy::{ prelude::*, @@ -22,7 +22,15 @@ struct VelloRenderer(Renderer); impl FromWorld for VelloRenderer { fn from_world(world: &mut World) -> Self { let device = world.get_resource::().unwrap(); - VelloRenderer(Renderer::new(device.wgpu_device()).unwrap()) + VelloRenderer( + Renderer::new( + device.wgpu_device(), + &RendererOptions { + surface_format: None, + }, + ) + .unwrap(), + ) } } diff --git a/examples/with_winit/Cargo.toml b/examples/with_winit/Cargo.toml index 9f2b926..3c62d8e 100644 --- a/examples/with_winit/Cargo.toml +++ b/examples/with_winit/Cargo.toml @@ -10,6 +10,15 @@ repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "with_winit" +crate-type = ["cdylib", "lib"] + +[[bin]] +# Stop the PDB collision warning on windows +name = "with_winit_bin" +path = "src/main.rs" + [dependencies] vello = { path = "../../", features = ["buffer_labels"] } scenes = { path = "../scenes" } @@ -20,11 +29,17 @@ wgpu = { workspace = true } winit = "0.28.1" pollster = "0.2.5" env_logger = "0.10.0" +log = "0.4.17" -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +[target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies] vello = { path = "../../", features = ["hot_reload"] } notify-debouncer-mini = "0.2.1" + +[target.'cfg(target_os = "android")'.dependencies] +winit = { version = "0.28", features = ["android-native-activity"] } +android_logger = "0.12.0" + [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.7" console_log = "0.2" diff --git a/examples/with_winit/src/hot_reload.rs b/examples/with_winit/src/hot_reload.rs index 2cd3d3f..dbfc6f4 100644 --- a/examples/with_winit/src/hot_reload.rs +++ b/examples/with_winit/src/hot_reload.rs @@ -1,8 +1,9 @@ use std::{path::Path, time::Duration}; +use anyhow::Result; use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult}; -pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> impl Sized { +pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> Result { let mut debouncer = new_debouncer( Duration::from_millis(500), None, @@ -10,19 +11,14 @@ pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> Ok(_) => f().unwrap(), Err(errors) => errors.iter().for_each(|e| println!("Error {:?}", e)), }, - ) - .unwrap(); + )?; - debouncer - .watcher() - .watch( - &Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../shader") - .canonicalize() - .unwrap(), - // We currently don't support hot reloading the imports, so don't recurse into there - RecursiveMode::NonRecursive, - ) - .expect("Could watch shaders directory"); - debouncer + debouncer.watcher().watch( + &Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../shader") + .canonicalize()?, + // We currently don't support hot reloading the imports, so don't recurse into there + RecursiveMode::NonRecursive, + )?; + Ok(debouncer) } diff --git a/examples/with_winit/src/lib.rs b/examples/with_winit/src/lib.rs new file mode 100644 index 0000000..d5d9bbe --- /dev/null +++ b/examples/with_winit/src/lib.rs @@ -0,0 +1,488 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Also licensed under MIT license, at your choice. + +use std::collections::HashSet; +use std::time::Instant; + +use anyhow::Result; +use clap::{CommandFactory, Parser}; +use scenes::{SceneParams, SceneSet, SimpleText}; +use vello::peniko::Color; +use vello::util::RenderSurface; +use vello::{ + kurbo::{Affine, Vec2}, + util::RenderContext, + Renderer, Scene, SceneBuilder, +}; +use vello::{RendererOptions, SceneFragment}; + +use winit::{ + event_loop::{EventLoop, EventLoopBuilder}, + window::Window, +}; + +#[cfg(not(any(target_arch = "wasm32", target_os = "android")))] +mod hot_reload; +mod multi_touch; + +#[derive(Parser, Debug)] +#[command(about, long_about = None, bin_name="cargo run -p with_winit --")] +struct Args { + /// Which scene (index) to start on + /// Switch between scenes with left and right arrow keys + #[arg(long)] + scene: Option, + #[command(flatten)] + args: scenes::Arguments, +} + +struct RenderState { + // SAFETY: We MUST drop the surface before the `window`, so the fields + // must be in this order + surface: RenderSurface, + window: Window, +} + +fn run( + event_loop: EventLoop, + args: Args, + mut scenes: SceneSet, + render_cx: RenderContext, + #[cfg(target_arch = "wasm32")] render_state: RenderState, +) { + use winit::{event::*, event_loop::ControlFlow}; + let mut renderers: Vec> = vec![]; + #[cfg(not(target_arch = "wasm32"))] + let mut render_cx = render_cx; + #[cfg(not(target_arch = "wasm32"))] + let mut render_state = None::; + // The design of `RenderContext` forces delayed renderer initialisation to + // not work on wasm, as WASM futures effectively must be 'static. + // Otherwise, this could work by sending the result to event_loop.proxy + // instead of blocking + #[cfg(target_arch = "wasm32")] + let mut render_state = { + renderers.resize_with(render_cx.devices.len(), || None); + let id = render_state.surface.dev_id; + renderers[id] = Some( + Renderer::new( + &render_cx.devices[id].device, + &RendererOptions { + surface_format: Some(render_state.surface.format), + }, + ) + .expect("Could create renderer"), + ); + Some(render_state) + }; + // Whilst suspended, we drop `render_state`, but need to keep the same window. + // If render_state exists, we must store the window in it, to maintain drop order + #[cfg(not(target_arch = "wasm32"))] + let mut cached_window = None; + + let mut scene = Scene::new(); + let mut fragment = SceneFragment::new(); + let mut simple_text = SimpleText::new(); + let start = Instant::now(); + + let mut touch_state = multi_touch::TouchState::new(); + // navigation_fingers are fingers which are used in the navigation 'zone' at the bottom + // of the screen. This ensures that one press on the screen doesn't have multiple actions + let mut navigation_fingers = HashSet::new(); + let mut transform = Affine::IDENTITY; + let mut mouse_down = false; + let mut prior_position: Option = None; + // We allow looping left and right through the scenes, so use a signed index + let mut scene_ix: i32 = 0; + if let Some(set_scene) = args.scene { + scene_ix = set_scene; + } + let mut prev_scene_ix = scene_ix - 1; + // _event_loop is used on non-wasm platforms to create new windows + event_loop.run(move |event, _event_loop, control_flow| match event { + Event::WindowEvent { + ref event, + window_id, + } => { + let Some(render_state) = &mut render_state else { return }; + if render_state.window.id() != window_id { + return; + } + match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { input, .. } => { + if input.state == ElementState::Pressed { + match input.virtual_keycode { + Some(VirtualKeyCode::Left) => scene_ix = scene_ix.saturating_sub(1), + Some(VirtualKeyCode::Right) => scene_ix = scene_ix.saturating_add(1), + Some(key @ VirtualKeyCode::Q) | Some(key @ VirtualKeyCode::E) => { + if let Some(prior_position) = prior_position { + let is_clockwise = key == VirtualKeyCode::E; + let angle = if is_clockwise { -0.05 } else { 0.05 }; + transform = Affine::translate(prior_position) + * Affine::rotate(angle) + * Affine::translate(-prior_position) + * transform; + } + } + Some(VirtualKeyCode::Escape) => { + *control_flow = ControlFlow::Exit; + } + _ => {} + } + } + } + WindowEvent::Touch(touch) => { + match touch.phase { + TouchPhase::Started => { + // We reserve the bottom third of the screen for navigation + // This also prevents strange effects whilst using the navigation gestures on Android + // TODO: How do we know what the client area is? Winit seems to just give us the + // full screen + // TODO: Render a display of the navigation regions. We don't do + // this currently because we haven't researched how to determine when we're + // in a touch context (i.e. Windows/Linux/MacOS with a touch screen could + // also be using mouse/keyboard controls) + // Note that winit's rendering is y-down + if touch.location.y + > render_state.surface.config.height as f64 * 2. / 3. + { + navigation_fingers.insert(touch.id); + // The left third of the navigation zone navigates backwards + if touch.location.x < render_state.surface.config.width as f64 / 3. + { + scene_ix = scene_ix.saturating_sub(1); + } else if touch.location.x + > 2. * render_state.surface.config.width as f64 / 3. + { + scene_ix = scene_ix.saturating_add(1); + } + } + } + TouchPhase::Ended | TouchPhase::Cancelled => { + // We intentionally ignore the result here + navigation_fingers.remove(&touch.id); + } + TouchPhase::Moved => (), + } + // See documentation on navigation_fingers + if !navigation_fingers.contains(&touch.id) { + touch_state.add_event(touch); + } + } + WindowEvent::Resized(size) => { + render_cx.resize_surface(&mut render_state.surface, size.width, size.height); + render_state.window.request_redraw(); + } + WindowEvent::MouseInput { state, button, .. } => { + if button == &MouseButton::Left { + mouse_down = state == &ElementState::Pressed; + } + } + WindowEvent::MouseWheel { delta, .. } => { + const BASE: f64 = 1.05; + const PIXELS_PER_LINE: f64 = 20.0; + + if let Some(prior_position) = prior_position { + let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta { + delta.y / PIXELS_PER_LINE + } else if let MouseScrollDelta::LineDelta(_, y) = delta { + *y as f64 + } else { + 0.0 + }; + transform = Affine::translate(prior_position) + * Affine::scale(BASE.powf(exponent)) + * Affine::translate(-prior_position) + * transform; + } else { + eprintln!("Scrolling without mouse in window; this shouldn't be possible"); + } + } + WindowEvent::CursorLeft { .. } => { + prior_position = None; + } + WindowEvent::CursorMoved { position, .. } => { + let position = Vec2::new(position.x, position.y); + if mouse_down { + if let Some(prior) = prior_position { + transform = Affine::translate(position - prior) * transform; + } + } + prior_position = Some(position); + } + _ => {} + } + } + Event::MainEventsCleared => { + touch_state.end_frame(); + let touch_info = touch_state.info(); + if let Some(touch_info) = touch_info { + let centre = Vec2::new(touch_info.zoom_centre.x, touch_info.zoom_centre.y); + transform = Affine::translate(touch_info.translation_delta) + * Affine::translate(centre) + * Affine::scale(touch_info.zoom_delta) + * Affine::rotate(touch_info.rotation_delta) + * Affine::translate(-centre) + * transform; + } + + if let Some(render_state) = &mut render_state { + render_state.window.request_redraw(); + } + } + Event::RedrawRequested(_) => { + let Some(render_state) = &mut render_state else { return }; + let width = render_state.surface.config.width; + let height = render_state.surface.config.height; + let device_handle = &render_cx.devices[render_state.surface.dev_id]; + + // Allow looping forever + scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32); + let example_scene = &mut scenes.scenes[scene_ix as usize]; + if prev_scene_ix != scene_ix { + transform = Affine::IDENTITY; + prev_scene_ix = scene_ix; + render_state + .window + .set_title(&format!("Vello demo - {}", example_scene.config.name)); + } + let mut builder = SceneBuilder::for_fragment(&mut fragment); + let mut scene_params = SceneParams { + time: start.elapsed().as_secs_f64(), + text: &mut simple_text, + resolution: None, + base_color: None, + }; + (example_scene.function)(&mut builder, &mut scene_params); + builder.finish(); + + // If the user specifies a base color in the CLI we use that. Otherwise we use any + // color specified by the scene. The default is black. + let render_params = vello::RenderParams { + base_color: args + .args + .base_color + .or(scene_params.base_color) + .unwrap_or(Color::BLACK), + width, + height, + }; + let mut builder = SceneBuilder::for_scene(&mut scene); + let mut transform = transform; + if let Some(resolution) = scene_params.resolution { + // Automatically scale the rendering to fill as much of the window as possible + // TODO: Apply svg view_box, somehow + let factor = Vec2::new(width as f64, height as f64); + let scale_factor = (factor.x / resolution.x).min(factor.y / resolution.y); + transform = transform * Affine::scale(scale_factor); + } + builder.append(&fragment, Some(transform)); + builder.finish(); + let surface_texture = render_state + .surface + .surface + .get_current_texture() + .expect("failed to get surface texture"); + #[cfg(not(target_arch = "wasm32"))] + { + vello::block_on_wgpu( + &device_handle.device, + renderers[render_state.surface.dev_id] + .as_mut() + .unwrap() + .render_to_surface_async( + &device_handle.device, + &device_handle.queue, + &scene, + &surface_texture, + &render_params, + ), + ) + .expect("failed to render to surface"); + } + // Note: in the wasm case, we're currently not running the robust + // pipeline, as it requires more async wiring for the readback. + #[cfg(target_arch = "wasm32")] + renderers[render_state.surface.dev_id] + .as_mut() + .unwrap() + .render_to_surface( + &device_handle.device, + &device_handle.queue, + &scene, + &surface_texture, + &render_params, + ) + .expect("failed to render to surface"); + surface_texture.present(); + device_handle.device.poll(wgpu::Maintain::Poll); + } + Event::UserEvent(event) => match event { + #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] + UserEvent::HotReload => { + let Some(render_state) = &mut render_state else { return }; + let device_handle = &render_cx.devices[render_state.surface.dev_id]; + eprintln!("==============\nReloading shaders"); + let start = Instant::now(); + let result = renderers[render_state.surface.dev_id] + .as_mut() + .unwrap() + .reload_shaders(&device_handle.device); + // We know that the only async here (`pop_error_scope`) is actually sync, so blocking is fine + match pollster::block_on(result) { + Ok(_) => eprintln!("Reloading took {:?}", start.elapsed()), + Err(e) => eprintln!("Failed to reload shaders because of {e}"), + } + } + }, + Event::Suspended => { + eprintln!("Suspending"); + #[cfg(not(target_arch = "wasm32"))] + // When we suspend, we need to remove the `wgpu` Surface + if let Some(render_state) = render_state.take() { + cached_window = Some(render_state.window); + } + *control_flow = ControlFlow::Wait; + } + Event::Resumed => { + #[cfg(target_arch = "wasm32")] + {} + #[cfg(not(target_arch = "wasm32"))] + { + let Option::None = render_state else { return }; + let window = cached_window + .take() + .unwrap_or_else(|| create_window(_event_loop)); + let size = window.inner_size(); + let surface_future = render_cx.create_surface(&window, size.width, size.height); + // We need to block here, in case a Suspended event appeared + let surface = pollster::block_on(surface_future); + render_state = { + let render_state = RenderState { window, surface }; + renderers.resize_with(render_cx.devices.len(), || None); + let id = render_state.surface.dev_id; + renderers[id].get_or_insert_with(|| { + eprintln!("Creating renderer {id}"); + Renderer::new( + &render_cx.devices[id].device, + &RendererOptions { + surface_format: Some(render_state.surface.format), + }, + ) + .expect("Could create renderer") + }); + Some(render_state) + }; + *control_flow = ControlFlow::Poll; + } + } + _ => {} + }); +} + +fn create_window(event_loop: &winit::event_loop::EventLoopWindowTarget) -> Window { + use winit::{dpi::LogicalSize, window::WindowBuilder}; + WindowBuilder::new() + .with_inner_size(LogicalSize::new(1044, 800)) + .with_resizable(true) + .with_title("Vello demo") + .build(&event_loop) + .unwrap() +} + +#[derive(Debug)] +enum UserEvent { + #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] + HotReload, +} + +pub fn main() -> Result<()> { + // TODO: initializing both env_logger and console_logger fails on wasm. + // Figure out a more principled approach. + #[cfg(not(target_arch = "wasm32"))] + env_logger::init(); + let args = Args::parse(); + let scenes = args.args.select_scene_set(|| Args::command())?; + if let Some(scenes) = scenes { + let event_loop = EventLoopBuilder::::with_user_event().build(); + #[allow(unused_mut)] + let mut render_cx = RenderContext::new().unwrap(); + #[cfg(not(target_arch = "wasm32"))] + { + #[cfg(not(target_os = "android"))] + let proxy = event_loop.create_proxy(); + #[cfg(not(target_os = "android"))] + let _keep = hot_reload::hot_reload(move || { + proxy.send_event(UserEvent::HotReload).ok().map(drop) + }); + + run(event_loop, args, scenes, render_cx); + } + #[cfg(target_arch = "wasm32")] + { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init().expect("could not initialize logger"); + use winit::platform::web::WindowExtWebSys; + let window = create_window(&event_loop); + // On wasm, append the canvas to the document body + let canvas = window.canvas(); + canvas.set_width(1044); + canvas.set_height(800); + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) + .expect("couldn't append canvas to document body"); + wasm_bindgen_futures::spawn_local(async move { + let size = window.inner_size(); + let surface = render_cx + .create_surface(&window, size.width, size.height) + .await; + let render_state = RenderState { window, surface }; + // No error handling here; if the event loop has finished, we don't need to send them the surface + run(event_loop, args, scenes, render_cx, render_state); + }); + } + } + Ok(()) +} + +#[cfg(target_os = "android")] +use winit::platform::android::activity::AndroidApp; + +#[cfg(target_os = "android")] +#[no_mangle] +fn android_main(app: AndroidApp) { + use winit::platform::android::EventLoopBuilderExtAndroid; + + android_logger::init_once( + android_logger::Config::default().with_max_level(log::LevelFilter::Warn), + ); + + let event_loop = EventLoopBuilder::with_user_event() + .with_android_app(app) + .build(); + let args = Args::parse(); + let scenes = args + .args + .select_scene_set(|| Args::command()) + .unwrap() + .unwrap(); + let render_cx = RenderContext::new().unwrap(); + + run(event_loop, args, scenes, render_cx); +} diff --git a/examples/with_winit/src/main.rs b/examples/with_winit/src/main.rs index dc3b202..9ca2e3c 100644 --- a/examples/with_winit/src/main.rs +++ b/examples/with_winit/src/main.rs @@ -1,301 +1,5 @@ -// Copyright 2022 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Also licensed under MIT license, at your choice. - -use std::time::Instant; - use anyhow::Result; -use clap::{CommandFactory, Parser}; -use scenes::{SceneParams, SceneSet, SimpleText}; -use vello::SceneFragment; -use vello::{ - block_on_wgpu, - kurbo::{Affine, Vec2}, - peniko::Color, - util::RenderContext, - Renderer, Scene, SceneBuilder, -}; - -use winit::{ - event_loop::{EventLoop, EventLoopBuilder}, - window::Window, -}; - -#[cfg(not(target_arch = "wasm32"))] -mod hot_reload; - -#[derive(Parser, Debug)] -#[command(about, long_about = None, bin_name="cargo run -p with_winit --")] -struct Args { - /// Path to the svg file to render. If not set, the GhostScript Tiger will be rendered - #[arg(long)] - #[cfg(not(target_arch = "wasm32"))] - svg: Option, - /// When rendering an svg, what scale to use - #[arg(long)] - scale: Option, - /// Which scene (index) to start on - /// Switch between scenes with left and right arrow keys - #[arg(long)] - scene: Option, - #[command(flatten)] - args: scenes::Arguments, -} - -async fn run(event_loop: EventLoop, window: Window, args: Args, mut scenes: SceneSet) { - use winit::{event::*, event_loop::ControlFlow}; - let mut render_cx = RenderContext::new().unwrap(); - let size = window.inner_size(); - let mut surface = render_cx - .create_surface(&window, size.width, size.height) - .await; - let device_handle = &render_cx.devices[surface.dev_id]; - let mut renderer = Renderer::new(&device_handle.device).unwrap(); - let mut scene = Scene::new(); - let mut fragment = SceneFragment::new(); - let mut simple_text = SimpleText::new(); - let start = Instant::now(); - - let mut transform = Affine::IDENTITY; - let mut mouse_down = false; - let mut prior_position: Option = None; - // We allow looping left and right through the scenes, so use a signed index - let mut scene_ix: i32 = 0; - if let Some(set_scene) = args.scene { - scene_ix = set_scene; - } - let mut prev_scene_ix = scene_ix - 1; - event_loop.run(move |event, _, control_flow| match event { - Event::WindowEvent { - ref event, - window_id, - } if window_id == window.id() => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::KeyboardInput { input, .. } => { - if input.state == ElementState::Pressed { - match input.virtual_keycode { - Some(VirtualKeyCode::Left) => scene_ix = scene_ix.saturating_sub(1), - Some(VirtualKeyCode::Right) => scene_ix = scene_ix.saturating_add(1), - Some(key @ VirtualKeyCode::Q) | Some(key @ VirtualKeyCode::E) => { - if let Some(prior_position) = prior_position { - let is_clockwise = key == VirtualKeyCode::E; - let angle = if is_clockwise { -0.05 } else { 0.05 }; - transform = Affine::translate(prior_position) - * Affine::rotate(angle) - * Affine::translate(-prior_position) - * transform; - } - } - Some(VirtualKeyCode::Escape) => { - *control_flow = ControlFlow::Exit; - } - _ => {} - } - } - } - WindowEvent::Resized(size) => { - render_cx.resize_surface(&mut surface, size.width, size.height); - window.request_redraw(); - } - WindowEvent::MouseInput { state, button, .. } => { - if button == &MouseButton::Left { - mouse_down = state == &ElementState::Pressed; - } - } - WindowEvent::MouseWheel { delta, .. } => { - const BASE: f64 = 1.05; - const PIXELS_PER_LINE: f64 = 20.0; - - if let Some(prior_position) = prior_position { - let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta { - delta.y / PIXELS_PER_LINE - } else if let MouseScrollDelta::LineDelta(_, y) = delta { - *y as f64 - } else { - 0.0 - }; - transform = Affine::translate(prior_position) - * Affine::scale(BASE.powf(exponent)) - * Affine::translate(-prior_position) - * transform; - } else { - eprintln!("Scrolling without mouse in window; this shouldn't be possible"); - } - } - WindowEvent::CursorLeft { .. } => { - prior_position = None; - } - WindowEvent::CursorMoved { position, .. } => { - let position = Vec2::new(position.x, position.y); - if mouse_down { - if let Some(prior) = prior_position { - transform = Affine::translate(position - prior) * transform; - } - } - prior_position = Some(position); - } - _ => {} - }, - Event::MainEventsCleared => { - window.request_redraw(); - } - Event::RedrawRequested(_) => { - let width = surface.config.width; - let height = surface.config.height; - let device_handle = &render_cx.devices[surface.dev_id]; - - // Allow looping forever - scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32); - let example_scene = &mut scenes.scenes[scene_ix as usize]; - if prev_scene_ix != scene_ix { - transform = Affine::IDENTITY; - prev_scene_ix = scene_ix; - window.set_title(&format!("Vello demo - {}", example_scene.config.name)); - } - let mut builder = SceneBuilder::for_fragment(&mut fragment); - let mut scene_params = SceneParams { - time: start.elapsed().as_secs_f64(), - text: &mut simple_text, - resolution: None, - base_color: None, - }; - (example_scene.function)(&mut builder, &mut scene_params); - builder.finish(); - - // If the user specifies a base color in the CLI we use that. Otherwise we use any - // color specified by the scene. The default is black. - let render_params = vello::RenderParams { - base_color: args - .args - .base_color - .or(scene_params.base_color) - .unwrap_or(Color::BLACK), - width, - height, - }; - let mut builder = SceneBuilder::for_scene(&mut scene); - let mut transform = transform; - if let Some(resolution) = scene_params.resolution { - let factor = Vec2::new(surface.config.width as f64, surface.config.height as f64); - let scale_factor = (factor.x / resolution.x).min(factor.y / resolution.y); - transform = transform * Affine::scale(scale_factor); - } - builder.append(&fragment, Some(transform)); - builder.finish(); - let surface_texture = surface - .surface - .get_current_texture() - .expect("failed to get surface texture"); - #[cfg(not(target_arch = "wasm32"))] - { - block_on_wgpu( - &device_handle.device, - renderer.render_to_surface_async( - &device_handle.device, - &device_handle.queue, - &scene, - &surface_texture, - &render_params, - ), - ) - .expect("failed to render to surface"); - } - // Note: in the wasm case, we're currently not running the robust - // pipeline, as it requires more async wiring for the readback. - #[cfg(target_arch = "wasm32")] - renderer - .render_to_surface( - &device_handle.device, - &device_handle.queue, - &scene, - &surface_texture, - &render_params, - ) - .expect("failed to render to surface"); - surface_texture.present(); - device_handle.device.poll(wgpu::Maintain::Poll); - } - Event::UserEvent(event) => match event { - #[cfg(not(target_arch = "wasm32"))] - UserEvent::HotReload => { - let device_handle = &render_cx.devices[surface.dev_id]; - eprintln!("==============\nReloading shaders"); - let start = Instant::now(); - let result = renderer.reload_shaders(&device_handle.device); - // We know that the only async here is actually sync, so we just block - match pollster::block_on(result) { - Ok(_) => eprintln!("Reloading took {:?}", start.elapsed()), - Err(e) => eprintln!("Failed to reload shaders because of {e}"), - } - } - }, - _ => {} - }); -} - -enum UserEvent { - #[cfg(not(target_arch = "wasm32"))] - HotReload, -} fn main() -> Result<()> { - // TODO: initializing both env_logger and console_logger fails on wasm. - // Figure out a more principled approach. - #[cfg(not(target_arch = "wasm32"))] - env_logger::init(); - let args = Args::parse(); - let scenes = args.args.select_scene_set(|| Args::command())?; - if let Some(scenes) = scenes { - #[cfg(not(target_arch = "wasm32"))] - { - use winit::{dpi::LogicalSize, window::WindowBuilder}; - let event_loop = EventLoopBuilder::::with_user_event().build(); - - let proxy = event_loop.create_proxy(); - let _keep = hot_reload::hot_reload(move || { - proxy.send_event(UserEvent::HotReload).ok().map(drop) - }); - - let window = WindowBuilder::new() - .with_inner_size(LogicalSize::new(1044, 800)) - .with_resizable(true) - .with_title("Vello demo") - .build(&event_loop) - .unwrap(); - pollster::block_on(run(event_loop, window, args, scenes)); - } - #[cfg(target_arch = "wasm32")] - { - let event_loop = EventLoopBuilder::::with_user_event().build(); - let window = winit::window::Window::new(&event_loop).unwrap(); - - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - console_log::init().expect("could not initialize logger"); - use winit::platform::web::WindowExtWebSys; - - // On wasm, append the canvas to the document body - let canvas = window.canvas(); - canvas.set_width(1044); - canvas.set_height(800); - web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.body()) - .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) - .expect("couldn't append canvas to document body"); - wasm_bindgen_futures::spawn_local(run(event_loop, window, args, scenes)); - } - } - Ok(()) + with_winit::main() } diff --git a/examples/with_winit/src/multi_touch.rs b/examples/with_winit/src/multi_touch.rs new file mode 100644 index 0000000..14d5bcb --- /dev/null +++ b/examples/with_winit/src/multi_touch.rs @@ -0,0 +1,289 @@ +/// Adapted from https://github.com/emilk/egui/blob/212656f3fc6b931b21eaad401e5cec2b0da93baa/crates/egui/src/input_state/touch_state.rs +use std::{collections::BTreeMap, fmt::Debug}; + +use vello::kurbo::{Point, Vec2}; +use winit::event::{Touch, TouchPhase}; + +/// All you probably need to know about a multi-touch gesture. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct MultiTouchInfo { + /// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no + /// [`MultiTouchInfo`] is created. + pub num_touches: usize, + + /// Proportional zoom factor (pinch gesture). + /// * `zoom = 1`: no change + /// * `zoom < 1`: pinch together + /// * `zoom > 1`: pinch spread + pub zoom_delta: f64, + + /// 2D non-proportional zoom factor (pinch gesture). + /// + /// For horizontal pinches, this will return `[z, 1]`, + /// for vertical pinches this will return `[1, z]`, + /// and otherwise this will return `[z, z]`, + /// where `z` is the zoom factor: + /// * `zoom = 1`: no change + /// * `zoom < 1`: pinch together + /// * `zoom > 1`: pinch spread + pub zoom_delta_2d: Vec2, + + /// Rotation in radians. Moving fingers around each other will change this value. This is a + /// relative value, comparing the orientation of fingers in the current frame with the previous + /// frame. If all fingers are resting, this value is `0.0`. + pub rotation_delta: f64, + + /// Relative movement (comparing previous frame and current frame) of the average position of + /// all touch points. Without movement this value is `Vec2::ZERO`. + /// + /// Note that this may not necessarily be measured in screen points (although it _will_ be for + /// most mobile devices). In general (depending on the touch device), touch coordinates cannot + /// be directly mapped to the screen. A touch always is considered to start at the position of + /// the pointer, but touch movement is always measured in the units delivered by the device, + /// and may depend on hardware and system settings. + pub translation_delta: Vec2, + pub zoom_centre: Point, +} + +/// The current state (for a specific touch device) of touch events and gestures. +#[derive(Clone)] +pub(crate) struct TouchState { + /// Active touches, if any. + /// + /// TouchId is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The + /// next touch will receive a new unique ID. + /// + /// Refer to [`ActiveTouch`]. + active_touches: BTreeMap, + + /// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this + /// holds state information + gesture_state: Option, + + added_or_removed_touches: bool, +} + +#[derive(Clone, Debug)] +struct GestureState { + pinch_type: PinchType, + previous: Option, + current: DynGestureState, +} + +/// Gesture data that can change over time +#[derive(Clone, Copy, Debug)] +struct DynGestureState { + /// used for proportional zooming + avg_distance: f64, + /// used for non-proportional zooming + avg_abs_distance2: Vec2, + avg_pos: Point, + heading: f64, +} + +/// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as +/// long as the finger/pen touches the surface. +#[derive(Clone, Copy, Debug)] +struct ActiveTouch { + /// Current position of this touch, in device coordinates (not necessarily screen position) + pos: Point, +} + +impl TouchState { + pub fn new() -> Self { + Self { + active_touches: Default::default(), + gesture_state: None, + added_or_removed_touches: false, + } + } + + pub fn add_event(&mut self, event: &Touch) { + let pos = Point::new(event.location.x, event.location.y); + match event.phase { + TouchPhase::Started => { + self.active_touches.insert(event.id, ActiveTouch { pos }); + self.added_or_removed_touches = true; + } + TouchPhase::Moved => { + if let Some(touch) = self.active_touches.get_mut(&event.id) { + touch.pos = Point::new(event.location.x, event.location.y); + } + } + TouchPhase::Ended | TouchPhase::Cancelled => { + self.active_touches.remove(&event.id); + self.added_or_removed_touches = true; + } + } + } + + pub fn end_frame(&mut self) { + // This needs to be called each frame, even if there are no new touch events. + // Otherwise, we would send the same old delta information multiple times: + self.update_gesture(); + + if self.added_or_removed_touches { + // Adding or removing fingers makes the average values "jump". We better forget + // about the previous values, and don't create delta information for this frame: + if let Some(ref mut state) = &mut self.gesture_state { + state.previous = None; + } + } + self.added_or_removed_touches = false; + } + + pub fn info(&self) -> Option { + self.gesture_state.as_ref().map(|state| { + // state.previous can be `None` when the number of simultaneous touches has just + // changed. In this case, we take `current` as `previous`, pretending that there + // was no change for the current frame. + let state_previous = state.previous.unwrap_or(state.current); + + let zoom_delta = if self.active_touches.len() > 1 { + state.current.avg_distance / state_previous.avg_distance + } else { + 1. + }; + + let zoom_delta2 = if self.active_touches.len() > 1 { + match state.pinch_type { + PinchType::Horizontal => Vec2::new( + state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x, + 1.0, + ), + PinchType::Vertical => Vec2::new( + 1.0, + state.current.avg_abs_distance2.y / state_previous.avg_abs_distance2.y, + ), + PinchType::Proportional => Vec2::new(zoom_delta, zoom_delta), + } + } else { + Vec2::new(1.0, 1.0) + }; + + MultiTouchInfo { + num_touches: self.active_touches.len(), + zoom_delta, + zoom_delta_2d: zoom_delta2, + zoom_centre: state.current.avg_pos, + rotation_delta: (state.current.heading - state_previous.heading), + translation_delta: state.current.avg_pos - state_previous.avg_pos, + } + }) + } + + fn update_gesture(&mut self) { + if let Some(dyn_state) = self.calc_dynamic_state() { + if let Some(ref mut state) = &mut self.gesture_state { + // updating an ongoing gesture + state.previous = Some(state.current); + state.current = dyn_state; + } else { + // starting a new gesture + self.gesture_state = Some(GestureState { + pinch_type: PinchType::classify(&self.active_touches), + previous: None, + current: dyn_state, + }); + } + } else { + // the end of a gesture (if there is any) + self.gesture_state = None; + } + } + + /// `None` if less than two fingers + fn calc_dynamic_state(&self) -> Option { + let num_touches = self.active_touches.len(); + if num_touches == 0 { + return None; + } + let mut state = DynGestureState { + avg_distance: 0.0, + avg_abs_distance2: Vec2::ZERO, + avg_pos: Point::ZERO, + heading: 0.0, + }; + let num_touches_recip = 1. / num_touches as f64; + + // first pass: calculate force and center of touch positions: + for touch in self.active_touches.values() { + state.avg_pos.x += touch.pos.x; + state.avg_pos.y += touch.pos.y; + } + state.avg_pos.x *= num_touches_recip; + state.avg_pos.y *= num_touches_recip; + + // second pass: calculate distances from center: + for touch in self.active_touches.values() { + state.avg_distance += state.avg_pos.distance(touch.pos); + state.avg_abs_distance2.x += (state.avg_pos.x - touch.pos.x).abs(); + state.avg_abs_distance2.y += (state.avg_pos.y - touch.pos.y).abs(); + } + state.avg_distance *= num_touches_recip; + state.avg_abs_distance2 *= num_touches_recip; + + // Calculate the direction from the first touch to the center position. + // This is not the perfect way of calculating the direction if more than two fingers + // are involved, but as long as all fingers rotate more or less at the same angular + // velocity, the shortcomings of this method will not be noticed. One can see the + // issues though, when touching with three or more fingers, and moving only one of them + // (it takes two hands to do this in a controlled manner). A better technique would be + // to store the current and previous directions (with reference to the center) for each + // touch individually, and then calculate the average of all individual changes in + // direction. But this approach cannot be implemented locally in this method, making + // everything a bit more complicated. + let first_touch = self.active_touches.values().next().unwrap(); + state.heading = (state.avg_pos - first_touch.pos).atan2(); + + Some(state) + } +} + +impl Debug for TouchState { + // This outputs less clutter than `#[derive(Debug)]`: + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (id, touch) in &self.active_touches { + f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?; + } + f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +enum PinchType { + Horizontal, + Vertical, + Proportional, +} + +impl PinchType { + fn classify(touches: &BTreeMap) -> Self { + // For non-proportional 2d zooming: + // If the user is pinching with two fingers that have roughly the same Y coord, + // then the Y zoom is unstable and should be 1. + // Similarly, if the fingers are directly above/below each other, + // we should only zoom on the Y axis. + // If the fingers are roughly on a diagonal, we revert to the proportional zooming. + + if touches.len() == 2 { + let mut touches = touches.values(); + let t0 = touches.next().unwrap().pos; + let t1 = touches.next().unwrap().pos; + + let dx = (t0.x - t1.x).abs(); + let dy = (t0.y - t1.y).abs(); + + if dx > 3.0 * dy { + Self::Horizontal + } else if dy > 3.0 * dx { + Self::Vertical + } else { + Self::Proportional + } + } else { + Self::Proportional + } + } +} diff --git a/src/lib.rs b/src/lib.rs index fcba817..f8b2a3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ pub type Result = std::result::Result; pub struct Renderer { engine: Engine, shaders: FullShaders, - blit: BlitPipeline, + blit: Option, target: Option, } @@ -63,12 +63,20 @@ pub struct RenderParams { pub height: u32, } +pub struct RendererOptions { + /// The format of the texture used for surfaces with this renderer/device + /// If None, the renderer cannot be used with surfaces + pub surface_format: Option, +} + impl Renderer { /// Creates a new renderer for the specified device. - pub fn new(device: &Device) -> Result { + pub fn new(device: &Device, render_options: &RendererOptions) -> Result { let mut engine = Engine::new(); let shaders = shaders::full_shaders(device, &mut engine)?; - let blit = BlitPipeline::new(device, TextureFormat::Bgra8Unorm); + let blit = render_options + .surface_format + .map(|surface_format| BlitPipeline::new(device, surface_format)); Ok(Self { engine, shaders, @@ -105,8 +113,9 @@ impl Renderer { /// This renders to an intermediate texture and then runs a render pass to blit to the /// specified surface texture. /// - /// The surface is assumed to be of the specified dimensions and have been created with the - /// [wgpu::TextureFormat::Bgra8Unorm] format. + /// The surface is assumed to be of the specified dimensions and have been configured with + /// the same format passed in the constructing [`RendererOptions`]' `surface_format`. + /// Panics if `surface_format` was `None` pub fn render_to_surface( &mut self, device: &Device, @@ -127,6 +136,10 @@ impl Renderer { target = TargetTexture::new(device, width, height); } self.render_to_texture(device, queue, scene, &target.view, ¶ms)?; + let blit = self + .blit + .as_ref() + .expect("renderer should have configured surface_format to use on a surface"); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); { @@ -135,7 +148,7 @@ impl Renderer { .create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, - layout: &self.blit.bind_layout, + layout: &blit.bind_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&target.view), @@ -153,7 +166,7 @@ impl Renderer { })], depth_stencil_attachment: None, }); - render_pass.set_pipeline(&self.blit.pipeline); + render_pass.set_pipeline(&blit.pipeline); render_pass.set_bind_group(0, &bind_group, &[]); render_pass.draw(0..6, 0..1); } @@ -205,7 +218,7 @@ impl Renderer { } else { return Err("channel was closed".into()); } - let mapped = buf_slice.get_mapped_range(); + let _mapped = buf_slice.get_mapped_range(); // println!("{:?}", bytemuck::cast_slice::<_, u32>(&mapped)); } // TODO: apply logic to determine whether we need to rerun coarse, and also @@ -220,6 +233,7 @@ impl Renderer { Ok(()) } + /// See [Self::render_to_surface] pub async fn render_to_surface_async( &mut self, device: &Device, @@ -241,6 +255,10 @@ impl Renderer { } self.render_to_texture_async(device, queue, scene, &target.view, params) .await?; + let blit = self + .blit + .as_ref() + .expect("renderer should have configured surface_format to use on a surface"); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); { @@ -249,7 +267,7 @@ impl Renderer { .create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, - layout: &self.blit.bind_layout, + layout: &blit.bind_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&target.view), @@ -267,7 +285,7 @@ impl Renderer { })], depth_stencil_attachment: None, }); - render_pass.set_pipeline(&self.blit.pipeline); + render_pass.set_pipeline(&blit.pipeline); render_pass.set_bind_group(0, &bind_group, &[]); render_pass.draw(0..6, 0..1); } diff --git a/src/render.rs b/src/render.rs index d5d29a6..3b84020 100644 --- a/src/render.rs +++ b/src/render.rs @@ -5,7 +5,6 @@ use bytemuck::{Pod, Zeroable}; use crate::{ encoding::Encoding, engine::{BufProxy, ImageFormat, ImageProxy, Recording, ResourceProxy}, - peniko::Color, shaders::{self, FullShaders, Shaders}, RenderParams, Scene, }; diff --git a/src/util.rs b/src/util.rs index ac1070e..e33d940 100644 --- a/src/util.rs +++ b/src/util.rs @@ -22,7 +22,7 @@ use super::Result; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use wgpu::{ - Adapter, Device, Instance, Limits, Queue, RequestAdapterOptions, Surface, SurfaceConfiguration, + Adapter, Device, Instance, Limits, Queue, Surface, SurfaceConfiguration, TextureFormat, }; /// Simple render context that maintains wgpu state for rendering the pipeline. @@ -55,7 +55,16 @@ impl RenderContext { W: HasRawWindowHandle + HasRawDisplayHandle, { let surface = unsafe { self.instance.create_surface(window) }.unwrap(); - let format = wgpu::TextureFormat::Bgra8Unorm; + let dev_id = self.device(Some(&surface)).await.unwrap(); + + let device_handle = &self.devices[dev_id]; + let capabilities = surface.get_capabilities(&device_handle.adapter); + let format = capabilities + .formats + .into_iter() + .find(|it| matches!(it, TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm)) + .expect("surface should support Rgba8Unorm or Bgra8Unorm"); + let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format, @@ -65,12 +74,12 @@ impl RenderContext { alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], }; - let dev_id = self.device(Some(&surface)).await.unwrap(); surface.configure(&self.devices[dev_id].device, &config); RenderSurface { surface, config, dev_id, + format, } } @@ -133,10 +142,12 @@ impl RenderContext { } /// Combination of surface and its configuration. +#[derive(Debug)] pub struct RenderSurface { pub surface: Surface, pub config: SurfaceConfiguration, pub dev_id: usize, + pub format: TextureFormat, } struct NullWake;