blob: 3ea440b0965ba6e954127627556c4b5e619846de [file] [log] [blame]
// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Winit example.
// The following lints are part of the Linebender standard set,
// but resolving them has been deferred for now.
// Feel free to send a PR that solves one or more of these.
#![allow(
unreachable_pub,
clippy::allow_attributes_without_reason,
clippy::cast_possible_truncation,
clippy::shadow_unrelated
)]
use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;
use minimal_pipeline_cache::{get_cache_directory, load_pipeline_cache, write_pipeline_cache};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use vello::low_level::DebugLayers;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
use winit::application::ApplicationHandler;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
use winit::keyboard::{Key, ModifiersState, NamedKey};
#[cfg(all(feature = "wgpu-profiler", not(target_arch = "wasm32")))]
use std::time::Duration;
#[cfg(all(feature = "wgpu-profiler", target_arch = "wasm32"))]
use web_time::Duration;
use clap::Parser;
use scenes::{ExampleScene, ImageCache, SceneParams, SceneSet, SimpleText};
use vello::kurbo::{Affine, Point, Vec2};
use vello::peniko::{Color, color::palette};
use vello::util::{RenderContext, RenderSurface};
use vello::{AaConfig, Renderer, RendererOptions, Scene, low_level::BumpAllocators};
use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::{Window, WindowAttributes};
use vello::wgpu::{self, PipelineCache};
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
mod hot_reload;
mod minimal_pipeline_cache;
mod multi_touch;
mod stats;
#[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,
#[arg(long)]
/// Whether to use CPU shaders
use_cpu: bool,
/// Used to disable vsync at startup. Can be toggled with the "V" key.
///
/// This setting is useful for Android, where it might be harder to press this key
#[arg(long)]
startup_vsync_off: bool,
/// Used to enable gpu profiling at startup. Can be toggled with the "G" key
///
/// It is off by default because it has adverse performance characteristics
#[arg(long)]
#[cfg(feature = "wgpu-profiler")]
startup_gpu_profiling_on: bool,
/// Whether to force initialising the shaders serially (rather than spawning threads)
/// This has no effect on wasm, and defaults to 1 on macOS for performance reasons
///
/// Use `0` for an automatic choice
#[arg(long, default_value_t=default_threads())]
num_init_threads: usize,
/// Use the asynchronous pipeline (if available) for rendering
///
/// The asynchronous pipeline is one approach for robust memory - see
/// <https://github.com/linebender/vello/issues/366>
///
/// However, it also has potential latency issues, especially for
/// accessibility technology, as it (currently) blocks the main thread for
/// extended periods
#[arg(long)]
async_pipeline: bool,
}
fn default_threads() -> usize {
#[cfg(target_os = "macos")]
return 1;
#[cfg(not(target_os = "macos"))]
return 0;
}
struct RenderState {
surface: RenderSurface<'static>,
window: Arc<Window>,
}
#[cfg(not(target_os = "android"))]
// TODO: Make this set configurable through the command line
// Alternatively, load anti-aliasing shaders on demand/asynchronously
const AA_CONFIGS: [AaConfig; 3] = [AaConfig::Area, AaConfig::Msaa8, AaConfig::Msaa16];
#[cfg(target_os = "android")]
// Hard code to only one on Android whilst we are working on startup speed
const AA_CONFIGS: [AaConfig; 1] = [AaConfig::Area];
struct VelloApp {
context: RenderContext,
renderers: Vec<Option<Renderer>>,
state: Option<RenderState>,
// Whilst suspended, we drop `render_state`, but need to keep the same window.
#[cfg(not(target_arch = "wasm32"))]
cached_window: Option<Arc<Window>>,
#[cfg(not(target_arch = "wasm32"))]
use_cpu: bool,
#[cfg(not(target_arch = "wasm32"))]
num_init_threads: usize,
scenes: Vec<ExampleScene>,
scene: Scene,
fragment: Scene,
simple_text: SimpleText,
images: ImageCache,
stats: stats::Stats,
stats_shown: bool,
base_color: Option<Color>,
async_pipeline: bool,
scene_complexity: Option<BumpAllocators>,
complexity_shown: bool,
vsync_on: bool,
#[cfg(feature = "wgpu-profiler")]
gpu_profiling_on: bool,
#[cfg(feature = "wgpu-profiler")]
profile_stored: Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
#[cfg(feature = "wgpu-profiler")]
profile_taken: Instant,
// We allow cycling through AA configs in either direction, so use a signed index
aa_config_ix: i32,
frame_start_time: Instant,
start: Instant,
touch_state: multi_touch::TouchState,
// 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
navigation_fingers: HashSet<u64>,
transform: Affine,
mouse_down: bool,
prior_position: Option<Point>,
// We allow looping left and right through the scenes, so use a signed index
scene_ix: i32,
complexity: usize,
prev_scene_ix: i32,
modifiers: ModifiersState,
debug: DebugLayers,
#[cfg(not(target_arch = "wasm32"))]
cache_data: Option<(PathBuf, std::sync::mpsc::Sender<(PipelineCache, PathBuf)>)>,
}
impl ApplicationHandler<UserEvent> for VelloApp {
#[cfg(target_arch = "wasm32")]
fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {}
#[cfg(not(target_arch = "wasm32"))]
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let None = self.state else {
return;
};
let window = self
.cached_window
.take()
.unwrap_or_else(|| Arc::new(event_loop.create_window(window_attributes()).unwrap()));
let size = window.inner_size();
let present_mode = if self.vsync_on {
wgpu::PresentMode::AutoVsync
} else {
wgpu::PresentMode::AutoNoVsync
};
let surface_future =
self.context
.create_surface(window.clone(), size.width, size.height, present_mode);
// We need to block here, in case a Suspended event appeared
let surface = pollster::block_on(surface_future).expect("Error creating surface");
self.state = {
let render_state = RenderState { window, surface };
self.renderers
.resize_with(self.context.devices.len(), || None);
let id = render_state.surface.dev_id;
self.renderers[id].get_or_insert_with(|| {
let device_handle = &self.context.devices[id];
let cache = if let Some((dir, tx)) = self.cache_data.as_ref() {
// Safety: Hoping for the best. Given that we're using as private a cache directory as possible, it's
// probably fine?
unsafe {
load_pipeline_cache(
&device_handle.device,
&device_handle.adapter().get_info(),
dir,
)
.unwrap()
.map(|(cache, file)| (cache, file, tx.clone()))
}
} else {
None
};
let start = Instant::now();
let renderer = Renderer::new(
&device_handle.device,
RendererOptions {
use_cpu: self.use_cpu,
antialiasing_support: AA_CONFIGS.iter().copied().collect(),
num_init_threads: NonZeroUsize::new(self.num_init_threads),
pipeline_cache: cache.as_ref().map(|(cache, _, _)| cache.clone()),
},
)
.map_err(|e| {
// Pretty-print any renderer creation error using Display formatting before unwrapping.
anyhow::format_err!("{e}")
})
.expect("Failed to create renderer");
log::info!("Creating renderer {id} took {:?}", start.elapsed());
#[cfg(feature = "wgpu-profiler")]
let mut renderer = renderer;
#[cfg(feature = "wgpu-profiler")]
renderer
.profiler
.change_settings(wgpu_profiler::GpuProfilerSettings {
enable_timer_queries: self.gpu_profiling_on,
enable_debug_groups: self.gpu_profiling_on,
..Default::default()
})
.expect("Not setting max_num_pending_frames");
if let Some((cache, file, tx)) = cache {
drop(tx.send((cache, file)));
}
renderer
});
Some(render_state)
};
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
let Some(render_state) = &mut self.state else {
return;
};
if render_state.window.id() != window_id {
return;
}
let _span = if !matches!(event, WindowEvent::RedrawRequested) {
Some(tracing::trace_span!("Handling window event", ?event).entered())
} else {
None
};
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::ModifiersChanged(m) => self.modifiers = m.state(),
WindowEvent::KeyboardInput { event, .. } => {
if event.state == ElementState::Pressed {
match event.logical_key.as_ref() {
Key::Named(NamedKey::ArrowLeft) => {
self.scene_ix = self.scene_ix.saturating_sub(1);
}
Key::Named(NamedKey::ArrowRight) => {
self.scene_ix = self.scene_ix.saturating_add(1);
}
Key::Named(NamedKey::ArrowUp) => self.complexity += 1,
Key::Named(NamedKey::ArrowDown) => {
self.complexity = self.complexity.saturating_sub(1);
}
Key::Named(NamedKey::Space) => {
self.transform = Affine::IDENTITY;
}
Key::Character(char) => {
// TODO: Have a more principled way of handling modifiers on keypress
// see e.g. https://xi.zulipchat.com/#narrow/channel/351333-glazier/topic/Keyboard.20shortcuts/with/403538769
let char = char.to_lowercase();
match char.as_str() {
"q" | "e" => {
if let Some(prior_position) = self.prior_position {
let is_clockwise = char == "e";
let angle = if is_clockwise { -0.05 } else { 0.05 };
self.transform =
self.transform.then_rotate_about(angle, prior_position);
}
}
"s" => {
self.stats_shown = !self.stats_shown;
}
"d" => {
self.complexity_shown = !self.complexity_shown;
}
"c" => {
self.stats.clear_min_and_max();
}
"m" => {
self.aa_config_ix = if self.modifiers.shift_key() {
self.aa_config_ix.saturating_sub(1)
} else {
self.aa_config_ix.saturating_add(1)
};
}
#[cfg(feature = "wgpu-profiler")]
"p" => {
if let Some(renderer) =
&self.renderers[render_state.surface.dev_id]
{
store_profiling(renderer, &self.profile_stored);
}
}
#[cfg(feature = "wgpu-profiler")]
"g" => {
self.gpu_profiling_on = !self.gpu_profiling_on;
if let Some(renderer) =
&mut self.renderers[render_state.surface.dev_id]
{
renderer
.profiler
.change_settings(wgpu_profiler::GpuProfilerSettings {
enable_timer_queries: self.gpu_profiling_on,
enable_debug_groups: self.gpu_profiling_on,
..Default::default()
})
.expect("Not setting max_num_pending_frames");
}
}
"v" => {
self.vsync_on = !self.vsync_on;
self.context.set_present_mode(
&mut render_state.surface,
if self.vsync_on {
wgpu::PresentMode::AutoVsync
} else {
wgpu::PresentMode::AutoNoVsync
},
);
}
debug_layer @ ("1" | "2" | "3" | "4") => {
match debug_layer {
"1" => {
self.debug.toggle(DebugLayers::BOUNDING_BOXES);
}
"2" => {
self.debug.toggle(DebugLayers::LINESOUP_SEGMENTS);
}
"3" => {
self.debug.toggle(DebugLayers::LINESOUP_POINTS);
}
"4" => {
self.debug.toggle(DebugLayers::VALIDATION);
}
_ => unreachable!(),
}
if !self.debug.is_empty() && !self.async_pipeline {
log::warn!(
"Debug Layers won't work without using `--async-pipeline`. Requested {:?}",
self.debug
);
}
}
_ => {}
}
}
Key::Named(NamedKey::Escape) => event_loop.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 let Some(RenderState { surface, .. }) = &self.state {
if touch.location.y > surface.config.height as f64 * 2. / 3. {
self.navigation_fingers.insert(touch.id);
// The left third of the navigation zone navigates backwards
if touch.location.x < surface.config.width as f64 / 3. {
self.scene_ix = self.scene_ix.saturating_sub(1);
} else if touch.location.x > 2. * surface.config.width as f64 / 3. {
self.scene_ix = self.scene_ix.saturating_add(1);
}
}
}
}
TouchPhase::Ended | TouchPhase::Cancelled => {
// We intentionally ignore the result here
self.navigation_fingers.remove(&touch.id);
}
TouchPhase::Moved => (),
}
// See documentation on navigation_fingers
if !self.navigation_fingers.contains(&touch.id) {
self.touch_state.add_event(&touch);
}
}
WindowEvent::Resized(size) => {
if let Some(RenderState { surface, window }) = &mut self.state {
self.context
.resize_surface(surface, size.width, size.height);
window.request_redraw();
}
}
WindowEvent::MouseInput { state, button, .. } => {
if button == MouseButton::Left {
self.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) = self.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
};
self.transform = self
.transform
.then_scale_about(BASE.powf(exponent), prior_position);
} else {
log::warn!("Scrolling without mouse in window; this shouldn't be possible");
}
}
WindowEvent::CursorLeft { .. } => {
self.prior_position = None;
}
WindowEvent::CursorMoved { position, .. } => {
let position = Point {
x: position.x,
y: position.y,
};
if self.mouse_down {
if let Some(prior) = self.prior_position {
self.transform = self.transform.then_translate(position - prior);
}
}
self.prior_position = Some(position);
}
WindowEvent::RedrawRequested => {
let _rendering_span = tracing::trace_span!("Actioning Requested Redraw").entered();
let encoding_span = tracing::trace_span!("Encoding scene").entered();
render_state.window.request_redraw();
let Some(RenderState { surface, window }) = &self.state else {
return;
};
let width = surface.config.width;
let height = surface.config.height;
let device_handle = &self.context.devices[surface.dev_id];
let snapshot = self.stats.snapshot();
// Allow looping forever
self.scene_ix = self.scene_ix.rem_euclid(self.scenes.len() as i32);
self.aa_config_ix = self.aa_config_ix.rem_euclid(AA_CONFIGS.len() as i32);
let example_scene = &mut self.scenes[self.scene_ix as usize];
if self.prev_scene_ix != self.scene_ix {
self.transform = Affine::IDENTITY;
self.prev_scene_ix = self.scene_ix;
window.set_title(&format!("Vello demo - {}", example_scene.config.name));
}
self.fragment.reset();
let mut scene_params = SceneParams {
time: self.start.elapsed().as_secs_f64(),
text: &mut self.simple_text,
images: &mut self.images,
resolution: None,
base_color: None,
interactive: true,
complexity: self.complexity,
};
example_scene
.function
.render(&mut self.fragment, &mut scene_params);
// 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 base_color = self
.base_color
.or(scene_params.base_color)
.unwrap_or(palette::css::BLACK);
let antialiasing_method = AA_CONFIGS[self.aa_config_ix as usize];
let render_params = vello::RenderParams {
base_color,
width,
height,
antialiasing_method,
};
self.scene.reset();
let mut transform = self.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 scale_factor =
(width as f64 / resolution.x).min(height as f64 / resolution.y);
transform *= Affine::scale(scale_factor);
}
self.scene.append(&self.fragment, Some(transform));
if self.stats_shown {
snapshot.draw_layer(
&mut self.scene,
scene_params.text,
width as f64,
height as f64,
self.stats.samples(),
self.complexity_shown
.then_some(self.scene_complexity)
.flatten(),
self.vsync_on,
antialiasing_method,
);
#[cfg(feature = "wgpu-profiler")]
if let Some(profiling_result) = self.renderers[surface.dev_id]
.as_mut()
.and_then(|renderer| renderer.profile_result.take())
{
if self.profile_stored.is_none()
|| self.profile_taken.elapsed() > Duration::from_secs(1)
{
self.profile_stored = Some(profiling_result);
self.profile_taken = Instant::now();
}
}
#[cfg(feature = "wgpu-profiler")]
if let Some(profiling_result) = self.profile_stored.as_ref() {
stats::draw_gpu_profiling(
&mut self.scene,
scene_params.text,
width as f64,
height as f64,
profiling_result,
);
}
}
drop(encoding_span);
let render_span = tracing::trace_span!("Dispatching render").entered();
// Note: we don't run the async/"robust" pipeline on web, as
// it requires more async wiring for the readback. See
// [#vello > async on wasm](https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/async.20on.20wasm/with/396685264)
#[expect(
deprecated,
reason = "We still want to use the async pipeline for the debug layers"
)]
if self.async_pipeline && cfg!(not(target_arch = "wasm32")) {
self.scene_complexity = vello::util::block_on_wgpu(
&device_handle.device,
self.renderers[surface.dev_id]
.as_mut()
.unwrap()
.render_to_texture_async(
&device_handle.device,
&device_handle.queue,
&self.scene,
&surface.target_view,
&render_params,
self.debug,
),
)
.expect("failed to render to texture");
} else {
self.renderers[surface.dev_id]
.as_mut()
.unwrap()
.render_to_texture(
&device_handle.device,
&device_handle.queue,
&self.scene,
&surface.target_view,
&render_params,
)
.expect("failed to render to texture");
}
drop(render_span);
let texture_span = tracing::trace_span!("Blitting to surface").entered();
let surface_texture = surface
.surface
.get_current_texture()
.expect("failed to get surface texture");
// Perform the copy
// (TODO: Does it improve throughput to acquire the surface after the previous texture render has happened?)
let mut encoder =
device_handle
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Surface Blit"),
});
surface.blitter.copy(
&device_handle.device,
&mut encoder,
&surface.target_view,
&surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default()),
);
device_handle.queue.submit([encoder.finish()]);
surface_texture.present();
drop(texture_span);
{
let _poll_span = tracing::trace_span!("Polling wgpu device").entered();
device_handle.device.poll(wgpu::PollType::Poll).unwrap();
}
let new_time = Instant::now();
self.stats.add_sample(stats::Sample {
frame_time_us: (new_time - self.frame_start_time).as_micros() as u64,
});
self.frame_start_time = new_time;
}
_ => {}
}
}
fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
self.touch_state.end_frame();
let touch_info = self.touch_state.info();
if let Some(touch_info) = touch_info {
let centre = Vec2::new(touch_info.zoom_centre.x, touch_info.zoom_centre.y);
self.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)
* self.transform;
}
if let Some(render_state) = &mut self.state {
render_state.window.request_redraw();
}
}
fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) {
match event {
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
UserEvent::HotReload => {
let Some(render_state) = &mut self.state else {
return;
};
let device_handle = &self.context.devices[render_state.surface.dev_id];
log::info!("==============\nReloading shaders");
let start = Instant::now();
let result = self.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(_) => log::info!("Reloading took {:?}", start.elapsed()),
Err(e) => log::error!("Failed to reload shaders: {e}"),
}
}
}
}
fn suspended(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
log::info!("Suspending");
#[cfg(not(target_arch = "wasm32"))]
// When we suspend, we need to remove the `wgpu` Surface
if let Some(render_state) = self.state.take() {
self.cached_window = Some(render_state.window);
}
}
}
fn run(
event_loop: EventLoop<UserEvent>,
args: Args,
scenes: SceneSet,
render_cx: RenderContext,
#[cfg(target_arch = "wasm32")] render_state: RenderState,
) {
use winit::keyboard::ModifiersState;
#[cfg(not(target_arch = "wasm32"))]
let (render_state, renderers) = (None::<RenderState>, vec![]);
let cache_directory = get_cache_directory(&event_loop).unwrap();
// 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 (render_state, renderers) = {
let mut renderers = vec![];
renderers.resize_with(render_cx.devices.len(), || None);
let id = render_state.surface.dev_id;
let device_handle = &render_cx.devices[id];
let cache: Option<(PipelineCache, PathBuf)> = if let Some(dir) = cache_directory.as_ref() {
// Safety: Hoping for the best. Given that we're using as private a cache directory as possible, it's
// probably fine?
unsafe {
load_pipeline_cache(
&device_handle.device,
&device_handle.adapter().get_info(),
dir,
)
.unwrap()
}
} else {
None
};
let renderer = Renderer::new(
&device_handle.device,
RendererOptions {
use_cpu: args.use_cpu,
antialiasing_support: AA_CONFIGS.iter().copied().collect(),
// We currently initialise on one thread on WASM, but mark this here
// anyway
num_init_threads: NonZeroUsize::new(1),
pipeline_cache: cache.as_ref().map(|(cache, _)| cache.clone()),
},
)
.map_err(|e| {
// Pretty-print any renderer creation error using Display formatting before unwrapping.
eprintln!("{e}");
e
})
.expect("Failed to create renderer");
#[cfg(feature = "wgpu-profiler")]
let mut renderer = renderer;
#[cfg(feature = "wgpu-profiler")]
renderer
.profiler
.change_settings(wgpu_profiler::GpuProfilerSettings {
enable_timer_queries: args.startup_gpu_profiling_on,
enable_debug_groups: args.startup_gpu_profiling_on,
..Default::default()
})
.expect("Not setting max_num_pending_frames");
renderers[id] = Some(renderer);
if let Some((cache, file)) = cache {
if let Err(e) = write_pipeline_cache(&file, &cache) {
log::error!("Failed to write pipeline cache: {e}");
}
}
(Some(render_state), renderers)
};
#[cfg(not(target_arch = "wasm32"))]
let cache_data = if let Some(cache_directory) = cache_directory {
let (tx, rx) = std::sync::mpsc::channel::<(PipelineCache, PathBuf)>();
std::thread::spawn(move || {
while let Ok((cache, path)) = rx.recv() {
if let Err(e) = write_pipeline_cache(&path, &cache) {
log::error!("Failed to write pipeline cache: {e}");
}
}
});
Some((cache_directory, tx))
} else {
None
};
let debug = DebugLayers::none();
let mut app = VelloApp {
context: render_cx,
renderers,
state: render_state,
#[cfg(not(target_arch = "wasm32"))]
cached_window: None,
#[cfg(not(target_arch = "wasm32"))]
use_cpu: args.use_cpu,
#[cfg(not(target_arch = "wasm32"))]
num_init_threads: args.num_init_threads,
scenes: scenes.scenes,
scene: Scene::new(),
fragment: Scene::new(),
simple_text: SimpleText::new(),
images: ImageCache::new(),
stats: stats::Stats::new(),
stats_shown: true,
base_color: args.args.base_color,
async_pipeline: args.async_pipeline,
scene_complexity: None,
complexity_shown: false,
vsync_on: !args.startup_vsync_off,
#[cfg(feature = "wgpu-profiler")]
gpu_profiling_on: args.startup_gpu_profiling_on,
#[cfg(feature = "wgpu-profiler")]
profile_stored: None,
#[cfg(feature = "wgpu-profiler")]
profile_taken: Instant::now(),
aa_config_ix: 0,
frame_start_time: Instant::now(),
start: Instant::now(),
touch_state: multi_touch::TouchState::new(),
navigation_fingers: HashSet::new(),
transform: Affine::IDENTITY,
mouse_down: false,
prior_position: None,
scene_ix: args.scene.unwrap_or(0),
complexity: 0,
prev_scene_ix: 0,
modifiers: ModifiersState::default(),
debug,
#[cfg(not(target_arch = "wasm32"))]
cache_data,
};
event_loop.run_app(&mut app).expect("run to completion");
}
#[cfg(feature = "wgpu-profiler")]
/// A function extracted to fix rustfmt
fn store_profiling(
renderer: &Renderer,
profile_stored: &Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
) {
if let Some(profile_result) = &renderer.profile_result.as_ref().or(profile_stored.as_ref()) {
// There can be empty results if the required features aren't supported
if !profile_result.is_empty() {
let path = std::path::Path::new("trace.json");
match wgpu_profiler::chrometrace::write_chrometrace(path, profile_result) {
Ok(()) => {
println!("Wrote trace to path {path:?}");
}
Err(e) => {
log::warn!("Failed to write trace {e}");
}
}
}
}
}
fn window_attributes() -> WindowAttributes {
Window::default_attributes()
.with_inner_size(LogicalSize::new(1044, 800))
.with_resizable(true)
.with_title("Vello demo")
}
#[derive(Debug)]
enum UserEvent {
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
HotReload,
}
#[cfg(target_arch = "wasm32")]
fn display_error_message() -> Option<()> {
let window = web_sys::window()?;
let document = window.document()?;
let elements = document.get_elements_by_tag_name("body");
let body = elements.item(0)?;
body.set_inner_html(
r#"<style>
p {
margin: 2em 10em;
font-family: sans-serif;
}
</style>
<p><a href="https://caniuse.com/webgpu">WebGPU</a>
is not enabled. Make sure your browser is updated to
<a href="https://chromiumdash.appspot.com/schedule">Chrome M113</a> or
another browser compatible with WebGPU.</p>"#,
);
Some(())
}
/// Entry point.
#[cfg(not(target_os = "android"))]
pub fn main() -> anyhow::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::builder()
.format_timestamp(Some(env_logger::TimestampPrecision::Millis))
.filter_level(log::LevelFilter::Warn)
.init();
let args = parse_arguments();
let scenes = args.args.select_scene_set()?;
if let Some(scenes) = scenes {
let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
let render_cx = RenderContext::new();
#[cfg(not(target_arch = "wasm32"))]
{
let proxy = event_loop.create_proxy();
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")]
{
let mut render_cx = render_cx;
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init().expect("could not initialize logger");
use winit::platform::web::WindowExtWebSys;
#[allow(deprecated)]
let window = Arc::new(event_loop.create_window(window_attributes()).unwrap());
// On wasm, append the canvas to the document body
let canvas = window.canvas().unwrap();
web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| doc.body())
.and_then(|body| body.append_child(canvas.as_ref()).ok())
.expect("couldn't append canvas to document body");
// Best effort to start with the canvas focused, taking input
drop(web_sys::HtmlElement::from(canvas).focus());
wasm_bindgen_futures::spawn_local(async move {
let (width, height, scale_factor) = web_sys::window()
.map(|w| {
(
w.inner_width().unwrap().as_f64().unwrap(),
w.inner_height().unwrap().as_f64().unwrap(),
w.device_pixel_ratio(),
)
})
.unwrap();
let size =
winit::dpi::PhysicalSize::from_logical::<_, f64>((width, height), scale_factor);
_ = window.request_inner_size(size);
let surface = render_cx
.create_surface(
window.clone(),
size.width,
size.height,
wgpu::PresentMode::AutoVsync,
)
.await;
if let Ok(surface) = surface {
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);
} else {
_ = display_error_message();
}
});
}
}
Ok(())
}
fn parse_arguments() -> Args {
// We allow baking in arguments at compile time. This is especially useful for
// Android and WASM.
// This is used on desktop platforms to allow debugging the same settings
if let Some(args) = option_env!("VELLO_STATIC_ARGS") {
// We split by whitespace here to allow passing multiple arguments
// In theory, we could do more advanced parsing/splitting (e.g. using quotes),
// but that would require a lot more effort
// We `chain` in a fake binary name, because clap ignores the first argument otherwise
// Ideally, we'd use the `no_binary_name` argument, but setting that at runtime would
// require globals or some worse hacks
Args::parse_from(std::iter::once("with_winit").chain(args.split_ascii_whitespace()))
} else {
Args::parse()
}
}
#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(app: AndroidApp) {
use winit::platform::android::EventLoopBuilderExtAndroid;
let config = android_logger::Config::default();
// We allow configuring the Android logging with an environment variable at build time
let config = if let Some(logging_config) = option_env!("VELLO_STATIC_LOG") {
let mut filter = android_logger::FilterBuilder::new();
filter.filter_level(log::LevelFilter::Warn);
filter.parse(logging_config);
let filter = filter.build();
// This shouldn't be needed in theory, but without this the max
// level is set to 0 (i.e. Off)
let config = config.with_max_level(filter.filter());
config.with_filter(filter)
} else {
config.with_max_level(log::LevelFilter::Warn)
};
android_logger::init_once(config);
// Send tracing events to Android Trace
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(tracing_android_trace::AndroidTraceLayer::new())
.try_init()
.unwrap();
log::info!(
"Max level: {}",
tracing::level_filters::LevelFilter::current()
);
let event_loop = EventLoop::with_user_event()
.with_android_app(app)
.build()
.expect("Required to continue");
let args = parse_arguments();
let scenes = args.args.select_scene_set().unwrap().unwrap();
let render_cx = RenderContext::new();
run(event_loop, args, scenes, render_cx);
}
#[cfg(all(feature = "_ci_dep_features_to_test", test))]
#[test]
// This just tests that the "kurbo" dependency we enable schemars for
// aligns to the same version that vello's peniko dependency resolves to.
fn test_kurbo_schemars_with_peniko() {
use std::marker::PhantomData;
#[expect(unused_qualifications)]
let _: PhantomData<kurbo::Rect> = PhantomData::<vello::peniko::kurbo::Rect>;
}