437 lines
15 KiB
Rust
437 lines
15 KiB
Rust
//! Example framework
|
|
|
|
// TODO(#3408): remove unwrap()
|
|
#![expect(clippy::unwrap_used)]
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::Context as _;
|
|
use re_renderer::device_caps::DeviceCaps;
|
|
use re_renderer::view_builder::ViewBuilder;
|
|
use re_renderer::{RenderConfig, RenderContext};
|
|
use web_time::Instant;
|
|
use winit::application::ApplicationHandler;
|
|
use winit::event::WindowEvent;
|
|
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
|
use winit::window::{Window, WindowId};
|
|
|
|
pub struct ViewDrawResult {
|
|
pub view_builder: ViewBuilder,
|
|
pub command_buffer: wgpu::CommandBuffer,
|
|
pub target_location: glam::Vec2,
|
|
}
|
|
|
|
pub trait Example {
|
|
fn title() -> &'static str;
|
|
|
|
fn new(re_ctx: &RenderContext) -> Self;
|
|
|
|
fn draw(
|
|
&mut self,
|
|
re_ctx: &RenderContext,
|
|
resolution: [u32; 2],
|
|
time: &Time,
|
|
pixels_per_point: f32,
|
|
) -> anyhow::Result<Vec<ViewDrawResult>>;
|
|
|
|
fn on_key_event(&mut self, _event: winit::event::KeyEvent) {}
|
|
|
|
fn on_cursor_moved(&mut self, _position_in_pixel: glam::UVec2) {}
|
|
|
|
fn suppress_frame_time_logging(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn present_mode(&self) -> wgpu::PresentMode {
|
|
wgpu::PresentMode::AutoVsync
|
|
}
|
|
|
|
fn should_exit(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn on_frame_finished(
|
|
&mut self,
|
|
_re_ctx: &RenderContext,
|
|
_submission_index: wgpu::SubmissionIndex,
|
|
) {
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::allow_attributes, dead_code)] // false positive
|
|
pub struct SplitView {
|
|
pub target_location: glam::Vec2,
|
|
pub resolution_in_pixel: [u32; 2],
|
|
}
|
|
|
|
#[allow(clippy::allow_attributes, dead_code)] // false positive
|
|
pub fn split_resolution(
|
|
resolution: [u32; 2],
|
|
num_rows: usize,
|
|
num_cols: usize,
|
|
) -> impl Iterator<Item = SplitView> {
|
|
let total_width = resolution[0] as f32;
|
|
let total_height = resolution[1] as f32;
|
|
let width = (total_width / num_cols as f32).floor();
|
|
let height = (total_height / num_rows as f32).floor();
|
|
(0..num_rows)
|
|
.flat_map(move |row| (0..num_cols).map(move |col| (row, col)))
|
|
.map(move |(row, col)| {
|
|
// very quick'n'dirty (uneven) borders
|
|
let y = f32::clamp(row as f32 * height + 2.0, 2.0, total_height - 2.0).floor();
|
|
let x = f32::clamp(col as f32 * width + 2.0, 2.0, total_width - 2.0).floor();
|
|
SplitView {
|
|
target_location: glam::vec2(x, y),
|
|
resolution_in_pixel: [(width - 4.0) as u32, (height - 4.0) as u32],
|
|
}
|
|
})
|
|
}
|
|
|
|
pub struct Time {
|
|
start_time: Instant,
|
|
last_draw_time: Instant,
|
|
pub last_frame_duration: web_time::Duration,
|
|
}
|
|
|
|
impl Time {
|
|
pub fn secs_since_startup(&self) -> f32 {
|
|
self.start_time.elapsed().as_secs_f32()
|
|
}
|
|
}
|
|
|
|
struct Application<E> {
|
|
window: Arc<Window>,
|
|
adapter: wgpu::Adapter,
|
|
surface: wgpu::Surface<'static>,
|
|
time: Time,
|
|
|
|
example: E,
|
|
|
|
re_ctx: RenderContext,
|
|
}
|
|
|
|
// Same as egui_wgpu::preferred_framebuffer_format
|
|
fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat {
|
|
for &format in formats {
|
|
if matches!(
|
|
format,
|
|
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
|
|
) {
|
|
return format;
|
|
}
|
|
}
|
|
formats[0] // take the first
|
|
}
|
|
|
|
impl<E: Example + 'static> Application<E> {
|
|
async fn new(window: Window) -> anyhow::Result<Self> {
|
|
let window = Arc::new(window);
|
|
|
|
let mut instance_desc = re_renderer::device_caps::instance_descriptor(None);
|
|
// Run without validation layers, they can be annoying on shader reload depending on the backend.
|
|
instance_desc.flags.remove(wgpu::InstanceFlags::VALIDATION);
|
|
|
|
let instance = wgpu::Instance::new(instance_desc);
|
|
let surface = instance.create_surface(window.clone()).unwrap();
|
|
let adapter = {
|
|
let enabled_backends = re_renderer::device_caps::default_backends();
|
|
let adapters = instance.enumerate_adapters(enabled_backends).await;
|
|
re_renderer::device_caps::select_adapter(&adapters, enabled_backends, Some(&surface))
|
|
.map_err(anyhow::Error::msg)
|
|
.context("failed to find an appropriate adapter")?
|
|
};
|
|
|
|
let device_caps = DeviceCaps::from_adapter(&adapter)?;
|
|
let (device, queue) = adapter
|
|
.request_device(&wgpu::DeviceDescriptor {
|
|
label: None,
|
|
required_features: wgpu::Features::empty(),
|
|
required_limits: device_caps.limits(),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.context("failed to create device")?;
|
|
|
|
let output_format_color =
|
|
preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats);
|
|
|
|
let re_ctx = RenderContext::new(&adapter, device, queue, output_format_color, |caps| {
|
|
RenderConfig::best_for_device_caps(caps)
|
|
})
|
|
.map_err(|err| anyhow::format_err!("{err}"))?;
|
|
|
|
let example = E::new(&re_ctx);
|
|
|
|
let app = Self {
|
|
window,
|
|
adapter,
|
|
surface,
|
|
re_ctx,
|
|
time: Time {
|
|
start_time: Instant::now(),
|
|
last_draw_time: Instant::now(),
|
|
last_frame_duration: web_time::Duration::from_secs(0),
|
|
},
|
|
|
|
example,
|
|
};
|
|
|
|
app.configure_surface(app.window.inner_size());
|
|
Ok(app)
|
|
}
|
|
|
|
fn configure_surface(&self, size: winit::dpi::PhysicalSize<u32>) {
|
|
if size.width == 0 || size.height == 0 {
|
|
return;
|
|
}
|
|
|
|
let surface_config = wgpu::SurfaceConfiguration {
|
|
present_mode: self.example.present_mode(),
|
|
format: self.re_ctx.output_format_color(),
|
|
view_formats: vec![self.re_ctx.output_format_color()],
|
|
..self
|
|
.surface
|
|
.get_default_config(&self.adapter, size.width, size.height)
|
|
.expect("The surface isn't supported by this adapter")
|
|
};
|
|
self.surface.configure(&self.re_ctx.device, &surface_config);
|
|
self.window.request_redraw();
|
|
}
|
|
|
|
fn on_window_event(&mut self, event: WindowEvent) -> bool {
|
|
match event {
|
|
WindowEvent::Resized(size) => {
|
|
self.configure_surface(size);
|
|
}
|
|
|
|
WindowEvent::KeyboardInput { event, .. } => self.example.on_key_event(event),
|
|
|
|
WindowEvent::CursorMoved { position, .. } => self
|
|
.example
|
|
// Don't round the position: The entire range from 0 to excluding 1 should fall into pixel coordinate 0!
|
|
.on_cursor_moved(glam::uvec2(position.x as u32, position.y as u32)),
|
|
|
|
WindowEvent::RedrawRequested => {
|
|
let frame = match self.surface.get_current_texture() {
|
|
wgpu::CurrentSurfaceTexture::Success(surface_texture)
|
|
| wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => surface_texture,
|
|
wgpu::CurrentSurfaceTexture::Timeout
|
|
| wgpu::CurrentSurfaceTexture::Occluded => {
|
|
// Try again later.
|
|
self.window.request_redraw();
|
|
return false;
|
|
}
|
|
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
|
|
// Reconfigure and try again later.
|
|
self.configure_surface(self.window.inner_size());
|
|
return false;
|
|
}
|
|
wgpu::CurrentSurfaceTexture::Validation => {
|
|
panic!("Validation error when acquiring next swap chain texture");
|
|
}
|
|
};
|
|
|
|
self.re_ctx.begin_frame();
|
|
|
|
let view = frame
|
|
.texture
|
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
|
|
|
let draw_results = self
|
|
.example
|
|
.draw(
|
|
&self.re_ctx,
|
|
[frame.texture.width(), frame.texture.height()],
|
|
&self.time,
|
|
self.window.scale_factor() as f32,
|
|
)
|
|
.expect("Failed to draw example");
|
|
|
|
let mut composite_cmd_encoder =
|
|
self.re_ctx
|
|
.device
|
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: "composite_encoder".into(),
|
|
});
|
|
|
|
{
|
|
let mut composite_pass =
|
|
composite_cmd_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: None,
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
view: &view,
|
|
depth_slice: None,
|
|
resolve_target: None,
|
|
ops: wgpu::Operations {
|
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
|
store: wgpu::StoreOp::Store,
|
|
},
|
|
})],
|
|
depth_stencil_attachment: None,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
multiview_mask: None,
|
|
});
|
|
|
|
for draw_result in &draw_results {
|
|
composite_pass.set_viewport(
|
|
draw_result.target_location.x,
|
|
draw_result.target_location.y,
|
|
draw_result.view_builder.resolution_in_pixel()[0] as f32,
|
|
draw_result.view_builder.resolution_in_pixel()[1] as f32,
|
|
0.0,
|
|
1.0,
|
|
);
|
|
draw_result
|
|
.view_builder
|
|
.composite(&self.re_ctx, &mut composite_pass);
|
|
}
|
|
};
|
|
|
|
self.re_ctx.before_submit();
|
|
let submission_index = self.re_ctx.queue.submit(std::iter::chain(
|
|
draw_results.into_iter().map(|d| d.command_buffer),
|
|
std::iter::once(composite_cmd_encoder.finish()),
|
|
));
|
|
self.window.pre_present_notify();
|
|
frame.present();
|
|
|
|
self.example
|
|
.on_frame_finished(&self.re_ctx, submission_index);
|
|
|
|
// Note that this measures CPU wall-clock frame time.
|
|
// Examples may wait for the GPU in `on_frame_finished` to include GPU completion backpressure.
|
|
let current_time = Instant::now();
|
|
let time_passed = current_time - self.time.last_draw_time;
|
|
self.time.last_draw_time = current_time;
|
|
self.time.last_frame_duration = time_passed;
|
|
|
|
// TODO(andreas): Display a median over n frames and while we're on it also stddev thereof.
|
|
// Do it only every second.
|
|
let time_until_next_report = 1.0 - self.time.secs_since_startup().fract();
|
|
if !self.example.suppress_frame_time_logging()
|
|
&& time_until_next_report - time_passed.as_secs_f32() < 0.0
|
|
{
|
|
let time_info_str = format!(
|
|
"{:.2} ms ({:.2} fps)",
|
|
time_passed.as_secs_f32() * 1000.0,
|
|
1.0 / time_passed.as_secs_f32()
|
|
);
|
|
re_log::info!("{time_info_str}");
|
|
}
|
|
|
|
if self.example.should_exit() {
|
|
return true;
|
|
}
|
|
|
|
self.window.request_redraw(); // Busy-painting
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::allow_attributes, dead_code)] // false positive
|
|
pub fn load_rerun_mesh(
|
|
re_ctx: &RenderContext,
|
|
) -> anyhow::Result<Vec<re_renderer::renderer::GpuMeshInstance>> {
|
|
let obj_data = include_bytes!("../../../tests/assets/rerun.obj");
|
|
Ok(
|
|
re_renderer::importer::obj::load_obj_from_buffer(obj_data, re_ctx)?
|
|
.into_gpu_meshes(re_ctx)?,
|
|
)
|
|
}
|
|
|
|
struct WrapApp<E: Example + 'static> {
|
|
app: Option<Application<E>>,
|
|
}
|
|
|
|
impl<E: Example + 'static> ApplicationHandler for WrapApp<E> {
|
|
fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
let attributes = winit::window::WindowAttributes::default()
|
|
.with_title(format!("re_renderer sample - {}", E::title()))
|
|
.with_inner_size(winit::dpi::PhysicalSize {
|
|
width: 1920,
|
|
height: 1080,
|
|
});
|
|
let window = _event_loop
|
|
.create_window(attributes)
|
|
.expect("Failed to create window");
|
|
self.app = Some(pollster::block_on(Application::new(window)).unwrap());
|
|
}
|
|
}
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
_window_id: WindowId,
|
|
event: WindowEvent,
|
|
) {
|
|
if event == WindowEvent::CloseRequested {
|
|
event_loop.exit();
|
|
}
|
|
|
|
if let Some(app) = &mut self.app
|
|
&& app.on_window_event(event)
|
|
{
|
|
event_loop.exit();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn start<E: Example + 'static>() {
|
|
re_log::setup_logging();
|
|
|
|
let event_loop = EventLoop::new().unwrap();
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
let mut wrap_app = WrapApp::<E> { app: None };
|
|
event_loop.run_app(&mut wrap_app).unwrap();
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
async fn run<E: Example + 'static>(event_loop: EventLoop<()>, window: Window) {
|
|
let app = Application::<E>::new(window).await.unwrap();
|
|
let mut wrap_app = WrapApp::<E> { app: Some(app) };
|
|
event_loop.run_app(&mut wrap_app).unwrap();
|
|
}
|
|
|
|
// Make sure panics are logged using `console.error`.
|
|
console_error_panic_hook::set_once();
|
|
|
|
re_log::setup_logging();
|
|
|
|
let window = winit::window::WindowAttributes::default()
|
|
.with_title(format!("re_renderer sample - {}", E::title()))
|
|
.with_inner_size(winit::dpi::PhysicalSize {
|
|
width: 1920,
|
|
height: 1080,
|
|
});
|
|
|
|
// TODO(emilk): port this to the winit 0.30 API, using maybe https://docs.rs/winit/latest/winit/platform/web/trait.EventLoopExtWebSys.html ?
|
|
#[expect(deprecated)]
|
|
let window = event_loop.create_window(window).unwrap();
|
|
|
|
use winit::platform::web::WindowExtWebSys as _;
|
|
let canvas = window.canvas().expect("Couldn't get canvas");
|
|
canvas.style().set_css_text("height: 100%; width: 100%;");
|
|
web_sys::window()
|
|
.and_then(|win| win.document())
|
|
.and_then(|doc| doc.body())
|
|
.and_then(|body| body.append_child(&canvas).ok())
|
|
.expect("couldn't append canvas to document body");
|
|
wasm_bindgen_futures::spawn_local(run::<E>(event_loop, window));
|
|
}
|
|
}
|
|
|
|
// This allows treating the framework as a standalone example,
|
|
// thus avoiding listing the example names in `Cargo.toml`.
|
|
#[expect(dead_code)]
|
|
fn main() {}
|