diff --git a/blade-graphics/src/vulkan/command.rs b/blade-graphics/src/vulkan/command.rs index 58d6ede0..4d24dd01 100644 --- a/blade-graphics/src/vulkan/command.rs +++ b/blade-graphics/src/vulkan/command.rs @@ -417,6 +417,27 @@ impl super::CommandEncoder { update_data: &mut self.update_data, } } + + pub(super) fn check_gpu_crash(&self, ret: Result) -> T { + match ret { + Ok(value) => value, + Err(vk::Result::ERROR_DEVICE_LOST) => match self.crash_handler { + Some(ref ch) => { + let last_id = unsafe { *(ch.marker_buf.data() as *mut u32) }; + if last_id != 0 { + let (history, last_marker) = ch.extract(last_id); + log::error!("Last GPU executed marker is '{last_marker}'"); + log::info!("Marker history: {}", history); + } + panic!("GPU has crashed in {}", ch.name); + } + None => { + panic!("GPU has crashed, and no debug information is available."); + } + }, + Err(other) => panic!("GPU error {}", other), + } + } } #[hidden_trait::expose] diff --git a/blade-graphics/src/vulkan/mod.rs b/blade-graphics/src/vulkan/mod.rs index 3c02f67a..336648cb 100644 --- a/blade-graphics/src/vulkan/mod.rs +++ b/blade-graphics/src/vulkan/mod.rs @@ -434,24 +434,7 @@ impl crate::traits::CommandDevice for Context { .core .queue_submit(queue.raw, &[vk_info.build()], vk::Fence::null()) }; - match ret { - Ok(()) => (), - Err(vk::Result::ERROR_DEVICE_LOST) => match encoder.crash_handler { - Some(ref ch) => { - let last_id = unsafe { *(ch.marker_buf.data() as *mut u32) }; - if last_id != 0 { - let (history, last_marker) = ch.extract(last_id); - log::error!("Last GPU executed marker is '{last_marker}'"); - log::info!("Marker history: {}", history); - } - panic!("GPU has crashed in {}", ch.name); - } - None => { - panic!("GPU has crashed, and no debug information is available."); - } - }, - Err(other) => panic!("Submit error {}", other), - } + encoder.check_gpu_crash(ret); if let Some(presentation) = encoder.present.take() { let surface = self.surface.as_ref().unwrap().lock().unwrap(); @@ -462,12 +445,8 @@ impl crate::traits::CommandDevice for Context { .swapchains(&swapchains) .image_indices(&image_indices) .wait_semaphores(&wait_semaphores); - unsafe { - surface - .extension - .queue_present(queue.raw, &present_info) - .unwrap() - }; + let ret = unsafe { surface.extension.queue_present(queue.raw, &present_info) }; + let _ = encoder.check_gpu_crash(ret); } SyncPoint { progress } diff --git a/examples/scene/data/scene.ron b/examples/scene/data/scene.ron index 284cbaa8..5245eade 100644 --- a/examples/scene/data/scene.ron +++ b/examples/scene/data/scene.ron @@ -4,10 +4,10 @@ orientation: (-0.07, 0.36, 0.01, 0.93), fov_y: 1.0, max_depth: 100.0, - speed: 1000.0, + speed: 1000.0, ), average_luminocity: 0.3, - models: [ + objects: [ ( path: "monkey.gltf", ), diff --git a/examples/scene/main.rs b/examples/scene/main.rs index d34db16c..374b9bfd 100644 --- a/examples/scene/main.rs +++ b/examples/scene/main.rs @@ -3,9 +3,17 @@ use blade_graphics as gpu; use blade_render::{AssetHub, Camera, RenderConfig, Renderer}; -use std::{collections::VecDeque, fmt, fs, path::Path, sync::Arc, time}; +use std::{ + collections::VecDeque, + fmt, fs, + path::{Path, PathBuf}, + sync::Arc, + time, +}; const FRAME_TIME_HISTORY: usize = 30; +const RENDER_WHILE_LOADING: bool = true; +const MAX_DEPTH: f32 = 1e9; #[derive(Clone, Copy, PartialEq, strum::EnumIter)] enum DebugBlitInput { @@ -28,12 +36,60 @@ impl fmt::Display for DebugBlitInput { } } -#[derive(serde::Deserialize)] +struct TransformComponents { + scale: glam::Vec3, + rotation: glam::Quat, + translation: glam::Vec3, +} +impl From for TransformComponents { + fn from(bm: blade_graphics::Transform) -> Self { + let transposed = glam::Mat4 { + x_axis: bm.x.into(), + y_axis: bm.y.into(), + z_axis: bm.z.into(), + w_axis: glam::Vec4::W, + }; + let (scale, rotation, translation) = transposed.transpose().to_scale_rotation_translation(); + Self { + scale, + rotation, + translation, + } + } +} +impl TransformComponents { + fn to_blade(&self) -> blade_graphics::Transform { + let m = glam::Mat4::from_scale_rotation_translation( + self.scale, + self.rotation, + self.translation, + ) + .transpose(); + blade_graphics::Transform { + x: m.x_axis.into(), + y: m.y_axis.into(), + z: m.z_axis.into(), + } + } + fn is_inversible(&self) -> bool { + self.scale + .x + .abs() + .min(self.scale.y.abs()) + .min(self.scale.z.abs()) + > 0.01 + } +} + +struct ObjectExtra { + path: PathBuf, +} + +#[derive(serde::Deserialize, serde::Serialize)] struct ConfigCamera { position: mint::Vector3, orientation: mint::Quaternion, fov_y: f32, - max_depth: f32, speed: f32, } @@ -49,24 +105,26 @@ fn default_luminocity() -> f32 { 1.0 } -#[derive(serde::Deserialize)] -struct ConfigModel { +#[derive(serde::Deserialize, serde::Serialize)] +struct ConfigObject { path: String, #[serde(default = "default_transform")] transform: mint::RowMatrix3x4, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, serde::Serialize)] struct ConfigScene { camera: ConfigCamera, #[serde(default)] environment_map: String, #[serde(default = "default_luminocity")] average_luminocity: f32, - models: Vec, + objects: Vec, } struct Example { + scene_path: PathBuf, + scene_environment_map: String, prev_temp_buffers: Vec, prev_acceleration_structures: Vec, prev_sync_point: Option, @@ -78,6 +136,10 @@ struct Example { context: Arc, environment_map: Option>, objects: Vec, + object_extras: Vec, + selected_object_index: Option, + need_picked_selection_frames: usize, + gizmo_mode: egui_gizmo::GizmoMode, have_objects_changed: bool, camera: blade_render::Camera, fly_speed: f32, @@ -94,6 +156,7 @@ struct Example { debug_blit: Option, debug_blit_input: DebugBlitInput, workers: Vec, + choir: Arc, } impl Example { @@ -112,7 +175,7 @@ impl Example { } #[profiling::function] - fn new(window: &winit::window::Window, scene_path: &Path) -> Self { + fn new(window: &winit::window::Window) -> Self { log::info!("Initializing"); let context = Arc::new(unsafe { @@ -132,7 +195,7 @@ impl Example { let num_workers = num_cpus::get_physical().max((num_cpus::get() * 3 + 2) / 4); log::info!("Initializing Choir with {} workers", num_workers); - let choir = Arc::new(choir::Choir::new()); + let choir = choir::Choir::new(); let workers = (0..num_workers) .map(|i| choir.add_worker(&format!("Worker-{}", i))) .collect(); @@ -141,49 +204,6 @@ impl Example { let (shaders, shader_task) = blade_render::Shaders::load("blade-render/code/".as_ref(), &asset_hub); - let config_scene: ConfigScene = - ron::de::from_bytes(&fs::read(scene_path).expect("Unable to open the scene file")) - .expect("Unable to parse the scene file"); - - let camera = Camera { - pos: config_scene.camera.position, - rot: glam::Quat::from(config_scene.camera.orientation) - .normalize() - .into(), - fov_y: config_scene.camera.fov_y, - depth: config_scene.camera.max_depth, - }; - - let mut environment_map = None; - let mut objects = Vec::new(); - let parent = scene_path.parent().unwrap(); - let mut load_finish = choir.spawn("load finish").init_dummy(); - if !config_scene.environment_map.is_empty() { - let meta = blade_render::texture::Meta { - format: blade_graphics::TextureFormat::Rgba32Float, - generate_mips: false, - y_flip: false, - }; - let (texture, texture_task) = asset_hub - .textures - .load(parent.join(&config_scene.environment_map), meta); - load_finish.depend_on(texture_task); - environment_map = Some(texture); - } - for config_model in config_scene.models { - let (model, model_task) = asset_hub.models.load( - parent.join(&config_model.path), - blade_render::model::Meta { - generate_tangents: true, - }, - ); - load_finish.depend_on(model_task); - objects.push(blade_render::Object { - transform: config_model.transform, - model, - }); - } - log::info!("Spinning up the renderer"); shader_task.join(); let mut command_encoder = context.create_command_encoder(gpu::CommandEncoderDesc { @@ -208,20 +228,42 @@ impl Example { let gui_painter = blade_egui::GuiPainter::new(&context, surface_format); Self { + scene_path: PathBuf::new(), + scene_environment_map: String::new(), prev_temp_buffers: Vec::new(), prev_acceleration_structures: Vec::new(), prev_sync_point: Some(sync_point), renderer, - scene_load_task: Some(load_finish.run()), + scene_load_task: None, gui_painter, command_encoder: Some(command_encoder), asset_hub, context, - environment_map, - objects, + environment_map: None, + objects: Vec::new(), + object_extras: Vec::new(), + selected_object_index: None, + need_picked_selection_frames: 0, + gizmo_mode: egui_gizmo::GizmoMode::Translate, have_objects_changed: false, - camera, - fly_speed: config_scene.camera.speed, + camera: blade_render::Camera { + pos: mint::Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + rot: mint::Quaternion { + v: mint::Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + s: 1.0, + }, + fov_y: 0.0, + depth: 0.0, + }, + fly_speed: 0.0, debug: blade_render::DebugConfig::default(), track_hot_reloads: false, need_accumulation_reset: true, @@ -230,7 +272,7 @@ impl Example { render_times: VecDeque::with_capacity(FRAME_TIME_HISTORY), ray_config: blade_render::RayConfig { num_environment_samples: 1, - environment_importance_sampling: !config_scene.environment_map.is_empty(), + environment_importance_sampling: false, temporal_history: 10, spatial_taps: 1, spatial_tap_history: 5, @@ -242,13 +284,14 @@ impl Example { temporal_weight: 0.1, }, post_proc_config: blade_render::PostProcConfig { - average_luminocity: config_scene.average_luminocity, + average_luminocity: 1.0, exposure_key_value: 1.0 / 9.6, white_level: 1.0, }, debug_blit: None, debug_blit_input: DebugBlitInput::None, workers, + choir, } } @@ -262,6 +305,100 @@ impl Example { .destroy_command_encoder(self.command_encoder.take().unwrap()); } + pub fn load_scene(&mut self, scene_path: &Path) { + if self.scene_load_task.is_some() { + log::error!("Unable to reload the scene while something is loading"); + return; + } + + self.objects.clear(); + self.object_extras.clear(); + self.selected_object_index = None; + self.have_objects_changed = true; + + log::info!("Loading scene from: {}", scene_path.display()); + let config_scene: ConfigScene = + ron::de::from_bytes(&fs::read(scene_path).expect("Unable to open the scene file")) + .expect("Unable to parse the scene file"); + + self.camera = Camera { + pos: config_scene.camera.position, + rot: glam::Quat::from(config_scene.camera.orientation) + .normalize() + .into(), + fov_y: config_scene.camera.fov_y, + depth: MAX_DEPTH, + }; + self.fly_speed = config_scene.camera.speed; + self.ray_config.environment_importance_sampling = !config_scene.environment_map.is_empty(); + self.post_proc_config.average_luminocity = config_scene.average_luminocity; + + self.environment_map = None; + let parent = scene_path.parent().unwrap(); + let mut load_finish = self.choir.spawn("load finish").init_dummy(); + + if !config_scene.environment_map.is_empty() { + let meta = blade_render::texture::Meta { + format: blade_graphics::TextureFormat::Rgba32Float, + generate_mips: false, + y_flip: false, + }; + let (texture, texture_task) = self + .asset_hub + .textures + .load(parent.join(&config_scene.environment_map), meta); + load_finish.depend_on(texture_task); + self.environment_map = Some(texture); + } + for config_object in config_scene.objects { + let (model, model_task) = self.asset_hub.models.load( + parent.join(&config_object.path), + blade_render::model::Meta { + generate_tangents: true, + }, + ); + load_finish.depend_on(model_task); + self.objects.push(blade_render::Object { + transform: config_object.transform, + model, + }); + self.object_extras.push(ObjectExtra { + path: PathBuf::from(config_object.path), + }); + } + + self.scene_load_task = Some(load_finish.run()); + self.scene_path = scene_path.to_owned(); + self.scene_environment_map = config_scene.environment_map; + } + + pub fn save_scene(&self, scene_path: &Path) { + let config_scene = ConfigScene { + camera: ConfigCamera { + position: self.camera.pos, + orientation: self.camera.rot, + fov_y: self.camera.fov_y, + speed: self.fly_speed, + }, + environment_map: self.scene_environment_map.clone(), + average_luminocity: self.post_proc_config.average_luminocity, + objects: self + .objects + .iter() + .zip(self.object_extras.iter()) + .map(|(object, extra)| ConfigObject { + path: extra.path.to_string_lossy().into_owned(), + transform: object.transform, + }) + .collect(), + }; + + let string = ron::ser::to_string_pretty(&config_scene, ron::ser::PrettyConfig::default()) + .expect("Unable to form the scene file"); + fs::write(scene_path, &string).expect("Unable to write the scene file"); + log::info!("Saving scene to: {}", scene_path.display()); + } + #[profiling::function] fn wait_for_previous_frame(&mut self) { if let Some(sp) = self.prev_sync_point.take() { @@ -324,10 +461,10 @@ impl Example { } } - //TODO: remove these checks. // We should be able to update TLAS and render content // even while it's still being loaded. - if self.scene_load_task.is_none() { + if self.scene_load_task.is_none() || RENDER_WHILE_LOADING { + assert_eq!(self.objects.len(), self.object_extras.len()); if self.have_objects_changed { self.renderer.build_scene( command_encoder, @@ -350,10 +487,14 @@ impl Example { ); self.need_accumulation_reset = false; - self.renderer - .ray_trace(command_encoder, self.debug, self.ray_config); - if self.denoiser_enabled { - self.renderer.denoise(command_encoder, self.denoiser_config); + //TODO: figure out why the main RT pipeline + // causes a GPU crash when there are no objects + if !self.objects.is_empty() { + self.renderer + .ray_trace(command_encoder, self.debug, self.ray_config); + if self.denoiser_enabled { + self.renderer.denoise(command_encoder, self.denoiser_config); + } } } @@ -399,8 +540,48 @@ impl Example { .extend(temp_acceleration_structures); } + fn add_manipulation_gizmo(&mut self, obj_index: usize, ui: &mut egui::Ui) { + let view_matrix = + glam::Mat4::from_rotation_translation(self.camera.rot.into(), self.camera.pos.into()) + .inverse(); + let extent = self.renderer.get_screen_size(); + let aspect = extent.width as f32 / extent.height as f32; + let projection_matrix = + glam::Mat4::perspective_rh(self.camera.fov_y, aspect, 1.0, self.camera.depth); + let model_matrix = mint::ColumnMatrix4::from({ + let t = self.objects[obj_index].transform; + mint::RowMatrix4 { + x: t.x, + y: t.y, + z: t.z, + w: mint::Vector4 { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0, + }, + } + }); + let gizmo = egui_gizmo::Gizmo::new("Object") + .view_matrix(mint::ColumnMatrix4::from(view_matrix)) + .projection_matrix(mint::ColumnMatrix4::from(projection_matrix)) + .model_matrix(model_matrix) + .mode(self.gizmo_mode) + .orientation(egui_gizmo::GizmoOrientation::Global) + .snapping(true); + if let Some(response) = gizmo.interact(ui) { + let tc = TransformComponents { + scale: response.scale, + rotation: response.rotation, + translation: response.translation, + }; + self.objects[obj_index].transform = tc.to_blade(); + self.have_objects_changed = true; + } + } + #[profiling::function] - fn add_gui(&mut self, ui: &mut egui::Ui) { + fn populate_view(&mut self, ui: &mut egui::Ui) { use strum::IntoEnumIterator as _; let delta = self.last_render_time.elapsed(); @@ -422,6 +603,15 @@ impl Example { return; } + let mut selection = blade_render::SelectionInfo::default(); + if self.debug.mouse_pos.is_some() { + selection = self.renderer.read_debug_selection_info(); + if self.need_picked_selection_frames > 0 { + self.need_picked_selection_frames -= 1; + self.selected_object_index = self.find_object(selection.custom_index); + } + } + egui::CollapsingHeader::new("Camera").show(ui, |ui| { ui.horizontal(|ui| { ui.label("Position:"); @@ -438,8 +628,8 @@ impl Example { }); ui.add(egui::Slider::new(&mut self.camera.fov_y, 0.5f32..=2.0f32).text("FOV")); ui.add( - egui::Slider::new(&mut self.camera.depth, 1f32..=1_000_000f32) - .text("depth") + egui::Slider::new(&mut self.fly_speed, 1f32..=10000f32) + .text("Fly speed") .logarithmic(true), ); }); @@ -475,10 +665,7 @@ impl Example { } // selection info - let mut selection = blade_render::SelectionInfo::default(); if let Some(screen_pos) = self.debug.mouse_pos { - selection = self.renderer.read_debug_selection_info(); - let style = ui.style(); egui::Frame::group(style).show(ui, |ui| { ui.horizontal(|ui| { @@ -550,55 +737,6 @@ impl Example { } }); }); - - let view_matrix = glam::Mat4::from_rotation_translation( - self.camera.rot.into(), - self.camera.pos.into(), - ) - .inverse(); - let extent = self.renderer.get_screen_size(); - let aspect = extent.width as f32 / extent.height as f32; - let projection_matrix = glam::Mat4::perspective_rh( - self.camera.fov_y, - aspect, - 1.0, - self.camera.depth, - ); - let obj_index = self.find_object(selection.custom_index); - let model_matrix = mint::ColumnMatrix4::from({ - let t = self.objects[obj_index].transform; - mint::RowMatrix4 { - x: t.x, - y: t.y, - z: t.z, - w: mint::Vector4 { - x: 0.0, - y: 0.0, - z: 0.0, - w: 1.0, - }, - } - }); - let gizmo = egui_gizmo::Gizmo::new("Object") - .view_matrix(mint::ColumnMatrix4::from(view_matrix)) - .projection_matrix(mint::ColumnMatrix4::from(projection_matrix)) - .model_matrix(mint::ColumnMatrix4::from(model_matrix)) - .orientation(egui_gizmo::GizmoOrientation::Global) - .snapping(true); - if let Some(response) = gizmo.interact(ui) { - let m = glam::Mat4::from_scale_rotation_translation( - response.scale, - response.rotation, - response.translation, - ) - .transpose(); - self.objects[obj_index].transform = blade_graphics::Transform { - x: m.x_axis.into(), - y: m.y_axis.into(), - z: m.z_axis.into(), - }; - self.have_objects_changed = true; - } } // blits @@ -745,16 +883,127 @@ impl Example { }); } - fn find_object(&self, geometry_index: u32) -> usize { + #[profiling::function] + fn populate_content(&mut self, ui: &mut egui::Ui) { + ui.group(|ui| { + ui.colored_label(egui::Color32::WHITE, self.scene_path.display().to_string()); + ui.horizontal(|ui| { + if ui.button("Save").clicked() { + self.save_scene(&self.scene_path); + } + if ui.button("Reload").clicked() { + let path = self.scene_path.clone(); + self.load_scene(&path); + } + }); + }); + + egui::CollapsingHeader::new("Objects") + .default_open(true) + .show(ui, |ui| { + for (index, extra) in self.object_extras.iter().enumerate() { + let name = extra.path.file_name().unwrap().to_str().unwrap(); + ui.selectable_value(&mut self.selected_object_index, Some(index), name); + } + }); + + if let Some(index) = self.selected_object_index { + self.add_manipulation_gizmo(index, ui); + ui.group(|ui| { + ui.horizontal(|ui| { + if ui.button("Unselect").clicked() { + self.selected_object_index = None; + } + if ui.button("Delete!").clicked() { + self.selected_object_index = None; + self.objects.remove(index); + self.object_extras.remove(index); + self.have_objects_changed = true; + } + }) + }); + } + + if let Some(index) = self.selected_object_index { + egui::CollapsingHeader::new("Transform") + .default_open(true) + .show(ui, |ui| { + let object = self.objects.get_mut(index).unwrap(); + let mut tc = TransformComponents::from(object.transform); + ui.horizontal(|ui| { + ui.selectable_value( + &mut self.gizmo_mode, + egui_gizmo::GizmoMode::Translate, + "Translate", + ); + ui.add(egui::DragValue::new(&mut tc.translation.x)); + ui.add(egui::DragValue::new(&mut tc.translation.y)); + ui.add(egui::DragValue::new(&mut tc.translation.z)); + }); + ui.horizontal(|ui| { + ui.selectable_value( + &mut self.gizmo_mode, + egui_gizmo::GizmoMode::Rotate, + "Rotate", + ); + ui.add(egui::DragValue::new(&mut tc.rotation.x)); + ui.add(egui::DragValue::new(&mut tc.rotation.y)); + ui.add(egui::DragValue::new(&mut tc.rotation.z)); + ui.add(egui::DragValue::new(&mut tc.rotation.w)); + }); + ui.horizontal(|ui| { + ui.selectable_value( + &mut self.gizmo_mode, + egui_gizmo::GizmoMode::Scale, + "Scale", + ); + ui.add(egui::DragValue::new(&mut tc.scale.x)); + ui.add(egui::DragValue::new(&mut tc.scale.y)); + ui.add(egui::DragValue::new(&mut tc.scale.z)); + }); + + let transform = tc.to_blade(); + if object.transform != transform { + if tc.is_inversible() { + object.transform = transform; + self.have_objects_changed = true; + } + } + }); + } + } + + fn find_object(&self, geometry_index: u32) -> Option { let mut index = geometry_index as usize; - for (i, object) in self.objects.iter().enumerate() { + for (obj_index, object) in self.objects.iter().enumerate() { let model = &self.asset_hub.models[object.model]; match index.checked_sub(model.geometries.len()) { Some(i) => index = i, - None => return i, + None => return Some(obj_index), } } - panic!("Object with geometry index {geometry_index} is not found"); + None + } + + fn add_object(&mut self, file_path: &Path) -> bool { + if self.scene_load_task.is_some() { + return false; + } + let (model, model_task) = self.asset_hub.models.load( + file_path, + blade_render::model::Meta { + generate_tangents: true, + }, + ); + self.scene_load_task = Some(model_task.clone()); + self.objects.push(blade_render::Object { + transform: blade_graphics::IDENTITY_TRANSFORM, + model, + }); + self.object_extras.push(ObjectExtra { + path: file_path.to_owned(), + }); + true } fn move_camera_by(&mut self, offset: glam::Vec3) { @@ -789,7 +1038,8 @@ fn main() { .nth(1) .unwrap_or("examples/scene/data/scene.ron".to_string()); - let mut example = Example::new(&window, Path::new(&path_to_scene)); + let mut example = Example::new(&window); + example.load_scene(Path::new(&path_to_scene)); struct Drag { screen_pos: glam::IVec2, @@ -888,6 +1138,7 @@ fn main() { } => { example.debug.mouse_pos = Some(last_mouse_pos); example.is_debug_drawing = true; + example.need_picked_selection_frames = 3; } winit::event::WindowEvent::MouseInput { state: winit::event::ElementState::Released, @@ -910,6 +1161,14 @@ fn main() { example.debug.mouse_pos = None; } } + winit::event::WindowEvent::DroppedFile(file_path) => { + if !example.add_object(&file_path) { + log::warn!( + "Unable to drop {}, loading in progress", + file_path.display() + ); + } + } _ => {} } } @@ -928,16 +1187,27 @@ fn main() { ); frame }; - egui::SidePanel::right("control_panel") + egui::SidePanel::right("view") .frame(frame) .show(egui_ctx, |ui| { - example.add_gui(ui); + example.populate_view(ui); + }); + egui::SidePanel::left("content") + .frame(frame) + .show(egui_ctx, |ui| { + example.populate_content(ui); + ui.separator(); if ui.button("Quit").clicked() { quit = true; } }); }); + //HACK: https://github.com/urholaukkarinen/egui-gizmo/issues/29 + if example.have_objects_changed { + drag_start = None; + } + egui_winit.handle_platform_output(&window, &egui_ctx, egui_output.platform_output); let primitives = egui_ctx.tessellate(egui_output.shapes);