diff --git a/libs/egui_dock b/libs/egui_dock index 01d4feb..a9dbb36 160000 --- a/libs/egui_dock +++ b/libs/egui_dock @@ -1 +1 @@ -Subproject commit 01d4febb7bd344a947cec4ddaf35aa3899061e33 +Subproject commit a9dbb366c5a647f798b425d11a6678fd85416bd6 diff --git a/src/compositor/bind.rs b/src/compositor/bind.rs index 3ecae1c..46dbe24 100644 --- a/src/compositor/bind.rs +++ b/src/compositor/bind.rs @@ -92,7 +92,7 @@ impl<'dev> BindingMapper<'dev> { chunks, output: { let mut views = Vec::new(); - views.resize_with(chunks as usize, || textures[0].make_view()); + views.resize_with(chunks as usize, || textures[0].create_view()); views.into_boxed_slice() }, map: HashMap::new(), @@ -108,7 +108,7 @@ impl<'dev> BindingMapper<'dev> { fn map_texture(&mut self, texture_index: usize) -> u32 { let mlen = self.map.len() as u32; *self.map.entry(texture_index).or_insert_with(|| { - self.output[mlen as usize] = self.textures[texture_index].make_view(); + self.output[mlen as usize] = self.textures[texture_index].create_view(); mlen }) } diff --git a/src/compositor/mod.rs b/src/compositor/mod.rs index ce6bca3..59b71d8 100644 --- a/src/compositor/mod.rs +++ b/src/compositor/mod.rs @@ -149,7 +149,7 @@ impl<'dev> CompositorStage<'dev> { Self { bindings: CpuBuffers::new(target.dev.chunks), buffers: GpuBuffers::new(&target.dev), - output: target.base_composite_texture(), + output: target.create_texture(), } } } @@ -182,6 +182,7 @@ impl<'dev> CompositorTarget<'dev> { /// Initial indices of the 2 triangle strips const INDICES: [u16; 4] = [0, 1, 2, 3]; + /// Create a new compositor target. pub fn new(dev: &'dev GpuHandle) -> Self { let device = &dev.device; @@ -190,7 +191,7 @@ impl<'dev> CompositorTarget<'dev> { let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("vertex_buffer"), contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); // Index draw buffer @@ -211,28 +212,31 @@ impl<'dev> CompositorTarget<'dev> { } } - pub fn base_composite_texture(&self) -> GpuTexture { + /// Create an empty texture for this compositor target. + fn create_texture(&self) -> GpuTexture { GpuTexture::empty_with_extent(&self.dev, self.dim.extent, GpuTexture::OUTPUT_USAGE) } - pub fn flip_vertices(&mut self, flip_hv: (bool, bool)) { + /// Flip the vertex data's foreground UV of the compositor target. + pub fn flip_vertices(&mut self, horizontal: bool, vertical: bool) { for v in &mut self.vertices { v.fg_coords = [ - if flip_hv.0 { + if horizontal { 1.0 - v.fg_coords[0] } else { v.fg_coords[0] }, - if flip_hv.1 { + if vertical { 1.0 - v.fg_coords[1] } else { v.fg_coords[1] }, ]; } - self.reload_vertices_buffer(); + self.load_vertex_buffer(); } + /// Rotate the vertex data's foreground UV of the compositor target. pub fn rotate_vertices(&mut self, ccw: bool) { let temp = self.vertices[0].fg_coords; if ccw { @@ -246,9 +250,15 @@ impl<'dev> CompositorTarget<'dev> { self.vertices[3].fg_coords = self.vertices[1].fg_coords; self.vertices[1].fg_coords = temp; } - self.reload_vertices_buffer(); + self.load_vertex_buffer(); } + /// Transpose the dimensions of the compositor target's output. + pub fn transpose_dimensions(&mut self) -> bool { + self.set_dimensions(self.dim.height, self.dim.width) + } + + /// Set the dimensions of the compositor target's output. pub fn set_dimensions(&mut self, width: u32, height: u32) -> bool { let buffer_dimensions = BufferDimensions::new(width, height); if self.dim == buffer_dimensions { @@ -271,21 +281,18 @@ impl<'dev> CompositorTarget<'dev> { true } - pub fn reload_vertices_buffer(&mut self) { - self.vertex_buffer = - self.dev - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("vertex_buffer"), - contents: bytemuck::cast_slice(&self.vertices), - usage: wgpu::BufferUsages::VERTEX, - }); + /// Load the GPU vertex buffer with updated data. + fn load_vertex_buffer(&mut self) { + self.dev + .queue + .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices)); } + /// Render composite layers using the compositor pipeline. pub fn render( &mut self, - compositor: &CompositorPipeline, - background: Option<[f32; 4]>, + pipeline: &CompositorPipeline, + bg: Option<[f32; 4]>, layers: &[CompositeLayer], textures: &[GpuTexture], ) { @@ -306,9 +313,9 @@ impl<'dev> CompositorTarget<'dev> { self.stages.push(CompositorStage::new(self)); } count += self.render_stage( - compositor, + pipeline, &mut encoder, - background, + bg, stage_idx, &layers[count..], textures, @@ -316,8 +323,10 @@ impl<'dev> CompositorTarget<'dev> { stage_idx += 1; } + // Truncate and remove stages that might no longer be necessary. self.stages.truncate(stage_idx); + // Copy the texture to the output of the target. encoder.copy_texture_to_texture( self.stages[stage_idx - 1].output.texture.as_image_copy(), self.output_texture @@ -332,19 +341,20 @@ impl<'dev> CompositorTarget<'dev> { })); } + /// Singular render pass. Many might be required to complete the compositing step. fn render_stage( &mut self, - compositor: &CompositorPipeline, + pipeline: &CompositorPipeline, encoder: &mut CommandEncoder, - background: Option<[f32; 4]>, + bg: Option<[f32; 4]>, stage_idx: usize, composite_layers: &[CompositeLayer], textures: &[GpuTexture], ) -> u32 { let composite_view = if stage_idx > 0 { - self.stages[stage_idx - 1].output.make_view() + self.stages[stage_idx - 1].output.create_view() } else { - self.base_composite_texture().make_view() + self.create_texture().create_view() }; let stage = &mut self.stages[stage_idx]; @@ -358,7 +368,7 @@ impl<'dev> CompositorTarget<'dev> { .dev .device .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &compositor.blending_bind_group_layout, + layout: &pipeline.blending_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -390,27 +400,28 @@ impl<'dev> CompositorTarget<'dev> { label: Some("mixing_bind_group"), }); - let output_view = stage.output.make_view(); + let output_view = stage.output.create_view(); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[ + // background color clear pass Some(wgpu::RenderPassColorAttachment { view: &output_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear( - background - .map(|[r, g, b, _]| wgpu::Color { - r: f64::from(r), - g: f64::from(g), - b: f64::from(b), - a: 1.0, - }) - .unwrap_or(wgpu::Color::TRANSPARENT), + bg.map(|[r, g, b, _]| wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: 1.0, + }) + .unwrap_or(wgpu::Color::TRANSPARENT), ), store: true, }, }), + // compositing pass Some(wgpu::RenderPassColorAttachment { view: &output_view, resolve_target: None, @@ -424,13 +435,14 @@ impl<'dev> CompositorTarget<'dev> { }); // Finish and set the render pass's binding groups and data - render_pass.set_pipeline(&compositor.render_pipeline); + render_pass.set_pipeline(&pipeline.render_pipeline); + // We use push constants for the binding count. render_pass.set_push_constants( wgpu::ShaderStages::FRAGMENT, 0, &stage.bindings.count.to_ne_bytes(), ); - render_pass.set_bind_group(0, &compositor.constant_bind_group, &[]); + render_pass.set_bind_group(0, &pipeline.constant_bind_group, &[]); render_pass.set_bind_group(1, &blending_bind_group, &[]); render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); @@ -449,6 +461,7 @@ pub struct CompositorPipeline { } impl CompositorPipeline { + /// Create a new compositor pipeline. pub fn new(dev: &GpuHandle) -> Self { let device = &dev.device; @@ -593,11 +606,16 @@ impl CompositorPipeline { } } -pub fn shader_load() -> wgpu::ShaderModuleDescriptor<'static> { +/// Load the shader. +fn shader_load() -> wgpu::ShaderModuleDescriptor<'static> { + // In release mode, the final binary includes the file directly so that + // the binary does not rely on the shader file being at a specific location. #[cfg(not(debug_assertions))] { wgpu::include_wgsl!("../shader.wgsl") } + // In debug mode, this reads directly from a file so that recompilation + // will not be necessary in the event that only the shader file changes. #[cfg(debug_assertions)] { wgpu::ShaderModuleDescriptor { diff --git a/src/compositor/tex.rs b/src/compositor/tex.rs index afc4688..bad18f8 100644 --- a/src/compositor/tex.rs +++ b/src/compositor/tex.rs @@ -51,7 +51,7 @@ impl GpuTexture { } /// Make a texture view of this GPU texture. - pub fn make_view(&self) -> wgpu::TextureView { + pub fn create_view(&self) -> wgpu::TextureView { self.texture .create_view(&wgpu::TextureViewDescriptor::default()) } @@ -66,7 +66,7 @@ impl GpuTexture { encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.make_view(), + view: &self.create_view(), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(color), @@ -116,7 +116,7 @@ impl GpuTexture { /// `dev` should be the same device that created this texture /// in the first place. pub fn clone(&self, dev: &GpuHandle) -> Self { - let c = Self::empty_with_extent( + let clone = Self::empty_with_extent( dev, self.size, Self::OUTPUT_USAGE | wgpu::TextureUsages::COPY_DST, @@ -128,12 +128,12 @@ impl GpuTexture { // Copy the data from the texture to the buffer encoder.copy_texture_to_texture( self.texture.as_image_copy(), - c.texture.as_image_copy(), + clone.texture.as_image_copy(), self.size, ); encoder.finish() })); - c + clone } /// Export the texture to the given path. @@ -151,6 +151,7 @@ impl GpuTexture { mapped_at_creation: false, }); + // Copy the texture to the output buffer dev.queue.submit(Some({ let mut encoder = dev .device @@ -195,6 +196,7 @@ impl GpuTexture { let buffer = image::imageops::crop_imm(&buffer, 0, 0, dim.width, dim.height).to_image(); + eprintln!("Saving the file to {}", path.display()); tokio::task::spawn_blocking(move || buffer.save(path)) .await .unwrap() diff --git a/src/gui/canvas.rs b/src/gui/canvas.rs index aad7d52..70e0b2a 100644 --- a/src/gui/canvas.rs +++ b/src/gui/canvas.rs @@ -3,7 +3,6 @@ use egui::*; /// 2D bounding box of f64 precision. /// The range of data values we show. #[derive(Clone, Copy, PartialEq, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct CanvasViewBounds { pub(crate) min: [f32; 2], pub(crate) max: [f32; 2], @@ -284,7 +283,6 @@ impl From for AutoBounds { } /// Information about the plot that has to persist between frames. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] struct ViewMemory { auto_bounds: AutoBounds, @@ -573,7 +571,7 @@ impl CanvasView { }; memory.store(ui.ctx(), plot_id); - // let response = response.on_hover_cursor(CursorIcon::Crosshair); + let response = response.on_hover_cursor(CursorIcon::Crosshair); InnerResponse { inner: (), @@ -665,8 +663,8 @@ impl PreparedView { .paint_at(&mut plot_ui, rect); } - let painter = plot_ui.painter(); if self.show_extended_crosshair { + let painter = plot_ui.painter(); if let Some(pointer) = response.hover_pos() { painter.vline( pointer.x, diff --git a/src/gui/layout.rs b/src/gui/layout.rs index a1c9fb3..beb1b57 100644 --- a/src/gui/layout.rs +++ b/src/gui/layout.rs @@ -6,6 +6,7 @@ use crate::{ silica::{BlendingMode, SilicaHierarchy}, }; use egui::*; +use egui_dock::NodeIndex; use parking_lot::{Mutex, RwLock}; use std::collections::HashMap; use std::sync::atomic::Ordering::{Acquire, Release}; @@ -13,6 +14,14 @@ use std::sync::atomic::{AtomicBool, AtomicUsize}; use super::canvas; +#[derive(Clone, Copy)] +pub struct StaticRefs { + pub dev: &'static GpuHandle, + pub compositor: &'static CompositorHandle, + pub toasts: &'static Mutex, + pub added_instances: &'static Mutex>, +} + #[derive(Hash, Clone, Copy, PartialEq, Eq)] pub struct InstanceKey(pub usize); @@ -54,36 +63,33 @@ pub struct CompositorHandle { pub pipeline: CompositorPipeline, } -async fn load_dialog( - dev: &'static GpuHandle, - compositor: &CompositorHandle, - toasts: &'static Mutex, -) { +async fn load_dialog(statics: StaticRefs, node_index: NodeIndex) { if let Some(handle) = rfd::AsyncFileDialog::new() - .add_filter("procreate", &["procreate"]) + .add_filter("Procreate Files", &["procreate"]) .pick_file() .await { - if let Err(err) = super::load_file(handle.path().to_path_buf(), dev, compositor).await { - toasts.lock().error(format!( - "File {} failed to load. Reason: {err}", - handle.file_name() - )); - } else { - toasts - .lock() - .success(format!("File {} successfully opened.", handle.file_name())); + match super::load_file(handle.path().to_path_buf(), statics.dev, statics.compositor).await { + Err(err) => { + statics.toasts.lock().error(format!( + "File {} failed to load. Reason: {err}", + handle.file_name() + )); + } + Ok(key) => { + statics + .toasts + .lock() + .success(format!("File {} successfully opened.", handle.file_name())); + statics.added_instances.lock().push((node_index, key)); + } } } else { - toasts.lock().info("Load cancelled."); + statics.toasts.lock().info("Load cancelled."); } } -async fn save_dialog( - dev: &'static GpuHandle, - copied_texture: GpuTexture, - toasts: &'static Mutex, -) { +async fn save_dialog(statics: StaticRefs, copied_texture: GpuTexture) { if let Some(handle) = rfd::AsyncFileDialog::new() .add_filter("png", image::ImageFormat::Png.extensions_str()) .add_filter("jpeg", image::ImageFormat::Jpeg.extensions_str()) @@ -96,36 +102,39 @@ async fn save_dialog( { let dim = BufferDimensions::from_extent(copied_texture.size); let path = handle.path().to_path_buf(); - if let Err(err) = copied_texture.export(dev, dim, path).await { - toasts.lock().error(format!( + if let Err(err) = copied_texture.export(statics.dev, dim, path).await { + statics.toasts.lock().error(format!( "File {} failed to export. Reason: {err}.", handle.file_name() )); } else { - toasts.lock().success(format!( + statics.toasts.lock().success(format!( "File {} successfully exported.", handle.file_name() )); } } else { - toasts.lock().info("Export cancelled."); + statics.toasts.lock().info("Export cancelled."); } } struct ControlsGui<'a> { - dev: &'static GpuHandle, + statics: &'a StaticRefs, rt: &'static tokio::runtime::Runtime, + selected_canvas: &'a InstanceKey, - compositor: &'static CompositorHandle, view_options: &'a mut ViewOptions, - toasts: &'static Mutex, } impl ControlsGui<'_> { fn layout_info(&self, ui: &mut Ui) { Grid::new("File Grid").show(ui, |ui| { - if let Some(Instance { file, .. }) = - self.compositor.instances.read().get(self.selected_canvas) + if let Some(Instance { file, .. }) = self + .statics + .compositor + .instances + .read() + .get(self.selected_canvas) { let file = file.read(); ui.label("Name"); @@ -145,33 +154,6 @@ impl ControlsGui<'_> { }); } - fn layout_file_control(&mut self, ui: &mut Ui) { - let instances = self.compositor.instances.read(); - Grid::new("File Grid").num_columns(2).show(ui, |ui| { - ui.label("Actions"); - ui.vertical(|ui| { - if ui.button("Open Other File").clicked() { - self.rt - .spawn(load_dialog(self.dev, self.compositor, self.toasts)); - } - if let Some(instance) = instances.get(self.selected_canvas) { - if ui.button("Export View").clicked() { - let copied_texture = instance - .target - .lock() - .output_texture - .as_ref() - .unwrap() - .clone(self.dev); - - self.rt - .spawn(save_dialog(self.dev, copied_texture, self.toasts)); - } - } - }); - }); - } - fn layout_view_control(&mut self, ui: &mut Ui) { Grid::new("View Grid").show(ui, |ui| { ui.label("Grid View"); @@ -188,7 +170,13 @@ impl ControlsGui<'_> { .checkbox(&mut self.view_options.smooth, "Enable") .changed() { - if let Some(instance) = self.compositor.instances.read().get(self.selected_canvas) { + if let Some(instance) = self + .statics + .compositor + .instances + .read() + .get(self.selected_canvas) + { instance.store_new_texture_or(true); } } @@ -203,17 +191,23 @@ impl ControlsGui<'_> { } fn layout_canvas_control(&mut self, ui: &mut Ui) { - if let Some(instance) = self.compositor.instances.read().get(self.selected_canvas) { + if let Some(instance) = self + .statics + .compositor + .instances + .read() + .get(self.selected_canvas) + { Grid::new("Canvas Grid").show(ui, |ui| { ui.label("Flip"); ui.horizontal(|ui| { if ui.button("Horizontal").clicked() { - instance.target.lock().flip_vertices((false, true)); + instance.target.lock().flip_vertices(false, true); instance.store_change_or(true); instance.store_new_texture_or(true); } if ui.button("Vertical").clicked() { - instance.target.lock().flip_vertices((true, false)); + instance.target.lock().flip_vertices(true, false); instance.store_change_or(true); instance.store_new_texture_or(true); } @@ -223,20 +217,40 @@ impl ControlsGui<'_> { ui.horizontal(|ui| { if ui.button("CCW").clicked() { let mut target = instance.target.lock(); - let dim = target.dim; target.rotate_vertices(true); - instance.store_new_texture_or(target.set_dimensions(dim.height, dim.width)); + instance.store_new_texture_or(target.transpose_dimensions()); instance.store_change_or(true); } if ui.button("CW").clicked() { let mut target = instance.target.lock(); - let dim = target.dim; target.rotate_vertices(false); - instance.store_new_texture_or(target.set_dimensions(dim.height, dim.width)); + instance.store_new_texture_or(target.transpose_dimensions()); instance.store_change_or(true); } }); }); + let instances = self.statics.compositor.instances.read(); + if let Some(instance) = instances.get(self.selected_canvas) { + ui.separator(); + Grid::new("File Grid").num_columns(2).show(ui, |ui| { + ui.label("Actions"); + ui.vertical(|ui| { + if ui.button("Export View").clicked() { + let copied_texture = instance + .target + .lock() + .output_texture + .as_ref() + .unwrap() + .clone(self.statics.dev); + + self.rt.spawn(save_dialog(*self.statics, copied_texture)); + } + }); + }); + } + } else { + ui.label("No canvas loaded."); } } @@ -306,13 +320,17 @@ impl ControlsGui<'_> { } fn layout_layers(&self, ui: &mut Ui) { - if let Some(instance) = self.compositor.instances.read().get(self.selected_canvas) { - let mut i = 0; - let mut changed = false; + if let Some(instance) = self + .statics + .compositor + .instances + .read() + .get(self.selected_canvas) + { let mut file = instance.file.write(); - Self::layout_layers_sub(ui, &mut file.layers, &mut i, &mut changed); + let mut changed = false; - ui.separator(); + // Let background controls be first since color controls are bad. Grid::new("layers.background").show(ui, |ui| { ui.label("Background"); changed |= ui.checkbox(&mut file.background_hidden, "Hidden").changed(); @@ -324,12 +342,15 @@ impl ControlsGui<'_> { let bg = unsafe { &mut *(file.background_color.as_mut_ptr() as *mut [f32; 3]) }; changed |= ui.color_edit_button_rgb(bg).changed(); }); + + ui.separator(); + + let mut i = 0; + Self::layout_layers_sub(ui, &mut file.layers, &mut i, &mut changed); instance.store_change_or(changed); } else { - ui.centered_and_justified(|ui| { - ui.label("No file hierachy."); - }); + ui.label("No file hierachy."); } } } @@ -343,25 +364,21 @@ pub struct ViewOptions { } pub struct ViewerGui { - pub dev: &'static GpuHandle, + pub statics: StaticRefs, pub rt: &'static tokio::runtime::Runtime, - pub compositor: &'static CompositorHandle, pub canvases: HashMap, - pub selected_canvas: InstanceKey, - pub view_options: ViewOptions, - pub queued_remove: Option, - pub canvas_tree: egui_dock::Tree, - pub viewer_tree: egui_dock::Tree, - pub toasts: &'static Mutex, } struct CanvasGui<'a> { + statics: &'a StaticRefs, + rt: &'static tokio::runtime::Runtime, + canvases: &'a mut HashMap, instances: &'a HashMap, view_options: &'a ViewOptions, @@ -389,6 +406,10 @@ impl egui_dock::TabViewer for CanvasGui<'_> { true } + fn on_add(&mut self, node: egui_dock::NodeIndex) { + self.rt.spawn(load_dialog(*self.statics, node)); + } + fn title(&mut self, tab: &mut Self::Tab) -> WidgetText { self.instances .get(tab) @@ -401,43 +422,49 @@ impl egui_dock::TabViewer for CanvasGui<'_> { impl ViewerGui { pub fn remove_index(&mut self, index: InstanceKey) { self.canvases.remove(&index); - self.compositor.instances.write().remove(&index); + self.statics.compositor.instances.write().remove(&index); } fn layout_view(&mut self, ui: &mut Ui) { ui.set_min_size(ui.available_size()); - let mut instances = self.compositor.instances.read(); + let mut instances = self.statics.compositor.instances.read(); if instances.is_empty() { - ui.centered_and_justified(|ui| { - if ui - .button("Load a Procreate file to begin viewing it.") - .clicked() - { - self.rt - .spawn(load_dialog(self.dev, self.compositor, self.toasts)); + ui.allocate_space(vec2( + 0.0, + ui.available_height() / 2.0 - ui.text_style_height(&style::TextStyle::Button), + )); + ui.vertical_centered(|ui| { + ui.label("Drag and drop Procreate file to view it."); + if ui.button("Load Procreate File").clicked() { + self.rt.spawn(load_dialog(self.statics, NodeIndex::root())); } }); } else { - for id in self - .canvases - .keys() - .filter(|i| self.canvas_tree.find_tab(i).is_none()) - .copied() - .collect::>() - { - self.canvas_tree.push_to_first_leaf(id); + if let Some(mut added_instances) = self.statics.added_instances.try_lock() { + for (node, id) in added_instances.drain(..) { + self.canvas_tree.set_focused_node(node); + self.canvas_tree.push_to_focused_leaf(id); + } } + if let Some((_, id)) = self.canvas_tree.find_active_focused() { self.selected_canvas = *id; } egui_dock::DockArea::new(&mut self.canvas_tree) .id(Id::new("view.dock")) - .style(egui_dock::Style::from_egui(ui.style().as_ref())) + .style( + egui_dock::StyleBuilder::from_egui(ui.style().as_ref()) + .show_add_buttons(true) + .build(), + ) .show_inside( ui, &mut CanvasGui { + statics: &self.statics, + rt: &self.rt, + view_options: &self.view_options, canvases: &mut self.canvases, instances: &mut instances, @@ -461,12 +488,10 @@ impl ViewerGui { .show_inside( ui, &mut ControlsGui { - dev: &mut self.dev, + statics: &self.statics, + rt: &self.rt, selected_canvas: &self.selected_canvas, view_options: &mut &mut self.view_options, - compositor: &self.compositor, - rt: self.rt, - toasts: self.toasts, }, ); }); @@ -484,7 +509,6 @@ pub enum ViewerTab { Information, ViewControls, CanvasControls, - Files, Hierarchy, } @@ -499,7 +523,6 @@ impl egui_dock::TabViewer for ControlsGui<'_> { ViewerTab::ViewControls => self.layout_view_control(ui), ViewerTab::CanvasControls => self.layout_canvas_control(ui), ViewerTab::Hierarchy => self.layout_layers(ui), - ViewerTab::Files => self.layout_file_control(ui), }); } @@ -509,7 +532,6 @@ impl egui_dock::TabViewer for ControlsGui<'_> { ViewerTab::ViewControls => "View", ViewerTab::CanvasControls => "Canvas", ViewerTab::Hierarchy => "Hierarchy", - ViewerTab::Files => "Files", } .into() } diff --git a/src/gui/mod.rs b/src/gui/mod.rs index ab18519..5f57f1a 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,7 +1,7 @@ mod canvas; mod layout; -use self::layout::{CompositorHandle, Instance, InstanceKey, ViewOptions, ViewerGui}; +use self::layout::{CompositorHandle, Instance, InstanceKey, StaticRefs, ViewOptions, ViewerGui}; use crate::{ compositor::{dev::GpuHandle, CompositeLayer, CompositorPipeline, CompositorTarget}, gui::layout::ViewerTab, @@ -62,32 +62,35 @@ async fn rendering_thread(cs: &CompositorHandle) { // to avoid putting unnecessary computational pressure on the GPU. limiter.tick().await; - for (_, instance) in cs.instances.read().iter() { - let file = instance.file.read(); - - let new_layer_config = instance.file.read().layers.clone(); - // Only force a recompute if we need to. - let background = (!file.background_hidden).then_some(file.background_color); - - drop(file); - - if instance.change_untick() { - let mut resolved_layers = Vec::new(); - let mut mask_layer = None; - linearize(&new_layer_config, &mut resolved_layers, &mut mask_layer); - - let mut lock = instance.target.lock(); - lock.render( - &cs.pipeline, - background, - &resolved_layers, - &instance.textures, - ); - // ENABLE TO DEBUG: hold the lock to make sure the GUI is responsive - // std::thread::sleep(std::time::Duration::from_secs(1)); - // Debugging notes: if the GPU is highly contended, the main - // GUI rendering can still be somewhat sluggish. - drop(lock); + for instance in cs.instances.read().values() { + // If the file is contended then it might be edited by the GUI. + // Might as well not render a soon to be outdated result. + if let Some(file) = instance.file.try_read() { + let new_layer_config = file.layers.clone(); + // Only force a recompute if we need to. + let background = (!file.background_hidden).then_some(file.background_color); + + // Drop the guard here, we no longer need it. + drop(file); + + if instance.change_untick() { + let mut resolved_layers = Vec::new(); + let mut mask_layer = None; + linearize(&new_layer_config, &mut resolved_layers, &mut mask_layer); + + let mut lock = instance.target.lock(); + lock.render( + &cs.pipeline, + background, + &resolved_layers, + &instance.textures, + ); + // ENABLE TO DEBUG: hold the lock to make sure the GUI is responsive + // std::thread::sleep(std::time::Duration::from_secs(1)); + // Debugging notes: if the GPU is highly contended, the main + // GUI rendering can still be somewhat sluggish. + drop(lock); + } } } } @@ -97,10 +100,10 @@ pub async fn load_file( path: PathBuf, dev: &'static GpuHandle, compositor: &CompositorHandle, -) -> Result<(), SilicaError> { +) -> Result { let (file, textures) = ProcreateFile::open(path, dev).await?; let mut target = CompositorTarget::new(dev); - target.flip_vertices((file.flipped.horizontally, file.flipped.vertically)); + target.flip_vertices(file.flipped.horizontally, file.flipped.vertically); target.set_dimensions(file.size.width, file.size.height); for _ in 0..file.orientation { @@ -110,8 +113,9 @@ pub async fn load_file( let id = compositor.curr_id.load(Acquire); compositor.curr_id.store(id + 1, Release); + let key = InstanceKey(id); compositor.instances.write().insert( - InstanceKey(id), + key, Instance { file: RwLock::new(file), target: Mutex::new(target), @@ -120,7 +124,7 @@ pub async fn load_file( changed: AtomicBool::new(true), }, ); - Ok(()) + Ok(key) } fn leak(value: T) -> &'static T { @@ -128,25 +132,38 @@ fn leak(value: T) -> &'static T { } pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::EventLoop<()>) -> ! { - // LEAK: obtain static reference because this will live for the rest of - // the lifetime of the program. - let rt = leak( - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(), - ); - let (dev, surface) = rt.block_on(GpuHandle::with_window(&window)).unwrap(); - let dev = leak(dev); - let compositor = leak(CompositorHandle { - instances: RwLock::new(HashMap::new()), - pipeline: CompositorPipeline::new(dev), - curr_id: AtomicUsize::new(0), - }); - let toasts = leak(Mutex::new(egui_notify::Toasts::default())); + let (statics, surface, rt) = { + // LEAK: obtain static reference because this will live for the rest of + // the lifetime of the program. This is simpler to handle than Arc hell. + let rt = leak( + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(), + ); + let (dev, surface) = rt.block_on(GpuHandle::with_window(&window)).unwrap(); + let dev = leak(dev); + let compositor = leak(CompositorHandle { + instances: RwLock::new(HashMap::new()), + pipeline: CompositorPipeline::new(dev), + curr_id: AtomicUsize::new(0), + }); + let toasts = leak(Mutex::new(egui_notify::Toasts::default())); + let added_instances = leak(Mutex::new(Vec::with_capacity(1))); + ( + StaticRefs { + dev, + compositor: &compositor, + toasts, + added_instances, + }, + surface, + rt, + ) + }; let window_size = window.inner_size(); - let surface_format = surface.get_supported_formats(&dev.adapter)[0]; + let surface_format = surface.get_supported_formats(&statics.dev.adapter)[0]; let mut surface_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, @@ -158,7 +175,7 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E size_in_pixels: [surface_config.width, surface_config.height], pixels_per_point: window.scale_factor() as f32, }; - surface.configure(&dev.device, &surface_config); + surface.configure(&statics.dev.device, &surface_config); let mut state = egui_winit::State::new(&event_loop); state.set_pixels_per_point(window.scale_factor() as f32); @@ -166,10 +183,10 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E let context = egui::Context::default(); context.set_pixels_per_point(window.scale_factor() as f32); - let mut egui_rpass = RenderPass::new(&dev.device, surface_format, 1); + let mut egui_rpass = RenderPass::new(&statics.dev.device, surface_format, 1); let mut editor = ViewerGui { - dev, + statics, rt, canvases: HashMap::new(), view_options: ViewOptions { @@ -180,7 +197,6 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E bottom_bar: false, }, selected_canvas: InstanceKey(0), - compositor: &compositor, canvas_tree: egui_dock::Tree::default(), viewer_tree: { use egui_dock::{NodeIndex, Tree}; @@ -192,15 +208,14 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E tree.split_below( NodeIndex::root(), 0.4, - vec![ViewerTab::Files, ViewerTab::Hierarchy], + vec![ViewerTab::Hierarchy], ); tree }, queued_remove: None, - toasts, }; - rt.spawn(rendering_thread(compositor)); + rt.spawn(rendering_thread(statics.compositor)); event_loop.run(move |event, _, control_flow| { match event { @@ -217,18 +232,20 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E surface_config.width = size.width; surface_config.height = size.height; screen_descriptor.size_in_pixels = [size.width, size.height]; - surface.configure(&dev.device, &surface_config); + surface.configure(&statics.dev.device, &surface_config); } } WindowEvent::DroppedFile(file) => { println!("File dropped: {:?}", file.as_path().display().to_string()); rt.spawn(async move { - if let Err(err) = load_file(file, &dev, compositor).await { - toasts.lock().error(format!( + if let Err(err) = + load_file(file, &statics.dev, statics.compositor).await + { + statics.toasts.lock().error(format!( "File from drag/drop failed to load. Reason: {err}" )); } else { - toasts.lock().success("Loaded file from drag/drop."); + statics.toasts.lock().success("Loaded file from drag/drop."); } }); } @@ -261,7 +278,7 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E context.begin_frame(input); editor.layout_gui(&context); - editor.toasts.lock().show(&context); + editor.statics.toasts.lock().show(&context); let output = context.end_frame(); state.handle_platform_output(&window, &context, output.platform_output); @@ -271,15 +288,26 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E // Upload all resources for the GPU. for (id, image_delta) in output.textures_delta.set { - egui_rpass.update_texture(&dev.device, &dev.queue, id, &image_delta); + egui_rpass.update_texture( + &statics.dev.device, + &statics.dev.queue, + id, + &image_delta, + ); } for id in output.textures_delta.free { egui_rpass.free_texture(&id); } - egui_rpass.update_buffers(&dev.device, &dev.queue, &paint_jobs, &screen_descriptor); + egui_rpass.update_buffers( + &statics.dev.device, + &statics.dev.queue, + &paint_jobs, + &screen_descriptor, + ); - dev.queue.submit(Some({ - let mut encoder = dev + statics.dev.queue.submit(Some({ + let mut encoder = statics + .dev .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); @@ -304,7 +332,7 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E // Do not block on any locks/rwlocks since we do not want to block // the GUI thread when the renderer is potentially taking a long // time to render a frame. - if let Some(instances) = compositor.instances.try_read() { + if let Some(instances) = statics.compositor.instances.try_read() { for (idx, instance) in instances.iter() { if instance.new_texture.load(Acquire) { if let Some(target) = instance.target.try_lock() { @@ -312,8 +340,8 @@ pub fn start_gui(window: winit::window::Window, event_loop: winit::event_loop::E *idx, ( egui_rpass.register_native_texture( - &dev.device, - &target.output_texture.as_ref().unwrap().make_view(), + &statics.dev.device, + &target.output_texture.as_ref().unwrap().create_view(), if editor.view_options.smooth { wgpu::FilterMode::Linear } else { diff --git a/src/main.rs b/src/main.rs index c61014d..25f594a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ fn main() -> Result<(), Box> { .with_decorations(true) .with_resizable(true) .with_transparent(false) - .with_title("Procreate Viewer") + .with_title("Silicate") .with_inner_size(INITIAL_SIZE) .with_window_icon(taskbar_icon) .build(&event_loop)?;