Make the with_winit example run on android (#273)

This commit is contained in:
Daniel McNab 2023-03-05 11:33:30 +00:00 committed by GitHub
parent 5d915113e8
commit 5156447346
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 995 additions and 388 deletions

View file

@ -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.<profile>`).
>
> 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

View file

@ -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,7 +86,12 @@ 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)
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);

View file

@ -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<Option<SceneSet>> {
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 {

View file

@ -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<SceneSet> {
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();
function: Box::new(svg_function_of(name.clone(), move || {
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);
}),
contents
})),
config: crate::SceneConfig {
animated: false,
name,
},
}
}
pub fn svg_function_of<R: AsRef<str>>(
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!()
}
}
};
}
}

View file

@ -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);
}
crate::svg::svg_function_of("Ghostscript Tiger".to_string(), move || contents)
}
// Support functions

View file

@ -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::<RenderDevice>().unwrap();
VelloRenderer(Renderer::new(device.wgpu_device()).unwrap())
VelloRenderer(
Renderer::new(
device.wgpu_device(),
&RendererOptions {
surface_format: None,
},
)
.unwrap(),
)
}
}

View file

@ -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"

View file

@ -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<impl Sized> {
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(
debouncer.watcher().watch(
&Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../shader")
.canonicalize()
.unwrap(),
.canonicalize()?,
// We currently don't support hot reloading the imports, so don't recurse into there
RecursiveMode::NonRecursive,
)
.expect("Could watch shaders directory");
debouncer
)?;
Ok(debouncer)
}

View file

@ -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<i32>,
#[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<UserEvent>,
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<Option<Renderer>> = vec![];
#[cfg(not(target_arch = "wasm32"))]
let mut render_cx = render_cx;
#[cfg(not(target_arch = "wasm32"))]
let mut render_state = None::<RenderState>;
// 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<Vec2> = 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<UserEvent>) -> 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::<UserEvent>::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);
}

View file

@ -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<std::path::PathBuf>,
/// When rendering an svg, what scale to use
#[arg(long)]
scale: Option<f64>,
/// Which scene (index) to start on
/// Switch between scenes with left and right arrow keys
#[arg(long)]
scene: Option<i32>,
#[command(flatten)]
args: scenes::Arguments,
}
async fn run(event_loop: EventLoop<UserEvent>, 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<Vec2> = 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::<UserEvent>::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::<UserEvent>::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()
}

View file

@ -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<u64, ActiveTouch>,
/// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this
/// holds state information
gesture_state: Option<GestureState>,
added_or_removed_touches: bool,
}
#[derive(Clone, Debug)]
struct GestureState {
pinch_type: PinchType,
previous: Option<DynGestureState>,
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<MultiTouchInfo> {
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<DynGestureState> {
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<u64, ActiveTouch>) -> 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
}
}
}

View file

@ -48,7 +48,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub struct Renderer {
engine: Engine,
shaders: FullShaders,
blit: BlitPipeline,
blit: Option<BlitPipeline>,
target: Option<TargetTexture>,
}
@ -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<TextureFormat>,
}
impl Renderer {
/// Creates a new renderer for the specified device.
pub fn new(device: &Device) -> Result<Self> {
pub fn new(device: &Device, render_options: &RendererOptions) -> Result<Self> {
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, &params)?;
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);
}

View file

@ -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,
};

View file

@ -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;