From d687ff022b35e1ef39be32efad755bcaf0b5a398 Mon Sep 17 00:00:00 2001 From: s-ol Date: Tue, 23 Dec 2025 14:50:48 +0100 Subject: wgpu, logging integrations --- src/main.rs | 42 ++++++----- src/preview.rs | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+), 20 deletions(-) create mode 100644 src/preview.rs (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 2258fba..75a5df4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![allow(clippy::use_self)] +use log::*; use std::collections::HashMap; use eframe::{App, CreationContext}; @@ -12,6 +13,8 @@ use egui_snarl::{ }, }; +mod preview; + const STRING_COLOR: Color32 = Color32::from_rgb(0x00, 0xb0, 0x00); const NUMBER_COLOR: Color32 = Color32::from_rgb(0xb0, 0x00, 0x00); const IMAGE_COLOR: Color32 = Color32::from_rgb(0xb0, 0x00, 0xb0); @@ -928,7 +931,7 @@ impl Expr { pub struct DemoApp { snarl: Snarl, - style: SnarlStyle, + preview: preview::Custom3d, } const fn default_style() -> SnarlStyle { @@ -975,15 +978,12 @@ impl DemoApp { }); // let snarl = Snarl::new(); - let style = cx.storage.map_or_else(default_style, |storage| { - storage - .get_string("style") - .and_then(|style| serde_json::from_str(&style).ok()) - .unwrap_or_else(default_style) - }); - // let style = SnarlStyle::new(); + info!("about to init"); - DemoApp { snarl, style } + DemoApp { + snarl, + preview: preview::Custom3d::new(cx).expect("Failed to init preview"), + } } } @@ -1011,13 +1011,7 @@ impl App for DemoApp { }); }); - egui::SidePanel::left("style").show(ctx, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - egui_probe::Probe::new(&mut self.style).show(ui); - }); - }); - - egui::SidePanel::right("selected-list").show(ctx, |ui| { + egui::SidePanel::left("selected-list").show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { ui.strong("Selected nodes"); @@ -1049,10 +1043,18 @@ impl App for DemoApp { }); }); + egui::SidePanel::right("preview").show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + egui::Frame::canvas(ui.style()).show(ui, |ui| { + self.preview.custom_painting(ui); + }); + }); + }); + egui::CentralPanel::default().show(ctx, |ui| { SnarlWidget::new() .id(Id::new("snarl-demo")) - .style(self.style) + .style(default_style()) .show(&mut self.snarl, &mut DemoViewer, ui); }); } @@ -1060,9 +1062,6 @@ impl App for DemoApp { fn save(&mut self, storage: &mut dyn eframe::Storage) { let snarl = serde_json::to_string(&self.snarl).unwrap(); storage.set_string("snarl", snarl); - - let style = serde_json::to_string(&self.style).unwrap(); - storage.set_string("style", style); } } @@ -1073,6 +1072,7 @@ fn main() -> eframe::Result<()> { viewport: egui::ViewportBuilder::default() .with_inner_size([400.0, 300.0]) .with_min_inner_size([300.0, 220.0]), + renderer: eframe::Renderer::Wgpu, ..Default::default() }; @@ -1095,6 +1095,8 @@ fn get_canvas_element() -> Option { // When compiling to web using trunk: #[cfg(target_arch = "wasm32")] fn main() { + eframe::WebLogger::init(log::LevelFilter::Debug).ok(); + let canvas = get_canvas_element().expect("Failed to find canvas with id 'egui_snarl_demo'"); let web_options = eframe::WebOptions::default(); diff --git a/src/preview.rs b/src/preview.rs new file mode 100644 index 0000000..33c6b77 --- /dev/null +++ b/src/preview.rs @@ -0,0 +1,232 @@ +#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps + +use log::*; +use std::num::NonZeroU64; + +use eframe::{ + egui_wgpu::wgpu::util::DeviceExt as _, + egui_wgpu::{self, wgpu}, +}; + +pub struct Custom3d { + angle: f32, +} + +impl Custom3d { + pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Option { + // Get the WGPU render state from the eframe creation context. This can also be retrieved + // from `eframe::Frame` when you don't have a `CreationContext` available. + info!("getting wgpu state"); + let wgpu_render_state = cc.wgpu_render_state.as_ref()?; + + let device = &wgpu_render_state.device; + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("custom3d"), + source: wgpu::ShaderSource::Wgsl( + " +struct VertexOut { + @location(0) color: vec4, + @builtin(position) position: vec4, +}; + +struct Uniforms { + @size(16) angle: f32, // pad to 16 bytes +}; + +@group(0) @binding(0) +var uniforms: Uniforms; + +var v_positions: array, 3> = array, 3>( + vec2(0.0, 1.0), + vec2(1.0, -1.0), + vec2(-1.0, -1.0), +); + +var v_colors: array, 3> = array, 3>( + vec4(1.0, 0.0, 0.0, 1.0), + vec4(0.0, 1.0, 0.0, 1.0), + vec4(0.0, 0.0, 1.0, 1.0), +); + +@vertex +fn vs_main(@builtin(vertex_index) v_idx: u32) -> VertexOut { + var out: VertexOut; + + out.position = vec4(v_positions[v_idx], 0.0, 1.0); + out.position.x = out.position.x * cos(uniforms.angle); + out.color = v_colors[v_idx]; + + return out; +} + +@fragment +fn fs_main(in: VertexOut) -> @location(0) vec4 { + return in.color; +} +" + .into(), + ), + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("custom3d"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: NonZeroU64::new(16), + }, + count: None, + }], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("custom3d"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("custom3d"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: None, + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu_render_state.target_format.into())], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("custom3d"), + contents: bytemuck::cast_slice(&[0.0_f32; 4]), // 16 bytes aligned! + // Mapping at creation (as done by the create_buffer_init utility) doesn't require us to to add the MAP_WRITE usage + // (this *happens* to workaround this bug ) + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("custom3d"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + // Because the graphics pipeline must have the same lifetime as the egui render pass, + // instead of storing the pipeline in our `Custom3d` struct, we insert it into the + // `paint_callback_resources` type map, which is stored alongside the render pass. + wgpu_render_state + .renderer + .write() + .callback_resources + .insert(TriangleRenderResources { + pipeline, + bind_group, + uniform_buffer, + }); + + Some(Self { angle: 0.0 }) + } +} + +// Callbacks in egui_wgpu have 3 stages: +// * prepare (per callback impl) +// * finish_prepare (once) +// * paint (per callback impl) +// +// The prepare callback is called every frame before paint and is given access to the wgpu +// Device and Queue, which can be used, for instance, to update buffers and uniforms before +// rendering. +// If [`egui_wgpu::Renderer`] has [`egui_wgpu::FinishPrepareCallback`] registered, +// it will be called after all `prepare` callbacks have been called. +// You can use this to update any shared resources that need to be updated once per frame +// after all callbacks have been processed. +// +// On both prepare methods you can use the main `CommandEncoder` that is passed-in, +// return an arbitrary number of user-defined `CommandBuffer`s, or both. +// The main command buffer, as well as all user-defined ones, will be submitted together +// to the GPU in a single call. +// +// The paint callback is called after finish prepare and is given access to egui's main render pass, +// which can be used to issue draw commands. +struct CustomTriangleCallback { + angle: f32, +} + +impl egui_wgpu::CallbackTrait for CustomTriangleCallback { + fn prepare( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + _screen_descriptor: &egui_wgpu::ScreenDescriptor, + _egui_encoder: &mut wgpu::CommandEncoder, + resources: &mut egui_wgpu::CallbackResources, + ) -> Vec { + let resources: &TriangleRenderResources = resources.get().unwrap(); + resources.prepare(device, queue, self.angle); + Vec::new() + } + + fn paint( + &self, + _info: egui::PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'static>, + resources: &egui_wgpu::CallbackResources, + ) { + let resources: &TriangleRenderResources = resources.get().unwrap(); + resources.paint(render_pass); + } +} + +impl Custom3d { + pub fn custom_painting(&mut self, ui: &mut egui::Ui) { + let (rect, response) = + ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); + + self.angle += response.drag_motion().x * 0.01; + ui.painter().add(egui_wgpu::Callback::new_paint_callback( + rect, + CustomTriangleCallback { angle: self.angle }, + )); + } +} + +struct TriangleRenderResources { + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, + uniform_buffer: wgpu::Buffer, +} + +impl TriangleRenderResources { + fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) { + // Update our uniform buffer with the angle from the UI + queue.write_buffer( + &self.uniform_buffer, + 0, + bytemuck::cast_slice(&[angle, 0.0, 0.0, 0.0]), + ); + } + + fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>) { + // Draw our triangle! + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.bind_group, &[]); + render_pass.draw(0..3, 0..1); + } +} -- cgit v1.2.3