From 1641585177b091ccd048d69583e392825ac0dcfc Mon Sep 17 00:00:00 2001 From: Tsuki Date: Tue, 19 Nov 2024 02:24:47 +0800 Subject: [PATCH] sync --- Cargo.lock | 22 ++ backup.txt | 24 ++ element_bridge/Cargo.toml | 5 + element_bridge/src/lib.rs | 74 +++++- mp/Cargo.toml | 6 +- mp/src/app.rs | 107 ++++++-- mp/src/app_ui.rs | 41 +-- mp/src/handle_events.rs | 44 ++++ mp/src/lib.rs | 4 +- mp/src/render/camera.rs | 1 - mp/src/render/data.rs | 46 ---- mp/src/render/mod.rs | 2 - mp/src/render_task.rs | 123 +++++++++ mp/src/shaders/colormap.rs | 63 ----- mp/src/shaders/ppi.rs | 69 ----- mp/src/widgets/area.rs | 135 ++++++++-- mp/src/widgets/mod.rs | 1 + mp/src/widgets/renderer.rs | 110 ++++++++ mp/src/windows_manager.rs | 73 ++++++ mp_elements/Cargo.toml | 8 +- mp_elements/build.rs | 5 + mp_elements/image.png | Bin 71369 -> 54175 bytes mp_elements/shaders/colormap.wgsl | 14 +- mp_elements/shaders/elements/ppi.wgsl | 10 +- mp_elements/shaders/elements/ppi_merged.wgsl | 38 ++- mp_elements/src/app.rs | 243 +++++++++++------- mp_elements/src/elements/mod.rs | 53 +++- mp_elements/src/elements/ppi.rs | 42 ++- mp_elements/src/elementvec.rs | 12 +- mp_elements/src/lib.rs | 1 + mp_elements/src/renderer/camera.rs | 1 + mp_elements/src/renderer/projection.rs | 12 + mp_elements/src/tools/colormap.rs | 212 +++++++++++++++ .../shaders => mp_elements/src/tools}/mod.rs | 1 - mp_elements/src/utils.rs | 4 +- 35 files changed, 1218 insertions(+), 388 deletions(-) create mode 100644 backup.txt create mode 100644 mp/src/handle_events.rs delete mode 100644 mp/src/render/camera.rs delete mode 100644 mp/src/render/data.rs delete mode 100644 mp/src/render/mod.rs create mode 100644 mp/src/render_task.rs delete mode 100644 mp/src/shaders/colormap.rs delete mode 100644 mp/src/shaders/ppi.rs create mode 100644 mp/src/widgets/renderer.rs create mode 100644 mp/src/windows_manager.rs create mode 100644 mp_elements/src/tools/colormap.rs rename {mp/src/shaders => mp_elements/src/tools}/mod.rs (58%) diff --git a/Cargo.lock b/Cargo.lock index b21735f..1fa29c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,17 @@ dependencies = [ "libloading 0.8.5", ] +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -653,6 +664,13 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "element_bridge" version = "0.1.0" +dependencies = [ + "flume", + "makepad-widgets", + "mp_elements", + "tokio", + "wgpu", +] [[package]] name = "encase" @@ -1693,6 +1711,9 @@ dependencies = [ name = "mp" version = "0.1.0" dependencies = [ + "async-trait", + "element_bridge", + "futures", "glam", "log", "makepad-widgets", @@ -1735,6 +1756,7 @@ dependencies = [ "flume", "glam", "image", + "log", "mp_core", "pollster", "quick_cache", diff --git a/backup.txt b/backup.txt new file mode 100644 index 0000000..2ae450c --- /dev/null +++ b/backup.txt @@ -0,0 +1,24 @@ +let data = DATAPOOL + .get_or_load( + "/Users/tsuki/Desktop/Z_RADR_I_X5775_20230726180000_O_DOR-XPD-CAP-FMT.BIN.zip", + ) + .unwrap(); + let first_block = data.get(0).unwrap(); + let first_block: &Data = &(*first_block); + + if let Ok(d) = first_block.try_into() { + let attachment = { + // Ctx + let mut pipelines = GIAPP.pipelines(); + let ppi = pipelines.ppi(); + + GIAPP.load_data( + ppi, + d, + &PPIConfig { + colormap: vec![[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], + color_range: [0.0, 1.0], + }, + ) + }; + } \ No newline at end of file diff --git a/element_bridge/Cargo.toml b/element_bridge/Cargo.toml index d1a8da9..026df2e 100644 --- a/element_bridge/Cargo.toml +++ b/element_bridge/Cargo.toml @@ -4,3 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +mp_elements = { path = "../mp_elements", version = "*" } +makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "rik", version = "0.6.0" } +flume = "0.11.1" +wgpu = "23.0.0" +tokio = { version = "1.41.1", features = ["sync"] } diff --git a/element_bridge/src/lib.rs b/element_bridge/src/lib.rs index b93cf3f..4aa5dee 100644 --- a/element_bridge/src/lib.rs +++ b/element_bridge/src/lib.rs @@ -1,14 +1,70 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +use makepad_widgets::{makepad_shader_compiler::builtin, Cx, Texture}; +use mp_elements::{ + app::{Ctx, DrawList, RenderWindow}, + App, +}; +use std::sync::{Arc, Mutex}; + +pub use mp_elements::app::Window; +#[derive(Clone)] +pub struct TextureBridge { + dirty: bool, + _buffer: Arc>>, + window: Arc, + _texture: Texture, } -#[cfg(test)] -mod tests { - use super::*; +impl TextureBridge { + pub fn new(texture: Texture, app: &App, window: Window) -> Self { + let window = app.create_window(window); + Self { + dirty: true, + _buffer: Arc::new(tokio::sync::Mutex::new(vec![0u8; 256 * 256 * 4])), + window: Arc::new(window), + _texture: texture, + } + } - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + pub fn texture(&self) -> &Texture { + &self._texture + } + + pub fn window(&self) -> &RenderWindow { + &self.window + } + + pub fn render_window(&self) -> Arc { + self.window.clone() + } + + pub async fn draw(&mut self, app: &App, draw_list: DrawList) { + app.draw(&self.window, draw_list).await; + } + + pub async fn load_data(buffer: Arc>>, ctx: &Ctx, render_window: &RenderWindow) { + let output = &render_window.output().output_buffer; + + let (sender, receiver) = flume::bounded(1); + let slice = output.slice(..); + slice.map_async(wgpu::MapMode::Read, move |r| sender.send(r).unwrap()); + + let device = &ctx.device; + device.poll(wgpu::Maintain::wait()).panic_on_timeout(); + receiver.recv_async().await.unwrap().unwrap(); + + { + let view = slice.get_mapped_range(); + let mut buffer = buffer.lock().unwrap(); + buffer.copy_from_slice(&view[..]); + } + output.unmap(); + } + + pub fn update_texture(&self, cx: &mut Cx, buffer: Vec) { + self._texture.put_back_vec_u8(cx, buffer, None); + } + + pub fn buffer(&self) -> Arc>> { + self._buffer.clone() } } diff --git a/mp/Cargo.toml b/mp/Cargo.toml index 2dcc5ac..2e69bcd 100644 --- a/mp/Cargo.toml +++ b/mp/Cargo.toml @@ -13,5 +13,9 @@ native-dialog = "0.7.0" once_cell = "1.20.2" tracing = "0.1.40" tracing-subscriber = "0.3.18" -mp_elements = { path = "../mp_elements", version = "*" } +mp_elements = { path = "../mp_elements", version = "*" } tokio = { version = "1.41.1", features = ["full"] } + +element_bridge = { path = "../element_bridge", version = "*" } +futures = "0.3.31" +async-trait = "0.1.83" diff --git a/mp/src/app.rs b/mp/src/app.rs index 278d211..ac36111 100644 --- a/mp/src/app.rs +++ b/mp/src/app.rs @@ -1,8 +1,17 @@ +use crate::widgets::area::TAreaWidgetRefExt; +use crate::windows_manager::WindowsManager; +use crate::{render_task::RenderTasks, PLUGIN_MANAGER, RUNTIME}; +use crate::{DATAPOOL, GIAPP}; +use ::log::info; use makepad_widgets::makepad_micro_serde::*; use makepad_widgets::*; -use mp_elements::elements::Element; +use mp_core::Data; +use mp_elements::elements::ppi::PPIConfig; +use mp_elements::elements::PPI; +use std::sync::Arc; +use window_menu::WindowMenuWidgetRefExt; -use crate::{DATAPOOL, GIAPP, PLUGIN_MANAGER}; +use tokio::sync::Mutex; live_design! { import makepad_widgets::base::*; @@ -48,6 +57,29 @@ live_design! { {} } + window_menu = { + main = Main {items: [app, file, window, help]} + + app = Sub {name: "MP", items: [about, line, settings, line, quit]} + about = Item {name: "About Makepad Studio", enabled: false} + settings = Item {name: "Settings", enabled: false} + quit = Item {name: "Quit Makepad Studio", key: KeyQ} + + file = Sub {name: "File", items: [open_file, open_window]} + open_file = Item {name: "Open File", enabled: true, shift: true, key: KeyO} + open_window = Item {name: "Open Folder", enabled: false, shift: true, key: KeyN} + + + window = Sub {name: "Window", items: [minimize, zoom, line, all_to_front]} + minimize = Item {name: "Minimize", enabled: false} + zoom = Item {name: "Zoom", enabled: false} + all_to_front = Item {name: "Bring All to Front", enabled: false} + + help = Sub {name: "Help", items: [about]} + + line = Line, + } + } } } @@ -58,6 +90,10 @@ live_design! { pub struct App { #[live] ui: WidgetRef, + #[rust] + render_tasks: Arc>, + #[rust] + windows_manager: WindowsManager, } impl LiveRegister for App { @@ -65,6 +101,7 @@ impl LiveRegister for App { crate::makepad_widgets::live_design(_cx); crate::app_ui::live_design(_cx); crate::widgets::area::live_design(_cx); + crate::widgets::renderer::live_design(_cx); } } @@ -74,31 +111,61 @@ struct AppStateRon { } impl MatchEvent for App { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - use native_dialog::FileDialog; - let ui = self.ui.clone(); - if ui.button(id!(open_modal)).clicked(&actions) { - let supported_extensions = PLUGIN_MANAGER.supported_extensions(); + // Start UP + fn handle_startup(&mut self, _cx: &mut Cx) {} - // let file = FileDialog::new() - // .add_filter("Supported files", &supported_extensions) - // .show_open_single_file() - // .unwrap(); - - // if let Some(file) = file { - // if let Ok(d) = DATAPOOL.get_or_load(file) {} - // } - - ui.modal(id!(modal)).open(cx); - // ui.popup_notification(id!(test_noti)).open(cx); - } - } + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {} } impl AppMain for App { fn handle_event(&mut self, cx: &mut Cx, event: &Event) { self.match_event(cx, event); self.ui.handle_event(cx, event, &mut Scope::empty()); + spawn_background(self, event); + handle_menu(event); + } +} + +fn spawn_background(app: &mut App, event: &Event) { + match event { + Event::Draw(_) => { + // Render Task + + info!("Starting background task......"); + let tasks = app.render_tasks.clone(); + RUNTIME.spawn(async move { + let tasks = tasks.lock().await; + tasks.render().await; + }); + let tasks = app.render_tasks.clone(); + let all_buffers = app.windows_manager.buffer(); + RUNTIME.spawn(async move { + let mut tasks = tasks.lock().await; + tasks.listen(all_buffers).await; + }); + } + _ => {} + } +} + +fn handle_menu(event: &Event) { + match event { + Event::MacosMenuCommand(command) => { + if command == &LiveId::from_str("open_file") { + use native_dialog::FileDialog; + info!("Open File Dialog"); + let supported_extensions = PLUGIN_MANAGER.supported_extensions(); + let file = FileDialog::new() + .add_filter("Supported files", &supported_extensions) + .show_open_single_file() + .unwrap(); + + if let Some(file) = file { + info!("File: {:?}", file); + } + } + } + _ => {} } } diff --git a/mp/src/app_ui.rs b/mp/src/app_ui.rs index 8d38d64..1896f5a 100644 --- a/mp/src/app_ui.rs +++ b/mp/src/app_ui.rs @@ -3,6 +3,7 @@ live_design! { import makepad_widgets::base::*; import makepad_widgets::theme_desktop_dark::*; import crate::widgets::area::Area; + import crate::widgets::renderer::IRenderer; import makepad_draw::shader::std::*; @@ -59,28 +60,28 @@ live_design! { y: 0.5 }, quad = { - draw: { - // this example shader is ported from kishimisu's tutorial - fn pixel(self) -> vec4 { - // let uv = self.pos - 0.5; - // let uv0 = uv; - // let finalColor = vec3(0.0); + // draw: { + // // this example shader is ported from kishimisu's tutorial + // fn pixel(self) -> vec4 { + // // let uv = self.pos - 0.5; + // // let uv0 = uv; + // // let finalColor = vec3(0.0); - // let i = 0; - // for _i in 0..4 { // you cannot refer to _i inside the for loop; use i instead - // uv = fract(uv * -1.5) - 0.5; - // let d = length(uv) * exp(-length(uv0)); - // let col = Pal::iq2(length(uv0) + float(i) * .4 + self.time * .4); - // d = sin(d*8. + self.time) / 8.; - // d = abs(d); - // d = pow(0.01 / d, 1.2); - // finalColor += col * d; - // i = i+1; - // } + // // let i = 0; + // // for _i in 0..4 { // you cannot refer to _i inside the for loop; use i instead + // // uv = fract(uv * -1.5) - 0.5; + // // let d = length(uv) * exp(-length(uv0)); + // // let col = Pal::iq2(length(uv0) + float(i) * .4 + self.time * .4); + // // d = sin(d*8. + self.time) / 8.; + // // d = abs(d); + // // d = pow(0.01 / d, 1.2); + // // finalColor += col * d; + // // i = i+1; + // // } - // return vec4(finalColor ,1); - } - } + // // return vec4(finalColor ,1); + // } + // } } diff --git a/mp/src/handle_events.rs b/mp/src/handle_events.rs new file mode 100644 index 0000000..c941978 --- /dev/null +++ b/mp/src/handle_events.rs @@ -0,0 +1,44 @@ +use log::*; +use std::sync::{Arc, Mutex}; + +use crate::{app::App, render_task::RenderTasks, windows_manager::WindowsManager, GIAPP, RUNTIME}; + +pub async fn register_task(tasks: Arc>, windows_manager: &mut WindowsManager) { + let render_window = { + let bridge = windows_manager.get_bridge(0).unwrap(); + let b = bridge.lock().unwrap(); + b.render_window().clone() + }; + + if let Ok(data) = DATAPOOL.get_or_load_async(file).await { + // First Block + let first_block = data.get(0).unwrap(); + let first_block: &Data = &*first_block; + let pipelines = GIAPP.pipelines(); + let ppi = pipelines.ppi(); + let first_block = first_block.try_into().unwrap(); + + let attachment = { + let ppi: &PPI = &*ppi; + let attachment = GIAPP.load_data( + ppi, + first_block, + &PPIConfig { + colormap: vec![[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], + color_range: [0.0, 1.0], + }, + ); + + attachment + }; + + let mut tasks = tasks.lock().await; + let mut draw_list = mp_elements::app::DrawList::new(); + let attachment = Arc::new(attachment); + draw_list.push(ppi.clone(), attachment); + tasks.register_task(0, render_window, draw_list); + + info!("task registered, all {}", tasks.tasks.len()); + info!("Data loaded"); + } +} diff --git a/mp/src/lib.rs b/mp/src/lib.rs index 60034e8..31dfb2f 100644 --- a/mp/src/lib.rs +++ b/mp/src/lib.rs @@ -1,12 +1,12 @@ use mp_elements; use tokio::runtime::Runtime; pub mod app; +pub mod windows_manager; pub use makepad_widgets; pub use makepad_widgets::makepad_draw; pub use makepad_widgets::makepad_platform; pub mod app_ui; -pub mod render; -pub mod shaders; +pub mod render_task; pub mod widgets; use mp_core::{config::Setting, datapool::DataPool, plugin_system::PluginManager}; diff --git a/mp/src/render/camera.rs b/mp/src/render/camera.rs deleted file mode 100644 index 81ce860..0000000 --- a/mp/src/render/camera.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct Camera {} diff --git a/mp/src/render/data.rs b/mp/src/render/data.rs deleted file mode 100644 index f5be4e4..0000000 --- a/mp/src/render/data.rs +++ /dev/null @@ -1,46 +0,0 @@ -use makepad_widgets::{Cx, Texture, TextureFormat}; -use mp_core::data::RadarGridData; - -pub enum Data { - GridData(GridData), -} -pub struct GridData { - texture: Texture, - clear_buffer: bool, - shape: Option>, -} - -impl GridData { - pub fn new(cx: &mut Cx) -> Self { - let mut texture = Texture::new_with_format(cx, TextureFormat::Unknown); - - texture.texture_id(); - - GridData { - texture, - clear_buffer: true, - shape: None, - } - } - - pub fn update(&mut self, cx: &mut Cx, data: &RadarGridData) { - let data = data.data.cast_to::(); - self.clear_buffer = false; - // self.shape = Some(data.dim().into()); - // self.texture.put_back_vec_f32(cx, data.into(), None); - } - - pub fn clear(&mut self) { - self.clear_buffer = true; - } - - pub fn texture(&self) -> &Texture { - &self.texture - } -} - -impl From for Data { - fn from(data: GridData) -> Self { - Data::GridData(data) - } -} diff --git a/mp/src/render/mod.rs b/mp/src/render/mod.rs deleted file mode 100644 index 2292b3a..0000000 --- a/mp/src/render/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod camera; -pub mod data; diff --git a/mp/src/render_task.rs b/mp/src/render_task.rs new file mode 100644 index 0000000..fd51969 --- /dev/null +++ b/mp/src/render_task.rs @@ -0,0 +1,123 @@ +use crate::{ + widgets::renderer, + windows_manager::{AllBuffers, WindowsManager}, + GIAPP, RUNTIME, +}; +use element_bridge::TextureBridge; +use futures::future::BoxFuture; +use log::info; +use makepad_widgets::{Cx, Texture}; +use mp_elements::{ + app::{DrawList, RenderWindow}, + App, +}; +use std::sync::{Arc, Mutex}; + +#[derive(Default)] +pub struct RenderTasks { + pub(crate) tasks: Vec<(tokio::sync::mpsc::Receiver>, RenderTask)>, +} + +impl RenderTasks { + pub fn new() -> Self { + Self { tasks: Vec::new() } + } + + fn push_task(&mut self, task: (tokio::sync::mpsc::Receiver>, RenderTask)) { + self.tasks.push(task); + } + + pub fn register_task( + &mut self, + bridge_id: usize, + bridge: Arc, + draw_list: DrawList, + ) { + let (task, receiver) = RenderTask::new(bridge_id, bridge, draw_list); + self.push_task((receiver, task)); + } + + pub fn clear(&mut self) { + self.tasks.clear(); + } + + pub async fn render(&self) { + // Draw all tasks + let futures: Vec<_> = self + .tasks + .iter() + .map(|(_, task)| async { + info!("Drawing task"); + task.draw().await; + }) + .collect(); + info!("all tasks: {:?}", futures.len()); + futures::future::join_all(futures).await; + } + + pub async fn listen(&mut self, manager: AllBuffers) { + let mut futures = Vec::new(); + + for (receiver, _) in self.tasks.iter_mut() { + let manager = manager.lock().unwrap(); + let buffer = manager.get(&0).unwrap().clone(); + let future = async move { + while let Some(data) = receiver.recv().await { + let mut buffer = buffer.lock().await; + info!("Received data"); + buffer.copy_from_slice(&data); + } + }; + futures.push(Box::pin(future)); + } + + futures::future::join_all(futures).await; + } +} + +pub struct RenderTask { + // bridge: element_bridge::TextureBridge, + bridge_id: usize, + sender: tokio::sync::mpsc::Sender>, + buffer: Arc>>, + render_window: Arc, + draw_list: DrawList, +} + +impl RenderTask { + pub fn new( + bridge_id: usize, + bridge_render_window: Arc, + draw_list: DrawList, + ) -> (Self, tokio::sync::mpsc::Receiver>) { + let (sender, receiver) = tokio::sync::mpsc::channel(1); + ( + Self { + bridge_id, + sender, + buffer: Arc::new(Mutex::new(vec![0u8; 256 * 256 * 4])), + render_window: bridge_render_window, + draw_list, + }, + receiver, + ) + } + + pub async fn draw(&self) { + GIAPP + .draw(&self.render_window, self.draw_list.clone()) + .await; + + let ctx = GIAPP.ctx(); + + TextureBridge::load_data(self.buffer.clone(), ctx, &self.render_window).await; + + let bf = { + let buffer = self.buffer.clone(); + let buffer = buffer.lock().unwrap(); + buffer.clone() + }; + + self.sender.send(bf).await.unwrap(); + } +} diff --git a/mp/src/shaders/colormap.rs b/mp/src/shaders/colormap.rs deleted file mode 100644 index 192e09d..0000000 --- a/mp/src/shaders/colormap.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::render::data::GridData; -use makepad_widgets::*; - -live_design! { - ColorMap = {{ColorMap}} { - - // Cameras - uniform view: mat4 - uniform projection: mat4 - uniform model: mat4 - - uniform conf: vec4 - - varing value: float - varing range: vec4 - - // Data - texture data: texture3d - // ColorMAPPER - textrue color_map: texture1d - - fn get_value_at(self, pos: vec3) -> float { - return sampler3d_rt(self.data, pos).r; - } - - fn vertex(self) -> vec4 { - let v = sampler2d_rt(self.data, ) - - } - - fn pixel(self) -> vec4 { - return vec4(1., 0., 0., 1.); - } - - } -} - -#[repr(C)] -#[derive(Live, LiveRegister)] -pub struct ColorMap { - #[deref] - draw_vars: DrawVars, - #[live] - geometry: GeometryQuad2D, - #[calc] - pub position: Vec3, - #[calc] - pub value: f32, -} - -impl LiveHook for ColorMap { - fn before_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) { - self.draw_vars - .before_apply_init_shader(cx, apply, index, nodes, &self.geometry); - } - - fn after_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) { - self.draw_vars - .after_apply_update_self(cx, apply, index, nodes, &self.geometry); - } -} - -impl ColorMap {} diff --git a/mp/src/shaders/ppi.rs b/mp/src/shaders/ppi.rs deleted file mode 100644 index 528f9b4..0000000 --- a/mp/src/shaders/ppi.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::render::camera::Camera; -use crate::render::data::GridData; -use makepad_widgets::*; - -live_design! { - PPI = {{PPI}} { - - // Cameras - uniform view: mat4 - uniform projection: mat4 - uniform model: mat4 - - uniform conf: vec4 - - varing value: float - varing range: vec4 - - // Data - texture data: texture2d - - // ColorMAPPER - textrue color_map: texture2d - - fn get_value_at(self, pos: vec3) -> float { - return sampler3d(self.data, pos).r; - } - - fn vertex(self) -> vec4 { - let v = sampler2d_rt(self.data, ) - - } - - fn pixel(self) -> vec4 { - return vec4(1., 0., 0., 1.); - } - - } -} - -#[repr(C)] -#[derive(Live, LiveRegister)] -pub struct PPI { - #[deref] - pub draw_vars: DrawVars, - #[live] - geometry: GeometryQuad2D, - #[calc] - pub position: Vec3, - #[calc] - pub value: f32, -} - -impl LiveHook for PPI { - fn before_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) { - self.draw_vars - .before_apply_init_shader(cx, apply, index, nodes, &self.geometry); - } - - fn after_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) { - self.draw_vars - .after_apply_update_self(cx, apply, index, nodes, &self.geometry); - } -} - -impl PPI { - pub fn update_draw_call_vars(&mut self, camera: &Camera, data: &GridData) { - self.draw_vars.texture_slots[0] = Some(data.texture().clone()); - } -} diff --git a/mp/src/widgets/area.rs b/mp/src/widgets/area.rs index 130e97d..6efa848 100644 --- a/mp/src/widgets/area.rs +++ b/mp/src/widgets/area.rs @@ -1,6 +1,16 @@ +use std::cell::RefCell; +use std::sync::Arc; +use std::sync::Mutex; + +use crate::windows_manager::WindowId; +use crate::windows_manager::WindowsManager; +use crate::GIAPP; +use element_bridge::{TextureBridge, Window}; use makepad_widgets::makepad_derive_widget::*; use makepad_widgets::makepad_draw::*; use makepad_widgets::widget::*; +use mp_elements::renderer::camera::Camera; +use mp_elements::renderer::projection::Projection; live_design! { Area = {{TArea}} {} @@ -17,32 +27,81 @@ pub struct TArea { layout: Layout, #[live] time: f32, - // #[rust] - // next_frame: NextFrame, + + #[rust] + bridge: Option>>, } -impl LiveHook for TArea { - fn after_new_from_doc(&mut self, cx: &mut Cx) { - // starts the animation cycle on startup - // self.next_frame = cx.new_next_frame(); +#[derive(Debug, Clone)] +pub enum Status { + Share(std::rc::Rc>), + Own(T), +} + +#[derive(Clone)] +pub struct TAreaState { + pub time: f32, + pub camera: Status, + pub bridge: Option, + pub projection: Projection, +} + +impl Default for TAreaState { + fn default() -> Self { + Self { + time: 0.0, + camera: Status::Own(Camera::default()), + bridge: None, + projection: Projection::default(), + } } } +impl TAreaState { + pub fn set_camera(&mut self, f: F) { + match self.camera { + Status::Share(ref camera) => { + f(&mut camera.borrow_mut()); + } + Status::Own(ref mut camera) => { + f(camera); + } + } + } + + pub fn resize(&mut self, width: f32, height: f32) { + self.projection.resize(width as u32, height as u32); + } + + pub fn set_bridge(&mut self, bridge: TextureBridge) { + self.bridge = Some(bridge); + } + + pub fn new_bridge(&mut self, cx: &mut Cx, window: Window) { + let texture = Texture::new_with_format( + cx, + TextureFormat::VecRGBAf32 { + width: window.width as usize, + height: window.height as usize, + data: None, + updated: TextureUpdated::Full, + }, + ); + + let bridge = TextureBridge::new(texture, &GIAPP, window); + self.bridge = Some(bridge); + } +} + +impl LiveHook for TArea {} + #[derive(Clone, DefaultNone)] pub enum MyWidgetAction { None, } impl Widget for TArea { - fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) { - // if let Some(ne) = self.next_frame.is_event(event) { - // // update time to use for animation - // self.time = (ne.time * 0.001).fract() as f32; - // // force updates, so that we can animate in the absence of user-generated events - // self.redraw(cx); - // self.next_frame = cx.new_next_frame(); - // } - } + fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {} fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep { self.draw.begin(cx, walk, self.layout); @@ -50,3 +109,49 @@ impl Widget for TArea { DrawStep::done() } } + +impl TArea { + fn set_bridge(&mut self, bridge: Arc>) { + self.bridge = Some(bridge); + self.draw + .draw_vars + .set_texture(0, self.bridge.as_ref().unwrap().lock().unwrap().texture()); + } + + fn init_bridge(&mut self, cx: &mut Cx, manager: &mut WindowsManager) { + let window_id = WindowId::new("Primary"); + + // let height = self.walk.height. + // let width = self.walk.width.fixed_or_zero(); + let height = 256; + let width = 256; + + let window = Window { + height: height as u32, + width: width as u32, + }; + + let texture = Texture::new_with_format( + cx, + TextureFormat::VecRGBAf32 { + width: window.width as usize, + height: window.height as usize, + data: None, + updated: TextureUpdated::Full, + }, + ); + + let bridge = TextureBridge::new(texture, &GIAPP, window); + let (_, bridge) = manager.add_window(window_id, bridge); + + self.set_bridge(bridge); + } +} + +impl TAreaRef { + pub fn init_bridge(&self, cx: &mut Cx, manager: &mut WindowsManager) { + if let Some(mut bridge) = self.borrow_mut() { + bridge.init_bridge(cx, manager); + } + } +} diff --git a/mp/src/widgets/mod.rs b/mp/src/widgets/mod.rs index 31c7df9..5eaa31d 100644 --- a/mp/src/widgets/mod.rs +++ b/mp/src/widgets/mod.rs @@ -1 +1,2 @@ pub mod area; +pub mod renderer; diff --git a/mp/src/widgets/renderer.rs b/mp/src/widgets/renderer.rs new file mode 100644 index 0000000..06efdac --- /dev/null +++ b/mp/src/widgets/renderer.rs @@ -0,0 +1,110 @@ +use std::cell::RefCell; + +use crate::GIAPP; +use element_bridge::{TextureBridge, Window}; +use makepad_widgets::makepad_derive_widget::*; +use makepad_widgets::makepad_draw::*; +use makepad_widgets::widget::*; +use mp_elements::renderer::camera::Camera; +use mp_elements::renderer::projection::Projection; + +live_design! { + IRenderer = {{Renderer}} { + + import crate::widgets::area::Area; + { + draw: { + fn pixel(self) -> vec4 { + return mix(#7,#4,self.pos.y); + } + } + } + + } +} + +#[derive(Live, Widget)] +pub struct Renderer { + #[redraw] + #[live] + draw: DrawQuad, + #[walk] + walk: Walk, + #[layout] + layout: Layout, + #[rust] + state: RenderState, +} + +#[derive(Debug, Clone)] +pub enum Status { + Share(std::rc::Rc>), + Own(T), +} + +#[derive(Clone)] +pub struct RenderState { + pub time: f32, + pub camera: Status, + pub bridge: Option, + pub projection: Projection, +} + +impl Default for RenderState { + fn default() -> Self { + Self { + time: 0.0, + camera: Status::Own(Camera::default()), + bridge: None, + projection: Projection::default(), + } + } +} + +impl RenderState { + pub fn set_camera(&mut self, f: F) { + match self.camera { + Status::Share(ref camera) => { + f(&mut camera.borrow_mut()); + } + Status::Own(ref mut camera) => { + f(camera); + } + } + } + + pub fn resize(&mut self, width: f32, height: f32) { + self.projection.resize(width as u32, height as u32); + } + + pub fn set_bridge(&mut self, bridge: TextureBridge) { + self.bridge = Some(bridge); + } + + pub fn new_bridge(&mut self, cx: &mut Cx, window: Window) { + let texture = Texture::new_with_format( + cx, + TextureFormat::VecRGBAf32 { + width: window.width as usize, + height: window.height as usize, + data: None, + updated: TextureUpdated::Full, + }, + ); + + let bridge = TextureBridge::new(texture, &GIAPP, window); + self.bridge = Some(bridge); + } +} + +impl LiveHook for Renderer {} + +impl Widget for Renderer { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {} + + fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep { + self.draw.begin(cx, walk, self.layout); + self.draw.end(cx); + DrawStep::done() + } +} diff --git a/mp/src/windows_manager.rs b/mp/src/windows_manager.rs new file mode 100644 index 0000000..4d44389 --- /dev/null +++ b/mp/src/windows_manager.rs @@ -0,0 +1,73 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use element_bridge::TextureBridge; + +pub type AllBuffers = Arc>>>>>; + +#[derive(Eq, Hash, PartialEq)] +pub struct WindowId { + id: u64, + name: String, +} +#[derive(Default)] +pub struct WindowsManager { + inner: HashMap>>, + + all_bridges: Vec<(usize, Arc>)>, + + all_revelant_buffers: Arc>>>>>, +} + +impl WindowsManager { + pub fn new() -> Self { + Self { + inner: HashMap::new(), + all_bridges: Vec::new(), + all_revelant_buffers: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn add_window( + &mut self, + id: WindowId, + bridge: TextureBridge, + ) -> (usize, Arc>) { + let bridge = Arc::new(Mutex::new(bridge)); + self.inner.insert(id, bridge.clone()); + + let new_id = self.all_bridges.len(); + self.all_bridges.push((new_id, bridge.clone())); + + let buffer = { + let bridge = bridge.lock().unwrap(); + bridge.buffer() + }; + + // self.all_revelant_buffers.push((new_id, buffer)); + self.all_revelant_buffers + .lock() + .unwrap() + .insert(new_id, buffer); + (new_id, bridge) + } + + pub fn get_bridge(&self, id: usize) -> Option>> { + self.all_bridges.get(id).map(|(_, bridge)| bridge.clone()) + } + + pub fn buffer(&self) -> AllBuffers { + self.all_revelant_buffers.clone() + } +} + +impl WindowId { + pub fn new>(name: S) -> Self { + Self { + id: 0, + name: name.into(), + } + } +} diff --git a/mp_elements/Cargo.toml b/mp_elements/Cargo.toml index 8c09113..a550f76 100644 --- a/mp_elements/Cargo.toml +++ b/mp_elements/Cargo.toml @@ -6,15 +6,17 @@ edition = "2021" [dependencies] bytemuck = { version = "1.19.0", features = ["derive"] } glam = { version = "0.29.2", features = ["bytemuck"] } -regex = "1.11.1" wgpu = "23.0.0" mp_core = { path = "../mp_core", version = "*" } -flume = "0.11.1" -pollster = "0.4.0" quick_cache = "0.6.9" encase = { version = "0.10.0", features = ["glam"] } image = "0.25.5" rust-embed = "8.5.0" +flume = "0.11.1" +log = "0.4.22" [build-dependencies] regex = "1.11.1" + +[dev-dependencies] +pollster = "0.4.0" diff --git a/mp_elements/build.rs b/mp_elements/build.rs index b7acf65..c40bb40 100644 --- a/mp_elements/build.rs +++ b/mp_elements/build.rs @@ -22,6 +22,11 @@ fn main() { println!("cargo:rerun-if-changed={}", path.display()); } + let path_name = path.file_stem().unwrap().to_str().unwrap(); + if path_name.contains("merged") { + continue; + } + let merged_shader = merge_shader(&path.display().to_string(), &shader_path_base); let out_path = Path::new(&crate_path) diff --git a/mp_elements/image.png b/mp_elements/image.png index c655438016a883063261dfcd7d4833388f8d5ec3..182a8d0316d932da323f547c8cf97801cad4ea24 100644 GIT binary patch literal 54175 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

8L%asL+}c}3~eQKfYc${3+hMsa5{_BpSOV; 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 literal 71369 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�t+8WoW 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 diff --git a/mp_elements/shaders/colormap.wgsl b/mp_elements/shaders/colormap.wgsl index 4d02b5e..44c42a1 100644 --- a/mp_elements/shaders/colormap.wgsl +++ b/mp_elements/shaders/colormap.wgsl @@ -1,20 +1,20 @@ -@group(1) @binding(0) var color_map_texture: texture_2d; +@group(1) @binding(0) var color_map_texture: texture_1d; @group(1) @binding(1) var color_map_sampler: sampler; @group(1) @binding(2) var color_map_params: ColorMapParams; struct ColorMapParams { color_count: u32, value_min: f32, - value_max: f32 + value_max: f32, invalid_value: f32 } fn find_idx(ratio: f32) -> f32 { var sum = 0.0; var i = 0.0; - var count = (color_map_params.color_count - 1) as f32; + let count = f32(color_map_params.color_count - 1); while (ratio > sum) { - sum += textureSample(color_map_texture, color_map_sampler, vec2(i / count, 0.0)).r; + sum += textureSample(color_map_texture, color_map_sampler, i / count).r; i += 1.0; } return i / count; @@ -22,8 +22,8 @@ fn find_idx(ratio: f32) -> f32 { fn linear_colormap(value: f32) -> vec4f { - var v = clamp((value - color_map_params.value_min) / (color_map_params.value_max - color_map_params.value_min), 0.0, 1.0); - float idx = find_idx(v); - let c0: vec3f = textureSample(color_map_texture, color_map_sampler, vec2(idx, 0.0)).rgb; + let v = clamp((value - color_map_params.value_min) / (color_map_params.value_max - color_map_params.value_min), 0.0, 1.0); + let idx = find_idx(v); + let c0 = textureSample(color_map_texture, color_map_sampler, idx).rgb; return vec4f(c0, 1.0); } \ No newline at end of file diff --git a/mp_elements/shaders/elements/ppi.wgsl b/mp_elements/shaders/elements/ppi.wgsl index d8a3289..8da1a87 100644 --- a/mp_elements/shaders/elements/ppi.wgsl +++ b/mp_elements/shaders/elements/ppi.wgsl @@ -1,4 +1,5 @@ #include "constants.wgsl"; +#include "colormap.wgsl"; // Common Uniforms // common_tools // model_matrix: mat4, @@ -12,9 +13,9 @@ // light_intensity: float, // Uniforms -@group(1) @binding(0) var params: UniformParams; +@group(2) @binding(0) var params: UniformParams; // Data Buffer -@group(1) @binding(1) var data: array; +@group(2) @binding(1) var data: array; struct UniformParams { origin: vec4f @@ -36,8 +37,7 @@ fn vertex( var out: VertexOutput; // Transform position - out.position = common_tools.proj_matrix * common_tools.view_matrix * common_tools.model_matrix * vec4f(position, 1.0); - // out.position = vec4(position.xyz, 1.0); + out.position = vec4(position.xyz, 1.0); out.r_range = r_range; let idx = u32(position.w); out.idx = idx; @@ -57,8 +57,8 @@ fn fragment(input: VertexOutput) -> @location(0) vec4f { // Sample data texture let value = data[input.idx]; let ear = polar_forward(input.position.xyz); - // var color = linear_colormap(value); + // let color = linear_colormap(value); var color = clamp(value / 75.0, 0.0, 1.0); // let r = ear.x; diff --git a/mp_elements/shaders/elements/ppi_merged.wgsl b/mp_elements/shaders/elements/ppi_merged.wgsl index 0736c03..5830148 100644 --- a/mp_elements/shaders/elements/ppi_merged.wgsl +++ b/mp_elements/shaders/elements/ppi_merged.wgsl @@ -26,6 +26,35 @@ const HALF_PI:f32 = 1.57079632679489661923132169163975144; const LOG2:f32 = 0.693147180559945309417232121458176568; const LOG10:f32 = 2.30258509299404568401799145468436421; +@group(1) @binding(0) var color_map_texture: texture_1d; +@group(1) @binding(1) var color_map_sampler: sampler; +@group(1) @binding(2) var color_map_params: ColorMapParams; + +struct ColorMapParams { + color_count: u32, + value_min: f32, + value_max: f32, + invalid_value: f32 +} + +fn find_idx(ratio: f32) -> f32 { + var sum = 0.0; + var i = 0.0; + let count = f32(color_map_params.color_count - 1); + while (ratio > sum) { + sum += textureSample(color_map_texture, color_map_sampler, i / count).r; + i += 1.0; + } + return i / count; +} + + +fn linear_colormap(value: f32) -> vec4f { + let v = clamp((value - color_map_params.value_min) / (color_map_params.value_max - color_map_params.value_min), 0.0, 1.0); + let idx = find_idx(v); + let c0 = textureSample(color_map_texture, color_map_sampler, idx).rgb; + return vec4f(c0, 1.0); +} // Common Uniforms // common_tools // model_matrix: mat4, @@ -39,9 +68,9 @@ const LOG10:f32 = 2.30258509299404568401799145468436421; // light_intensity: float, // Uniforms -@group(1) @binding(0) var params: UniformParams; +@group(2) @binding(0) var params: UniformParams; // Data Buffer -@group(1) @binding(1) var data: array; +@group(2) @binding(1) var data: array; struct UniformParams { origin: vec4f @@ -63,8 +92,7 @@ fn vertex( var out: VertexOutput; // Transform position - out.position = common_tools.proj_matrix * common_tools.view_matrix * common_tools.model_matrix * vec4f(position, 1.0); - // out.position = vec4(position.xyz, 1.0); + out.position = vec4(position.xyz, 1.0); out.r_range = r_range; let idx = u32(position.w); out.idx = idx; @@ -84,8 +112,8 @@ fn fragment(input: VertexOutput) -> @location(0) vec4f { // Sample data texture let value = data[input.idx]; let ear = polar_forward(input.position.xyz); - // var color = linear_colormap(value); + // let color = linear_colormap(value); var color = clamp(value / 75.0, 0.0, 1.0); // let r = ear.x; diff --git a/mp_elements/src/app.rs b/mp_elements/src/app.rs index ea44e5d..9299fb3 100644 --- a/mp_elements/src/app.rs +++ b/mp_elements/src/app.rs @@ -1,12 +1,12 @@ +use std::cell::RefCell; +use std::sync::{Arc, Mutex, MutexGuard}; + +use crate::elements::{Element, ElementAttach, Elements, ElementsRef}; +use crate::elementvec::ElementVec; +use encase; use quick_cache::sync::Cache; use wgpu::util::DeviceExt; - -use crate::elements::{Element, ElementAttach, ElementsRef}; -use crate::elementvec::ElementVec; use wgpu::{Backends, Instance}; - -use encase; - type DB = std::sync::Arc; const BACKENDS_DEFAULT: u32 = Backends::DX12.bits() @@ -15,13 +15,8 @@ const BACKENDS_DEFAULT: u32 = Backends::DX12.bits() | Backends::BROWSER_WEBGPU.bits(); pub struct App { - _texture: wgpu::Texture, - _texture_size: wgpu::Extent3d, - texture_view: wgpu::TextureView, - output: Output, - pub pipelines: Option, - pub ctx: Ctx, - + pub(crate) pipelines: Option, + ctx: Ctx, buffer_pool: DataBufferPool, } @@ -59,44 +54,22 @@ impl App { // Create a new instance of the common utils struct. This struct contains the bind group layout, bind group, and uniform buffer. let common_utils = CommonUtils::new(&device); - // Create a new texture. This texture will be used as the output texture for the render pass. - let texture_size = wgpu::Extent3d { - width: 256, - height: 256, - depth_or_array_layers: 1, - }; - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("output texture"), - size: texture_size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - - // Create a texture view from the texture. This texture view will be used as the output texture for the render pass. - let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - // Buffer pool let buffer_pool = DataBufferPool::new(); let ctx = Ctx::new(device, queue, common_utils); - let output = Output::new(&ctx.device); - Self { pipelines: None, - _texture: texture, - _texture_size: texture_size, - texture_view, buffer_pool, - output, ctx, } } + pub fn buffer_pool(&mut self) -> &mut DataBufferPool { + &mut self.buffer_pool + } + // Create a new context struct. This struct contains references to the device, queue, bind group layout, and bind group. pub fn ctx(&self) -> &Ctx { &self.ctx @@ -117,7 +90,7 @@ impl App { } // Draw the elements in the draw list. - pub async fn draw(&self, draw_list: DrawList<'_, '_>) { + pub async fn draw(&self, window: &RenderWindow, draw_list: DrawList) { let mut encoder = self .ctx .device @@ -129,7 +102,7 @@ impl App { let render_pass_desc = wgpu::RenderPassDescriptor { label: Some("Some Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.texture_view, + view: &window.texture_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), @@ -161,24 +134,105 @@ impl App { attach.bind(&mut render_pass); // Draw the element. - element.draw(attach, &mut render_pass); + element.draw(&attach, &mut render_pass); } } - // output - self.output - .output(&mut encoder, &self._texture, self._texture_size, &self.ctx); + // Output + window.finish(&mut encoder); self.ctx.queue.submit(Some(encoder.finish())); - - self.output.get_data(&self.ctx).await; } - pub fn load_data<'a, T>(&mut self, element: &T, data: &T::Data) -> ElementAttach + pub fn drop_all_buffers(&mut self) { + self.buffer_pool.buffers.clear(); + } + + pub fn load_data<'a, T>(&self, element: &T, data: &T::Data, config: &T::Config) -> ElementAttach where T: Element, { - let buffer_pool = &mut self.buffer_pool; + let buffer_pool = &self.buffer_pool; let ctx = &self.ctx; - element.load_data(&ctx, data, buffer_pool) + element.load_data(&ctx, data, buffer_pool, config) + } + + pub fn create_window(&self, window: Window) -> RenderWindow { + RenderWindow::new(window, &self.ctx.device) + } +} + +pub struct Window { + pub width: u32, + pub height: u32, +} + +pub struct RenderWindow { + _texture: wgpu::Texture, + _texture_size: wgpu::Extent3d, + texture_view: wgpu::TextureView, + output: Output, + window: Window, +} + +impl RenderWindow { + pub fn new(window: Window, device: &wgpu::Device) -> Self { + // Create a new texture. This texture will be used as the output texture for the render pass. + let texture_size = wgpu::Extent3d { + width: window.width, + height: window.height, + depth_or_array_layers: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("output texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + // Create a texture view from the texture. This texture view will be used as the output texture for the render pass. + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let output = Output::new(device); + + Self { + _texture: texture, + _texture_size: texture_size, + texture_view, + output, + window, + } + } + + pub fn output(&self) -> &Output { + &self.output + } + + pub fn finish(&self, encoder: &mut wgpu::CommandEncoder) { + encoder.copy_texture_to_buffer( + wgpu::ImageCopyTexture { + texture: &self._texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::ImageCopyBuffer { + buffer: &self.output.output_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(256 * 4), + rows_per_image: Some(256), + }, + }, + wgpu::Extent3d { + width: self.window.width, + height: self.window.height, + depth_or_array_layers: 1, + }, + ); } } @@ -202,16 +256,21 @@ impl Ctx { } } -pub struct DrawList<'b, 'a: 'b> { - pub elements: Vec<(&'b ElementAttach, ElementsRef<'a>)>, // 修复字段访问权限 +#[derive(Clone)] +pub struct DrawList { + pub elements: Vec<(std::sync::Arc, Elements)>, // 修复字段访问权限 } -impl<'b, 'a: 'b> DrawList<'b, 'a> { +impl DrawList { pub fn new() -> Self { Self { elements: vec![] } } - pub fn push>>(&mut self, element: Ele, attach: &'a ElementAttach) { + pub fn push>( + &mut self, + element: Ele, + attach: std::sync::Arc, + ) { self.elements.push((attach, element.into())); } } @@ -298,7 +357,7 @@ impl DataBufferPool { } } - pub fn get_or_create_buffer(&mut self, key: BufferKey, f: F) -> DB + pub fn get_or_create_buffer(&self, key: BufferKey, f: F) -> DB where F: FnOnce() -> wgpu::Buffer, { @@ -325,7 +384,7 @@ impl BufferKey { } pub struct Output { - output_buffer: wgpu::Buffer, + pub output_buffer: wgpu::Buffer, } impl Output { @@ -346,34 +405,6 @@ impl Output { Self { output_buffer } } - pub fn output( - &self, - encoder: &mut wgpu::CommandEncoder, - texture: &wgpu::Texture, - texture_size: wgpu::Extent3d, - ctx: &Ctx, - ) { - let u32_size = std::mem::size_of::() as u32; - - encoder.copy_texture_to_buffer( - wgpu::ImageCopyTexture { - aspect: wgpu::TextureAspect::All, - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - }, - wgpu::ImageCopyBuffer { - buffer: &self.output_buffer, - layout: wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(u32_size * 256), - rows_per_image: Some(256), - }, - }, - texture_size, - ); - } - pub async fn get_data(&self, ctx: &Ctx) { let device = &ctx.device; // 需要对映射变量设置范围,以便我们能够解除缓冲区的映射 @@ -404,29 +435,27 @@ impl Output { mod test { use mp_core::{PluginManager, RadarGridData}; + use wgpu::core::pipeline; - use crate::elements::{Element, PPI}; + use crate::elements::{ppi::PPIConfig, Element, PPI}; use super::*; #[test] fn test_app() { let plugin_manager = PluginManager::new( - // r#"/Users/tsuki/projects/mp/loaders"# - r#"C:\Users\qwin7\projects\radarmp\loaders"#, + r#"/Users/tsuki/projects/mp/loaders"#, // r#"C:\Users\qwin7\projects\radarmp\loaders"#, ) .unwrap(); let data = plugin_manager.try_load_data( - // "/Users/tsuki/Desktop/Z_RADR_I_X5775_20230726180000_O_DOR-XPD-CAP-FMT.BIN.zip", - r#"C:\Users\qwin7\Downloads\ZJSXAA_20230113070200_R.dat.gz"#, + "/Users/tsuki/Desktop/Z_RADR_I_X5775_20230726180000_O_DOR-XPD-CAP-FMT.BIN.zip", + // r#"C:\Users\qwin7\Downloads\ZJSXAA_20230113070200_R.dat.gz"#, ); pollster::block_on(async { let mut app = App::instant().await; app.init().await; - let pipelines = app.pipelines.as_ref().unwrap(); - let ppi = pipelines.ppi(); let ctx = &app.ctx; let buffer_pool = &mut app.buffer_pool; @@ -434,17 +463,33 @@ mod test { let first_block = data.first().unwrap(); // Convert the first block into a PPI struct. - if let Ok(data) = first_block.try_into() { - let attachment = ppi.load_data(&ctx, data, buffer_pool); + // if let Ok(data) = first_block.try_into() { + // // let attachment = { + // // let pipelines = app.pipelines.as_mut().unwrap(); + // // let ppi = pipelines.ppi(); - // Create a new draw list and push the attachment into it. + // // // let attachment = ppi.load_data( + // // // &ctx, + // // // data, + // // // buffer_pool, + // // // &PPIConfig { + // // // colormap: vec![[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], + // // // color_range: [0.0, 1.0], + // // // }, + // // // ); + // // attachment + // // }; - let mut draw_list = DrawList::new(); - draw_list.push(ppi, &attachment); + // let pipeline = app.pipelines(); + // let ppi = pipeline.ppi(); - // Draw the elements in the draw list. - app.draw(draw_list).await; - } + // // Create a new draw list and push the attachment into it. + // let mut draw_list = DrawList::new(); + // // draw_list.push(ppi, &attachment); + + // // Draw the elements in the draw list. + // // app.draw(draw_list).await; + // } } else { panic!("Failed to load data"); } diff --git a/mp_elements/src/elements/mod.rs b/mp_elements/src/elements/mod.rs index 6a8d304..d18f662 100644 --- a/mp_elements/src/elements/mod.rs +++ b/mp_elements/src/elements/mod.rs @@ -1,22 +1,33 @@ +use std::sync::Arc; pub mod ppi; -use crate::app::{BufferKey, Ctx, DataBufferPool}; +use crate::{ + app::{BufferKey, Ctx, DataBufferPool}, + App, +}; +use mp_core::config; pub use ppi::PPI; use std::any::Any; use wgpu::util::DeviceExt; macro_rules! elements { ($(($element_name:ident,$element: ty),)+) => { + #[derive(Clone)] pub enum Elements { - $($element_name($element),)+ + $($element_name(Arc<$element>),)+ } + #[derive(Clone, Copy)] pub enum ElementsRef<'a> { $($element_name(&'a $element),)+ } + pub enum ElementsMut<'a> { + $($element_name(&'a mut $element),)+ + } + $( - impl From<$element> for Elements { - fn from(element: $element) -> Self { + impl From> for Elements { + fn from(element: Arc<$element>) -> Self { Elements::$element_name(element) } } @@ -26,6 +37,21 @@ macro_rules! elements { ElementsRef::$element_name(element) } } + + impl<'a> From<&'a mut $element> for ElementsMut<'a> { + fn from(element: &'a mut $element) -> Self { + ElementsMut::$element_name(element) + } + } + + impl<'a> From<&'a mut $element> for ElementsRef<'a> { + fn from(element: &'a mut $element) -> Self { + ElementsRef::$element_name(element) + } + } + + + )+ impl Elements { @@ -57,6 +83,20 @@ macro_rules! elements { } + impl<'a> ElementsMut<'a> { + pub fn pipeline(&'a self) -> &wgpu::RenderPipeline { + match self { + $(ElementsMut::$element_name(element) => element.pipeline(),)+ + } + } + + pub fn draw(&'a self, attach: &ElementAttach, render_pass: &mut wgpu::RenderPass) { + match self { + $(ElementsMut::$element_name(element) => element.draw(attach, render_pass),)+ + } + } + } + }; } @@ -65,6 +105,8 @@ pub trait Element { type Uniform; type Data; + type Config; + fn new(ctx: &Ctx) -> Self; // fn new_attachment<'a>(&self, ctx: &Ctx) -> ElementAttach; @@ -84,7 +126,8 @@ pub trait Element { &self, ctx: &Ctx, data: &Self::Data, - buffer_pool: &mut DataBufferPool, + buffer_pool: &DataBufferPool, + config: &Self::Config, ) -> ElementAttach; } diff --git a/mp_elements/src/elements/ppi.rs b/mp_elements/src/elements/ppi.rs index e204df8..68e666e 100644 --- a/mp_elements/src/elements/ppi.rs +++ b/mp_elements/src/elements/ppi.rs @@ -1,6 +1,6 @@ -use crate::Shaders; -use rust_embed::RustEmbed; -use std::{ops::Sub, result, vec}; +use crate::tools::colormap::{ColorMap, ColormapParams}; +use log::*; +use std::{ops::Sub, vec}; use crate::{ app::{BufferKey, Ctx, DataBufferPool}, @@ -20,6 +20,12 @@ const RMAXNUM: u64 = 50; pub struct PPI { bind_group_layout: wgpu::BindGroupLayout, pipeline: wgpu::RenderPipeline, + colormap: Option, +} + +pub struct PPIConfig { + pub colormap: Vec<[f32; 4]>, + pub color_range: [f32; 2], } #[repr(C)] @@ -67,8 +73,14 @@ impl Element for PPI { type Uniform = PPIUniform; type Data = RadarGridData; + type Config = PPIConfig; + fn new(ctx: &Ctx) -> Self { let device = &ctx.device; + + // Tools + let colormap_layout = ColorMap::layout(device); + // Group Layout let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("PPI Bind Group Layout"), @@ -106,6 +118,8 @@ impl Element for PPI { bind_group_layouts: &[ // common_tools &ctx.common_tools.bind_group_layout, + // colormap + &colormap_layout, // ppi &bind_group_layout, ], @@ -155,6 +169,7 @@ impl Element for PPI { PPI { bind_group_layout: bind_group_layout, pipeline: render_pipeline, + colormap: None, } } @@ -188,12 +203,13 @@ impl Element for PPI { &self, ctx: &Ctx, data: &Self::Data, - buffer_pool: &mut DataBufferPool, + buffer_pool: &DataBufferPool, + config: &Self::Config, ) -> ElementAttach { + info!("Loading PPI data"); let (vertex, index) = self.bake(data); - println!("index: {:?}", &(index.as_ref()).unwrap()[0..24]); - println!("vertex: {:?}", &vertex[0..4]); let device = &ctx.device; + let queue = &ctx.queue; let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("PPI Uniform Buffer"), @@ -247,12 +263,22 @@ impl Element for PPI { label: Some("PPI Bind Group"), }); + // ColorMap Bind Group + let color_map = ColorMap::new( + device, + queue, + ColormapParams::new(config.colormap.clone(), config.color_range), + ); + let color_map_group = color_map.bind_group(device); + // self.colormap = Some(color_map); + ElementAttach { vertex_buffer: vertex_buffer, index_buffer: Some(index_buffer), num_indices, uniform_buffer: Some(uniform_buffer), - bind_group: vec![(1, bind_group)], + // bind group 0 is always common tools + bind_group: vec![(1, color_map_group), (2, bind_group)], data_buffer_key: Some(key), } } @@ -262,7 +288,7 @@ impl PPI { fn init_shader(device: &wgpu::Device) -> wgpu::ShaderModule { // let shader_str = merge_shader(r#"/Users/tsuki/projects/mp/mp_elements/shaders/ppi.wgsl"#); - let shader_str = get_shader("ppi_merged.wgsl").as_ref(); + let shader_str = get_shader("elements/ppi_merged.wgsl"); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("PPI Shader Module"), diff --git a/mp_elements/src/elementvec.rs b/mp_elements/src/elementvec.rs index be75368..e13d68b 100644 --- a/mp_elements/src/elementvec.rs +++ b/mp_elements/src/elementvec.rs @@ -1,27 +1,29 @@ use crate::app::Ctx; use crate::elements::*; +use std::sync::Arc; macro_rules! elementvec { - ($({$element: ident, $element_ty: ty})+) => { + ($({$element: ident, $element_ty: ty, $element_mut: ident})+) => { pub struct ElementVec { - $($element: $element_ty,)+ + $($element: Arc<$element_ty> ,)+ } impl ElementVec { pub fn init(ctx: &Ctx) -> Self { // Compile the shaders, create the pipelines, etc. Self { - $($element: <$element_ty>::new(ctx),)+ + $($element: Arc::new((<$element_ty>::new(ctx))) ,)+ } } - $(pub fn $element(&self) -> &$element_ty { + $(pub fn $element(&self) -> &Arc< $element_ty> { &self.$element })+ + } }; () => {}; } elementvec!({ - ppi, PPI + ppi, PPI, ppi_mut }); diff --git a/mp_elements/src/lib.rs b/mp_elements/src/lib.rs index 8092bf7..c7a2c38 100644 --- a/mp_elements/src/lib.rs +++ b/mp_elements/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; pub mod elements; pub mod elementvec; pub mod renderer; +pub mod tools; mod utils; pub use app::App; diff --git a/mp_elements/src/renderer/camera.rs b/mp_elements/src/renderer/camera.rs index 5f5a980..ef345f4 100644 --- a/mp_elements/src/renderer/camera.rs +++ b/mp_elements/src/renderer/camera.rs @@ -1,4 +1,5 @@ use glam::*; +#[derive(Debug, Clone)] pub struct Camera { pub position: Vec3, pub center: Vec3, diff --git a/mp_elements/src/renderer/projection.rs b/mp_elements/src/renderer/projection.rs index d09ece8..5dd0db7 100644 --- a/mp_elements/src/renderer/projection.rs +++ b/mp_elements/src/renderer/projection.rs @@ -1,4 +1,5 @@ use glam::*; +#[derive(Clone, Debug)] pub struct Projection { aspect: f32, fovy: f32, @@ -24,3 +25,14 @@ impl Projection { Mat4::perspective_rh(self.fovy, self.aspect, self.znear, self.zfar) } } + +impl Default for Projection { + fn default() -> Self { + Self { + aspect: 1.0, + fovy: 45.0, + znear: 0.1, + zfar: 100.0, + } + } +} diff --git a/mp_elements/src/tools/colormap.rs b/mp_elements/src/tools/colormap.rs new file mode 100644 index 0000000..7a1478c --- /dev/null +++ b/mp_elements/src/tools/colormap.rs @@ -0,0 +1,212 @@ +use wgpu::util::DeviceExt; + +pub struct ColorMap { + uniform: ColorMapUniform, + uniform_buffer: wgpu::Buffer, + unifrom_bind_group_layout: wgpu::BindGroupLayout, + + texture: wgpu::Texture, + texture_view: wgpu::TextureView, + sampler: wgpu::Sampler, +} + +pub struct ColormapParams { + pub colors: Vec<[f32; 4]>, + pub color_range: [f32; 2], +} + +impl ColormapParams { + pub fn from_u8(colors: Vec<[u8; 4]>, color_range: [f32; 2]) -> Self { + let colors = colors + .iter() + .map(|c| { + let c = [c[0] as f32, c[1] as f32, c[2] as f32, c[3] as f32]; + let c = [c[0] / 255.0, c[1] / 255.0, c[2] / 255.0, c[3] / 255.0]; + c + }) + .collect::>(); + ColormapParams { + colors, + color_range, + } + } + pub fn new(colors: Vec<[f32; 4]>, color_range: [f32; 2]) -> Self { + ColormapParams { + colors, + color_range, + } + } + fn as_texture_data(&self) -> Vec { + self.colors + .iter() + .flat_map(|color| bytemuck::bytes_of(color)) + .copied() + .collect() + } +} + +#[derive(Debug, Clone, Copy, encase::ShaderType)] +#[repr(C)] +pub struct ColorMapUniform { + color_count: u32, + value_min: f32, + value_max: f32, + invalid_value: f32, +} + +impl ColorMapUniform { + fn as_slice(&self) -> encase::internal::Result> { + let mut buffer = encase::UniformBuffer::new(Vec::new()); + buffer.write(self)?; + Ok(buffer.into_inner()) + } +} + +impl ColorMap { + pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, params: ColormapParams) -> Self { + let uniform = ColorMapUniform { + color_count: params.colors.len() as u32, + value_min: params.color_range[0], + value_max: params.color_range[1], + invalid_value: 0.0, + }; + + let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("ColorMap Uniform Buffer"), + contents: uniform.as_slice().unwrap().as_slice(), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let texture = Self::create_color_texture(device, queue, ¶ms); + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = Self::create_sampler(device); + + let bind_group_layout = Self::layout(device); + + ColorMap { + uniform, + uniform_buffer: buffer, + unifrom_bind_group_layout: bind_group_layout, + sampler, + texture, + texture_view, + // uniform_bind_group: bind_group, + } + } + + pub fn layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("ColorMap Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::all(), + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D1, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::all(), + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }) + } + + pub fn bind_group(&self, device: &wgpu::Device) -> wgpu::BindGroup { + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ColorMap Bind Group"), + layout: &self.unifrom_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&self.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: self.uniform_buffer.as_entire_binding(), + }, + ], + }); + + bind_group + } + + pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout { + &self.unifrom_bind_group_layout + } + + pub fn set_uniform(&mut self, f: F) + where + F: FnOnce(&mut ColorMapUniform), + { + f(&mut self.uniform); + } + + pub fn update(&mut self, queue: &wgpu::Queue) { + queue.write_buffer( + &self.uniform_buffer, + 0, + self.uniform.as_slice().unwrap().as_slice(), + ); + } + + fn create_color_texture( + device: &wgpu::Device, + queue: &wgpu::Queue, + params: &ColormapParams, + ) -> wgpu::Texture { + device.create_texture_with_data( + queue, + &wgpu::TextureDescriptor { + label: Some("ColorMap Texture"), + size: wgpu::Extent3d { + width: params.colors.len() as u32, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D1, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }, + wgpu::util::TextureDataOrder::LayerMajor, + ¶ms.as_texture_data(), + ) + } + + fn create_sampler(device: &wgpu::Device) -> wgpu::Sampler { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("ColorMap Sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + sampler + } +} diff --git a/mp/src/shaders/mod.rs b/mp_elements/src/tools/mod.rs similarity index 58% rename from mp/src/shaders/mod.rs rename to mp_elements/src/tools/mod.rs index 0092133..83c08c5 100644 --- a/mp/src/shaders/mod.rs +++ b/mp_elements/src/tools/mod.rs @@ -1,2 +1 @@ pub mod colormap; -pub mod ppi; diff --git a/mp_elements/src/utils.rs b/mp_elements/src/utils.rs index 9591969..c772381 100644 --- a/mp_elements/src/utils.rs +++ b/mp_elements/src/utils.rs @@ -2,8 +2,8 @@ use crate::Shaders; use std::borrow::Cow; use std::str; -pub fn get_shader(name: &str) -> Cow<'static, str> { +pub fn get_shader(name: &str) -> String { let file = Shaders::get(name).unwrap(); - let string = String::from_utf8_lossy(&file.data); + let string = String::from_utf8(file.data.to_vec()).unwrap(); string }