hotline_rs/
client.rs

1use crate::gfx;
2use crate::gfx::ReadBackRequest;
3use crate::os;
4use crate::imgui;
5use crate::plugin::PluginInstance;
6use crate::pmfx;
7use crate::imdraw;
8use crate::primitives;
9use crate::plugin;
10use crate::reloader;
11use crate::image;
12
13use gfx::{SwapChain, CmdBuf, Texture, RenderPass, Heap};
14
15use os::Window;
16use imgui::UserInterface;
17use plugin::PluginReloadResponder;
18use reloader::Reloader;
19
20use serde::{Deserialize, Serialize};
21
22use std::path::PathBuf;
23use std::collections::{HashMap, VecDeque};
24use std::time::SystemTime;
25
26use maths_rs::vec::vec4f;
27
28const STATUS_BAR_HEIGHT : f32 = 10.0;
29
30/// Information to create a hotline context which will create an app, window, device.
31pub struct HotlineInfo {
32    /// Name for the app and window title
33    pub name: String,
34    /// Window rect {pos_x pos_y, width, height}
35    pub window_rect: os::Rect<i32>,
36    /// Signify if the app is DPI aware or not
37    pub dpi_aware: bool,
38    /// Clear colour of the default swap chain
39    pub clear_colour: Option<gfx::ClearColour>,
40    /// Optional name of gpu adaptor, use None for the default / primary device
41    pub adapter_name: Option<String>,
42    /// Number of buffers in the swap chain (2 for double buffered, 3 for tripple etc)
43    pub num_buffers: u32,
44    /// Size of the default device heap for shader resources (textures, buffers, etc)
45    pub shader_heap_size: usize, 
46    /// Size of the default device heap for render targets
47    pub render_target_heap_size: usize,
48    /// Size of the default device heap for depth stencil targets
49    pub depth_stencil_heap_size: usize,
50    /// Optional user config, the default will be automatically located in the file system, this allows to override the launch configuration
51    pub user_config: Option<UserConfig>
52}
53
54/// Time structure to pass around to plugins and systems
55#[derive(Clone)]
56pub struct Time {
57    /// Delta time in seconds since the last frame... scaled time_scale or may be paused
58    pub delta: f32,
59    /// A raw delta time not affected by any scaling or pausing
60    pub raw_delta: f32,
61    /// Delta time for the CPU update (ignoring v-sync / present cost)
62    pub update_delta: f32,
63    /// Accumulated delta time
64    pub accumulated: f32,
65    /// Smoothed delta time to reduce spikes
66    pub smooth_delta: f32,
67    /// System time that the last frame started
68    pub frame_start: SystemTime,
69    /// Control the run state of the program, pause render / update
70    pub paused: bool,
71    /// Control whether delta time is time or 0
72    pub delta_paused: bool,
73    /// Control the delta time (speed up or slo-mo)
74    pub time_scale: f32,
75    /// Force fixed time delta
76    fixed_delta: Option<f32>
77}
78const SMOOTH_DELTA_FRAMES : usize = 120;
79
80/// Time trait implementation
81impl Time {
82    /// Instantiates a new time initialised to 0
83    fn new() -> Self {
84        Self {
85            delta: 0.0,
86            raw_delta: 0.0,
87            update_delta: 0.0,
88            accumulated: 0.0,
89            smooth_delta: 0.0,
90            frame_start: SystemTime::now(),
91            paused: false,
92            delta_paused: false,
93            time_scale: 1.0,
94            fixed_delta: None
95        }
96    }
97}
98
99/// Useful defaults for quick HotlineInfo initialisation
100impl Default for HotlineInfo {
101    fn default() -> Self {
102        HotlineInfo {
103            name: "hotline".to_string(),
104            window_rect: os::Rect {
105                x: 0,
106                y: 0,
107                width: 1280,
108                height: 1300
109            },
110            dpi_aware: true,
111            clear_colour: Some(gfx::ClearColour {
112                r: 0.45,
113                g: 0.55,
114                b: 0.60,
115                a: 1.00,
116            }),
117            num_buffers: 2,
118            adapter_name: None,
119            shader_heap_size: 1024,
120            render_target_heap_size: 128,
121            depth_stencil_heap_size: 64,
122            user_config: None
123        }
124    }
125}
126
127/// Hotline client data members
128pub struct Client<D: gfx::Device, A: os::App> {
129    pub app: A,
130    pub device: D,
131    pub main_window: A::Window,
132    pub swap_chain: D::SwapChain,
133    pub pmfx: pmfx::Pmfx<D>,
134    pub cmd_buf: D::CmdBuf,
135    pub imdraw: imdraw::ImDraw<D>,
136    pub imgui: imgui::ImGui<D, A>,
137    pub unit_quad_mesh: pmfx::Mesh<D>,
138    pub user_config: UserConfig,
139    pub time: Time,
140    pub libs: HashMap<String, hot_lib_reloader::LibReloader>,
141    plugins: Vec<PluginCollection>,
142    delta_history: VecDeque<f32>,
143    instance_name: String,
144    status_bar_height: f32
145}
146
147/// Serialisable plugin
148#[derive(Serialize, Deserialize, Clone)]
149pub struct PluginInfo {
150    pub path: String
151}
152
153/// Serialisable user configration settings and saved state
154#[derive(Serialize, Deserialize, Clone)]
155pub struct UserConfig {
156    // pos xy, size xy
157    pub main_window_rect: os::Rect<i32>,
158    pub console_window_rect: Option<os::Rect<i32>>,
159    pub plugins: Option<HashMap<String, PluginInfo>>,
160    pub plugin_data: Option<HashMap<String, serde_json::Value>>
161}
162
163/// Internal enum to track plugin state and syncornise unloads, reloads and setups etc.
164#[derive(PartialEq, Eq)]
165enum PluginState {
166    None,
167    Reload,
168    Setup,
169    Unload,
170}
171
172/// Container data describing a plugin 
173struct PluginCollection {
174    name: String,
175    reloader: reloader::Reloader,
176    instance: PluginInstance,
177    state: PluginState
178}
179
180/// Hotline `Client` implementation
181impl<D, A> Client<D, A> where D: gfx::Device, A: os::App, D::RenderPipeline: gfx::Pipeline {
182    /// Create a hotline context consisting of core resources
183    pub fn create(info: HotlineInfo) -> Result<Self, super::Error> {
184        // read user config or get defaults
185        let user_config_path = super::get_data_path("../user_config.json");
186        let saved_user_config = if std::path::Path::new(&user_config_path).exists() {
187            let user_data = std::fs::read(user_config_path)?;
188            serde_json::from_slice(&user_data).unwrap()
189        }
190        else {
191            UserConfig {
192                main_window_rect: info.window_rect,
193                console_window_rect: None,
194                plugin_data: Some(HashMap::new()),
195                plugins: None
196            }
197        };
198        
199        // override by the supplied user config
200        let user_config = info.user_config.unwrap_or(saved_user_config);
201        
202        // app
203        let mut app = A::create(os::AppInfo {
204            name: info.name.to_string(),
205            num_buffers: info.num_buffers,
206            dpi_aware: info.dpi_aware,
207            window: false,
208        });
209        if let Some(console_rect) = user_config.console_window_rect {
210            app.set_console_window_rect(console_rect);
211        }
212    
213        // device
214        let mut device = D::create(&gfx::DeviceInfo {
215            adapter_name: info.adapter_name,
216            shader_heap_size: info.shader_heap_size,
217            render_target_heap_size: info.render_target_heap_size,
218            depth_stencil_heap_size: info.depth_stencil_heap_size,
219        });
220    
221        // main window
222        let main_window = app.create_window(os::WindowInfo {
223            title: info.name.to_string(),
224            rect: user_config.main_window_rect,
225            style: os::WindowStyleFlags::NONE,
226            parent_handle: None,
227        });
228    
229        // swap chain
230        let swap_chain_info = gfx::SwapChainInfo {
231            num_buffers: info.num_buffers,
232            format: gfx::Format::RGBA8n,
233            clear_colour: info.clear_colour
234        };
235        let mut swap_chain = device.create_swap_chain::<A>(&swap_chain_info, &main_window)?;
236
237        // imdraw
238        let imdraw_info = imdraw::ImDrawInfo {
239            initial_buffer_size_2d: 65535 * 1024,
240            initial_buffer_size_3d: 65535 * 1024
241        };
242        let imdraw : imdraw::ImDraw<D> = imdraw::ImDraw::create(&imdraw_info).unwrap();
243
244        // imgui    
245        let mut imgui_info = imgui::ImGuiInfo::<D, A> {
246            device: &mut device,
247            swap_chain: &mut swap_chain,
248            main_window: &main_window,
249            fonts: vec![
250                imgui::FontInfo {
251                    filepath: super::get_data_path("fonts/cousine_regular.ttf"),
252                    glyph_ranges: None
253                },
254                imgui::FontInfo {
255                    filepath: super::get_data_path("fonts/font_awesome.ttf"),
256                    glyph_ranges: Some(vec![
257                        [font_awesome::MINIMUM_CODEPOINT as u32, font_awesome::MAXIMUM_CODEPOINT as u32]
258                    ])
259                }
260            ]
261        };
262        let imgui = imgui::ImGui::create(&mut imgui_info)?;
263
264        // pmfx
265        let mut pmfx = pmfx::Pmfx::<D>::create(&mut device, info.shader_heap_size);
266
267        // core pipelines
268        pmfx.load(super::get_data_path("shaders/imdraw").as_str())?;
269        pmfx.create_render_pipeline(&device, "imdraw_blit", swap_chain.get_backbuffer_pass())?;
270
271        let size = main_window.get_size();
272        pmfx.update_window(&mut device, (size.x as f32, size.y as f32), "main_window")?;
273
274        // blit pmfx
275        let unit_quad_mesh = primitives::create_unit_quad_mesh(&mut device);
276
277        // default cmd buf
278        let cmd_buf = device.create_cmd_buf(info.num_buffers);
279
280        // create a client
281        let mut client = Client {
282            app,
283            device,
284            main_window,
285            swap_chain,
286            cmd_buf,
287            pmfx,
288            imdraw,
289            imgui,
290            unit_quad_mesh,
291            user_config: user_config.clone(),
292            plugins: Vec::new(),
293            libs: HashMap::new(),
294            time: Time::new(),
295            delta_history: VecDeque::new(),
296            instance_name: info.name,
297            status_bar_height: STATUS_BAR_HEIGHT
298        };
299
300        // automatically load plugins from prev session
301        if let Some(plugin_info) = &user_config.plugins {
302            for (name, info) in plugin_info {
303                client.add_plugin_lib(name, &info.path)
304            }
305        }
306   
307        Ok(client)
308    }
309
310    fn update_time(&mut self) {
311        // sync to new frame time
312        let prev_frame_start = self.time.frame_start;
313        self.time.frame_start = SystemTime::now();
314        let elapsed = prev_frame_start.elapsed();
315        if let Ok(elapsed) = elapsed {
316            // raw delta each frame
317            self.time.raw_delta = elapsed.as_secs_f32();
318            
319            // track delta
320            if !self.time.delta_paused {
321                self.time.delta = elapsed.as_secs_f32() * self.time.time_scale;
322
323                // increment accumulated
324                self.time.accumulated += self.time.delta;
325
326                // record history
327                self.delta_history.push_front(self.time.delta);
328                if self.delta_history.len() > SMOOTH_DELTA_FRAMES {
329                    self.delta_history.pop_back();
330                }
331    
332                // calculate smooth delta
333                let sum : f32 = self.delta_history.iter().sum();
334                self.time.smooth_delta = sum / self.delta_history.len() as f32;
335            }
336            else {
337                // paused delta
338                self.time.delta = 0.0;
339                self.time.accumulated = 0.0;
340                self.time.smooth_delta = 0.0;
341                self.delta_history.clear();
342            }
343        }
344    }
345
346    /// Start a new frame syncronised to the swap chain
347    pub fn new_frame(&mut self) -> Result<(), super::Error> {
348        self.update_time();
349
350        // update window and swap chain for the new frame
351        self.main_window.update(&mut self.app);
352        self.swap_chain.update::<A>(&mut self.device, &self.main_window, &mut self.cmd_buf);
353
354        // reset main command buffer
355        self.cmd_buf.reset(&self.swap_chain);
356
357        // start imgui new frame
358        self.imgui.new_frame(&mut self.app, &mut self.main_window, &mut self.device);
359        self.imgui.add_main_dock(self.status_bar_height);
360        self.status_bar_height = self.imgui.add_status_bar(self.status_bar_height);
361
362        // check for focus on the dock
363        let dock_input = self.imgui.main_dock_hovered();
364        self.app.set_input_enabled(
365            !self.imgui.want_capture_keyboard() || dock_input, 
366            !self.imgui.want_capture_mouse() || dock_input);
367
368        let size = self.main_window.get_size();
369        self.pmfx.update_window(&mut self.device, (size.x as f32, size.y as f32), "main_window")?;
370
371        let size = self.imgui.get_main_dock_size();
372        self.pmfx.update_window(&mut self.device, size, "main_dock")?;
373
374        // start new pmfx frame
375        self.pmfx.new_frame(&mut self.device, &self.swap_chain)?;
376
377        // user config changes
378        self.update_user_config_windows();
379
380        Ok(())
381    }
382
383    /// internal function to manage tracking user config values and changes, writes to disk if change are detected
384    fn save_user_config(&mut self) {
385        let user_config_file_text = serde_json::to_string_pretty(&self.user_config).unwrap();
386        let user_config_path = super::get_data_path("../user_config.json");
387        std::fs::File::create(&user_config_path).unwrap();
388        std::fs::write(&user_config_path, user_config_file_text).unwrap();
389    }
390
391    /// Intenral function to save both the `user_config.json` and `imgui.ini` to a disk location, for saving re-usable presets
392    fn save_configs_to_location(&mut self, path: &str) {
393        let user_config_file_text = serde_json::to_string_pretty(&self.user_config).unwrap();
394        let user_config_path = format!("{}/user_config.json", path);
395        std::fs::File::create(&user_config_path).unwrap();
396        std::fs::write(&user_config_path, user_config_file_text).unwrap();
397    }
398    
399    /// internal function to manage tracking user config values and changes, writes to disk if change are detected
400    fn update_user_config_windows(&mut self) {
401        // track any changes and write once
402        let mut invalidated = false;
403        
404        // main window pos / size
405        let current = self.main_window.get_window_rect();
406        if current.x > 0 && current.y > 0 && self.user_config.main_window_rect != current {
407            self.user_config.main_window_rect = self.main_window.get_window_rect();
408            invalidated = true;
409        }
410
411        // console window pos / size
412        if let Some(console_window_rect) = self.user_config.console_window_rect {
413            let current = self.app.get_console_window_rect();
414            if current.x > 0 && current.y > 0 && console_window_rect != current {
415                self.user_config.console_window_rect = Some(self.app.get_console_window_rect());
416                invalidated = true;
417            }
418        }
419        else {
420            let current = self.app.get_console_window_rect();
421            if current.x > 0 && current.y > 0 {
422                self.user_config.console_window_rect = Some(self.app.get_console_window_rect());
423                invalidated = true;
424            }
425        }
426
427        // write to file
428        if invalidated {
429            self.save_user_config();
430        }
431    }
432
433    /// Render and display a pmfx target 'blit_view_name' to the main window, draw imgui and swap buffers
434    pub fn present(&mut self, blit_view_name: &str) {
435        // execute pmfx command buffers first
436        self.pmfx.execute(&mut self.device);
437
438        // main pass
439        self.cmd_buf.transition_barrier(&gfx::TransitionBarrier {
440            texture: Some(self.swap_chain.get_backbuffer_texture()),
441            buffer: None,
442            state_before: gfx::ResourceState::Present,
443            state_after: gfx::ResourceState::RenderTarget,
444        });
445        
446        // clear window
447        self.cmd_buf.begin_render_pass(self.swap_chain.get_backbuffer_pass_mut());
448        self.cmd_buf.end_render_pass();
449
450        // blit
451        self.cmd_buf.begin_render_pass(self.swap_chain.get_backbuffer_pass_no_clear());
452       
453        // get srv index of the pmfx target to blit to the window, if the target exists
454        if let Some(tex) = self.pmfx.get_texture(blit_view_name) {
455            // blit to main window
456            let vp_rect = self.main_window.get_viewport_rect();
457            self.cmd_buf.begin_event(0xff65cf82, "blit_pmfx");
458            self.cmd_buf.set_viewport(&gfx::Viewport::from(vp_rect));
459            self.cmd_buf.set_scissor_rect(&gfx::ScissorRect::from(vp_rect));
460            let srv = tex.get_srv_index().unwrap();
461            let fmt = self.swap_chain.get_backbuffer_pass_mut().get_format_hash();
462
463            let pipeline = self.pmfx.get_render_pipeline_for_format("imdraw_blit", fmt).unwrap();
464            let heap = self.device.get_shader_heap();
465
466            self.cmd_buf.set_render_pipeline(pipeline);
467            self.cmd_buf.push_render_constants(0, 2, 0, &[vp_rect.width as f32, vp_rect.height as f32]);
468            
469            self.cmd_buf.set_binding(pipeline, heap, 1, srv);
470            
471            self.cmd_buf.set_index_buffer(&self.unit_quad_mesh.ib);
472            self.cmd_buf.set_vertex_buffer(&self.unit_quad_mesh.vb, 0);
473            self.cmd_buf.draw_indexed_instanced(6, 1, 0, 0, 0);
474            self.cmd_buf.end_event();
475        }
476
477        let image_heaps = vec![
478            &self.pmfx.shader_heap
479        ];
480
481        // render imgui
482        self.cmd_buf.begin_event(0xff1fb6c4, "imgui");
483        
484        self.imgui.render(
485            &mut self.app, 
486            &mut self.main_window, 
487            &mut self.device, 
488            &mut self.cmd_buf,
489            &image_heaps
490        );
491        
492        self.cmd_buf.end_event();
493
494        self.cmd_buf.end_render_pass();
495        
496        // transition to present
497        self.cmd_buf.transition_barrier(&gfx::TransitionBarrier {
498            texture: Some(self.swap_chain.get_backbuffer_texture()),
499            buffer: None,
500            state_before: gfx::ResourceState::RenderTarget,
501            state_after: gfx::ResourceState::Present,
502        });
503        self.cmd_buf.close().unwrap();
504
505        // execute the main window command buffer + swap
506        self.device.execute(&self.cmd_buf);
507        self.swap_chain.swap(&self.device);
508    }
509
510    /// This assumes you pass the path to a `Cargo.toml` for a `dylib` which you want to load dynamically
511    /// The lib can implement the `hotline_plugin!` and `Plugin` trait, but that is not required
512    /// You can also just load libs and use `lib.get_symbol` to find custom callable code for other plugins.
513    pub fn add_plugin_lib(&mut self, name: &str, path: &str) {
514        let abs_path = if path == "/plugins" {
515            super::get_data_path("../../plugins")
516        }
517        else {
518            String::from(path)
519        };
520
521        let lib_path = PathBuf::from(abs_path.to_string())
522            .join("target")
523            .join(crate::get_config_name())
524            .to_str().unwrap().to_string();
525        
526        let src_path = PathBuf::from(abs_path.to_string())
527            .join(name)
528            .join("src")
529            .join("lib.rs")
530            .to_str().unwrap().to_string();
531
532        let plugin = PluginReloadResponder {
533            name: name.to_string(),
534            path: abs_path.to_string(),
535            output_filepath: lib_path.to_string(),
536            files: vec![
537                src_path
538            ],
539        };
540
541        if !std::path::Path::new(&lib_path).join(name.to_string() + ".dll").exists() {
542            println!("hotline_rs::client:: plugin not found: {}/{}", lib_path, name);
543            return;
544        }
545
546        println!("hotline_rs::client:: loading plugin: {}/{}", lib_path, name);
547        let lib = hot_lib_reloader::LibReloader::new(&lib_path, name, None).unwrap();
548        unsafe {
549            // create instance if it is a Plugin trait
550            let create = lib.get_symbol::<unsafe extern fn() -> *mut core::ffi::c_void>("create".as_bytes());
551            
552            let instance = if let Ok(create) = create {
553                // create function returns pointer to instance
554                create()
555            }
556            else {
557                // allow null instances, in plugins which only export function calls and not plugin traits
558                std::ptr::null_mut()
559            };
560            
561            // keep hold of everything for updating
562            self.plugins.push( PluginCollection {
563                name: name.to_string(),
564                instance, 
565                reloader: Reloader::create(Box::new(plugin)),
566                state: PluginState::Setup
567            });
568            self.libs.insert(name.to_string(), lib);
569        }
570
571        // Track the plugin for auto re-loading
572        if self.user_config.plugins.is_none() {
573            self.user_config.plugins = Some(HashMap::new());
574        }
575
576        // plugins inside the main repro can have the abs path truncated so they are portable
577        let hotline_path = super::get_data_path("../..").replace('\\', "/");
578        let path = abs_path.replace(&hotline_path, "").replace('\\', "/");
579
580        if let Some(plugin_info) = &mut self.user_config.plugins {
581            if plugin_info.contains_key(name) {
582                plugin_info.remove(name);
583            }
584            plugin_info.insert(name.to_string(), PluginInfo { path });
585        }
586    }
587
588    /// Intenral core-ui function, it displays the main menu bar in the main window and
589    /// A plugin menu which allows users to reload or unload live plugins.
590    fn core_ui(&mut self) {
591        // main menu bar 
592        if self.imgui.begin_main_menu_bar() {
593            
594            if self.imgui.begin_menu("File") {
595                // allow us to add plugins from files (libs)
596                if self.imgui.menu_item("Open") {
597                    let file = A::open_file_dialog(os::OpenFileDialogFlags::FILES, vec![".toml"]);
598                    if let Ok(file) = file {
599                        if !file.is_empty() {
600                            // add plugin from dll
601                            let plugin_path = PathBuf::from(file[0].to_string());
602                            let plugin_name = plugin_path.parent().unwrap().file_name().unwrap();
603                            let plugin_path = plugin_path.parent().unwrap().parent().unwrap();
604                            self.add_plugin_lib(plugin_name.to_str().unwrap(), plugin_path.to_str().unwrap());
605                        }
606                    }
607                }
608
609                // save configs for presets
610                if self.imgui.menu_item("Save User Config") {
611                    let folder = A::open_file_dialog(os::OpenFileDialogFlags::FOLDERS, Vec::new());
612                    if let Ok(folder) = folder {
613                        if !folder.is_empty() {
614                            self.save_configs_to_location(&folder[0]);
615                            self.imgui.save_ini_settings_to_location(&folder[0]);
616                        }
617                    }
618                }
619
620                self.imgui.separator();
621                if self.imgui.menu_item("Exit") {
622                    self.app.exit(0);
623                }
624
625                self.imgui.end_menu();
626            }
627
628            // menu per plugin to allow the user to unload or reload
629            if self.imgui.begin_menu("Plugin") {
630                for plugin in &mut self.plugins {
631                    if self.imgui.begin_menu(&plugin.name) {
632                        if self.imgui.menu_item("Reload") {
633                            plugin.state = PluginState::Setup;
634                        }
635                        if self.imgui.menu_item("Unload") {
636                            plugin.state = PluginState::Unload;
637                        }
638                        self.imgui.end_menu();
639                    }
640                }
641                self.imgui.end_menu();
642            }
643
644            if self.imgui.begin_menu("Time") {
645                // pause dt
646                let pause_text = if self.time.delta_paused {
647                    "Resume Delta Time"
648                }
649                else {
650                    "Pause Delta Time"
651                };
652
653                if self.imgui.menu_item(pause_text) {
654                    self.time.delta_paused = !self.time.delta_paused;
655                }
656
657                // pause updates
658                let pause_text = if self.time.paused {
659                    "Resume Updates"
660                }
661                else {
662                    "Pause Updates"
663                };
664
665                if self.imgui.menu_item(pause_text) {
666                    self.time.paused = !self.time.paused;
667                }
668
669                // fixed time step
670                if let Some(mut step) = self.time.fixed_delta {
671                    let mut fixed = true;
672                    if self.imgui.checkbox("##", &mut fixed) && !fixed {
673                        self.time.fixed_delta = None;
674                    }
675                    self.imgui.same_line();
676                    self.imgui.dummy(5.0, 0.0);
677                    self.imgui.same_line();
678                    if self.imgui.input_float("Fixed Timestep", &mut step) {
679                        self.time.fixed_delta = Some(step)
680                    }
681                }
682                else {
683                    let mut fixed = false;
684                    if self.imgui.checkbox("Fixed Timestep", &mut fixed) && fixed {
685                        self.time.fixed_delta = Some(1.0 / 60.0);
686                    }
687                }
688
689                // time scaling
690                self.imgui.input_float("Time Scale", &mut self.time.time_scale);
691                
692                self.imgui.end_menu();
693            }
694
695            self.imgui.end_main_menu_bar();
696        }
697        // status bar
698        if self.imgui.begin_window("status_bar") {
699            // fps / cpu / gpu
700            let fps = maths_rs::round(1.0 / self.time.raw_delta) as u32;
701            let cpu_ms = self.time.raw_delta * 1000.0;
702            let gpu_ms = self.pmfx.get_total_stats().gpu_time_ms;
703            self.imgui.text(
704                &format!("fps: {} | cpu: {:.2}(ms) | gpu: {:.2}(ms)", 
705                fps, cpu_ms, gpu_ms
706            ));
707            self.imgui.same_line();
708
709            // hot reloading (plugins)
710            let mut hot_name = String::from("");
711            let mut col = vec4f(1.0, 1.0, 1.0, 1.0);
712            for plugin in &self.plugins {
713                if plugin.reloader.is_hot() {
714                    if !hot_name.is_empty() {
715                        hot_name += " | "
716                    }
717                    hot_name = plugin.name.to_string();
718                    col = vec4f(1.0, 0.0, 0.0, 1.0);
719                }
720            }
721
722            // hot reloading (pmfx)
723            if self.pmfx.reloader.is_hot() {
724                if !hot_name.is_empty() {
725                    hot_name += " | "
726                }
727                hot_name += "pmfx";
728                col = vec4f(1.0, 0.0, 0.0, 1.0);
729            }
730
731            let hot_text = format!("{} {}", hot_name, font_awesome::strs::FIRE);
732            self.imgui.right_align(self.imgui.calc_text_size(&hot_text).0 + 10.0);
733            self.imgui.colour_text(&hot_text, col);
734        }
735        self.imgui.end();
736    }
737
738    /// Internal plugin yupdate function process reloads, setups and updates of hooked in plugins
739    fn update_plugins(mut self) -> Self {
740        // take the plugin mem so we can decouple the shared mutability between client and plugins
741        let mut plugins = std::mem::take(&mut self.plugins);
742
743        // call plugin ui functions
744        for plugin in &mut plugins {
745            let lib = self.libs.get(&plugin.name).expect("hotline::client: lib missing for plugin");
746            unsafe {
747                let ui = lib.get_symbol::<unsafe extern fn(Self, *mut core::ffi::c_void, *mut core::ffi::c_void) -> Self>("ui".as_bytes());
748                if let Ok(ui_fn) = ui {
749                    let imgui_ctx = self.imgui.get_current_context();
750                    self = ui_fn(self, plugin.instance, imgui_ctx);
751                }
752            }
753        }
754
755        // check for reloads
756        let mut reload = false;
757        for plugin in &mut plugins {
758            if plugin.reloader.check_for_reload() == reloader::ReloadState::Available || plugin.state == PluginState::Reload {
759                    self.swap_chain.wait_for_last_frame();
760                    reload = true;
761                    plugin.state = PluginState::Reload;
762                    break;
763            }
764        }
765
766        // if we require a reload, we also re-setup all the other plugins
767        // this could be configured to only re-setup necessary plugins that are dependent
768        if reload {
769            for plugin in &mut plugins {
770                if plugin.state == PluginState::None {
771                    plugin.state = PluginState::Setup 
772                }
773            }
774        }
775
776        // perfrom unloads this will clean up memory, setup will be called again afterwards
777        for plugin in &plugins {
778            if plugin.state != PluginState::None {
779                unsafe {
780                    let lib = self.libs.get(&plugin.name).expect("hotline::client: lib missing for plugin");
781                    let unload = lib.get_symbol::<unsafe extern fn(Self, PluginInstance) -> Self>("unload".as_bytes());
782                    if let Ok(unload_fn) = unload {
783                        self = unload_fn(self, plugin.instance);
784                    }
785                }
786            }
787        }
788
789        // remove unloaded plugins entirely
790        loop {
791            let mut todo = false;
792            for i in 0..plugins.len() {
793                if plugins[i].state == PluginState::Unload {
794                    if let Some(plugin_info) = &mut self.user_config.plugins {
795                        plugin_info.remove_entry(&plugins[i].name);
796                    }
797                    self.libs.remove_entry(&plugins[i].name);
798                    plugins.remove(i);
799                    todo = true;
800                    break;
801                }
802            }
803            if !todo {
804                break;
805            }
806        }
807        
808        // reload, actual reloading the lib of any libs which had changes
809        for plugin in &mut plugins {
810            if plugin.state == PluginState::Reload {                        
811                // wait for lib reloader itself
812                let lib = self.libs.get_mut(&plugin.name).expect("hotline::client: lib missing for plugin");
813                let start = SystemTime::now();
814                loop {
815                    if lib.update().unwrap() {
816                        break;
817                    }
818                    if start.elapsed().unwrap() > std::time::Duration::from_secs(10) {
819                        println!("hotline::client: [warning] reloading plugin: {} timed out", plugin.name);
820                        break;
821                    }
822                    std::hint::spin_loop();
823                }
824
825                // signal it's ok to continue
826                plugin.reloader.complete_reload();
827
828                // create a new instance of the plugin
829                unsafe {
830                    let create = lib.get_symbol::<unsafe extern fn() -> *mut core::ffi::c_void>("create".as_bytes());
831                    if let Ok(create_fn) = create {
832                        plugin.instance = create_fn();
833                    }
834                }
835                // after reload, setup everything again
836                plugin.state = PluginState::Setup;
837            }
838        }
839
840        // setup
841        for plugin in &plugins {
842            let lib = self.libs.get(&plugin.name).expect("hotline::client: lib missing for plugin");
843            unsafe {
844                if plugin.state == PluginState::Setup {
845                    let setup = lib.get_symbol::<unsafe extern fn(Self, *mut core::ffi::c_void) -> Self>("setup".as_bytes());
846                    if let Ok(setup_fn) = setup {
847                        self = setup_fn(self, plugin.instance);
848                    }
849                }
850            }
851        }
852        
853        // update
854        if !self.time.paused {
855            for plugin in &mut plugins {
856                let lib = self.libs.get(&plugin.name).expect("hotline::client: lib missing for plugin");
857                unsafe {
858                    let update = lib.get_symbol::<unsafe extern fn(Self, *mut core::ffi::c_void) -> Self>("update".as_bytes());
859                    if let Ok(update_fn) = update {
860                        self = update_fn(self, plugin.instance);
861                    }
862                }
863                plugin.state = PluginState::None;
864            }
865        }
866
867        // move plugins back and return self
868        self.plugins = plugins;
869        self
870    }
871
872    /// Unloads all plugins and drops all mem
873    fn unload(mut self) {
874        let plugins = std::mem::take(&mut self.plugins);
875        for plugin in &plugins {
876            unsafe {
877                let lib = self.libs.get(&plugin.name).expect("hotline::client: lib missing for plugin");
878                let unload = lib.get_symbol::<unsafe extern fn(Self, PluginInstance) -> Self>("unload".as_bytes());
879                if let Ok(unload_fn) = unload {
880                    self = unload_fn(self, plugin.instance);
881                }
882            }
883        }
884    }
885
886    /// Allows users to pass serializable data which is stored into the `UserConfig` for the app.
887    /// Plugin data is arrange as a json object / dictionary hash map as so:
888    /// "plugin_data": {
889    ///     "plugin_name": {
890    ///         "plugin_data_members": true
891    ///     }
892    ///     "another_plugin_name": {
893    ///         "another_plugin_name_data": true
894    ///     }
895    /// }
896    pub fn serialise_plugin_data<T: Serialize>(&mut self, plugin_name: &str, data: &T) {
897        let serialised = serde_json::to_value(data).unwrap();
898        if self.user_config.plugin_data.is_none() {
899            self.user_config.plugin_data = Some(HashMap::new());
900        }
901
902        if let Some(plugin_data) = &mut self.user_config.plugin_data {
903            *plugin_data.entry(plugin_name.to_string()).or_insert(serde_json::Value::default()) = serialised;
904        }
905    }
906
907    /// Deserialises string json into a `T` returning defaults if the entry does not exist
908    pub fn deserialise_plugin_data<T: Default + serde::de::DeserializeOwned>(&mut self, plugin_name: &str) -> T {
909        // deserialise user data saved from a previous session
910        if let Some(plugin_data) = &self.user_config.plugin_data {
911            if plugin_data.contains_key(plugin_name) {
912                serde_json::from_value(plugin_data[plugin_name].clone()).unwrap()
913            }
914            else {
915                T::default()
916            }
917        }
918        else {
919            T::default()
920        }
921    }
922    
923    /// Very simple run loop which can take control of your application, you could roll your own
924    pub fn run(mut self) -> Result<(), super::Error> {
925        while self.app.run() {
926            
927            self.new_frame()?;
928
929            self.core_ui();
930            self.pmfx.show_ui(&mut self.imgui, true);
931
932            self = self.update_plugins();
933
934            if let Some(tex) = self.pmfx.get_texture("main_colour") {
935                self.imgui.image_window("main_dock", tex);
936            }
937
938            // record time before we do gpu stuff
939            if let Ok(elapsed) = self.time.frame_start.elapsed() {
940                self.time.update_delta = elapsed.as_secs_f32();
941            }
942
943            self.present("");
944
945            // print d3d debug info messages to the console
946            let info_queue = self.device.get_info_queue_messages()?;
947            for msg in info_queue {
948                println!("{}", msg);
949            }
950
951            // cleanup heaps
952            self.pmfx.shader_heap.cleanup_dropped_resources(&self.swap_chain);
953            self.device.cleanup_dropped_resources(&self.swap_chain);
954        }
955
956        // save out values for next time
957        self.save_user_config();
958        self.imgui.save_ini_settings();
959        self.swap_chain.wait_for_last_frame();
960
961        // unloads plugins, dropping all gpu resources
962        self.unload();
963
964        Ok(())
965    }
966
967    /// Very simple run loop which can take control of your application, you could roll your own
968    pub fn run_once(mut self) -> Result<(), super::Error> {
969        for i in 0..3 {
970            self.new_frame()?;
971        
972            self.core_ui();
973            self.pmfx.show_ui(&mut self.imgui, true);
974    
975            self = self.update_plugins();
976    
977            if let Some(tex) = self.pmfx.get_texture("main_colour") {
978                self.imgui.image_window("main_dock", tex);
979            }
980            
981            // execute pmfx command buffers first
982            self.pmfx.execute(&mut self.device);
983    
984            // main pass
985            self.cmd_buf.transition_barrier(&gfx::TransitionBarrier {
986                texture: Some(self.swap_chain.get_backbuffer_texture()),
987                buffer: None,
988                state_before: gfx::ResourceState::Present,
989                state_after: gfx::ResourceState::RenderTarget,
990            });
991    
992            // clear window
993            self.cmd_buf.begin_render_pass(self.swap_chain.get_backbuffer_pass_mut());
994            self.cmd_buf.end_render_pass();
995    
996            // blit
997            self.cmd_buf.begin_render_pass(self.swap_chain.get_backbuffer_pass_no_clear());
998    
999            // render imgui
1000            self.cmd_buf.begin_event(0xff1fb6c4, "imgui");
1001
1002            let image_heaps = vec![
1003                &self.pmfx.shader_heap
1004            ];
1005            
1006            self.imgui.render(
1007                &mut self.app, 
1008                &mut self.main_window, 
1009                &mut self.device, 
1010                &mut self.cmd_buf,
1011                &image_heaps
1012            );
1013            
1014            self.cmd_buf.end_event();
1015    
1016            self.cmd_buf.end_render_pass();
1017    
1018            // transition to present
1019            self.cmd_buf.transition_barrier(&gfx::TransitionBarrier {
1020                texture: Some(self.swap_chain.get_backbuffer_texture()),
1021                buffer: None,
1022                state_before: gfx::ResourceState::RenderTarget,
1023                state_after: gfx::ResourceState::Present,
1024            });
1025    
1026            let readback_request = self.cmd_buf.read_back_backbuffer(&self.swap_chain)?;
1027    
1028            self.cmd_buf.close().unwrap();
1029    
1030            // execute the main window command buffer + swap
1031            self.device.execute(&self.cmd_buf);
1032            self.swap_chain.swap(&self.device);
1033
1034            self.swap_chain.wait_for_last_frame();
1035
1036            // wait for the second frame so the first one has data to read
1037            if i == 2 {
1038                let data = readback_request.map(&gfx::MapInfo {
1039                    subresource: 0,
1040                    read_start: 0,
1041                    read_end: usize::MAX
1042                })?;
1043        
1044                let output_dir = "target/test_output";
1045                if !std::path::PathBuf::from(output_dir.to_string()).exists() {
1046                    std::fs::create_dir(output_dir)?;
1047                }
1048                
1049                let output_filepath = format!("{}/{}.png", output_dir, &self.instance_name);
1050                image::write_to_file_from_gpu(&output_filepath, &data)?;
1051        
1052                readback_request.unmap();
1053            }
1054
1055            // cleanup heaps
1056            self.pmfx.shader_heap.cleanup_dropped_resources(&self.swap_chain);
1057            self.device.cleanup_dropped_resources(&self.swap_chain);
1058        }
1059
1060        self.swap_chain.wait_for_last_frame();
1061        
1062        // unloads plugins, dropping all gpu resources
1063        self.unload();
1064
1065        Ok(())
1066    }
1067}