Implemented hot reloading for EGUI (needs duplicated code from anim crate refactoring out)

This commit is contained in:
Ben Wallis 2021-05-05 21:05:22 +01:00
parent 63b8fe1a5f
commit 0654d78260
14 changed files with 433 additions and 37 deletions

11
Cargo.lock generated
View File

@ -6382,11 +6382,22 @@ dependencies = [
name = "veloren-voxygen-egui"
version = "0.9.0"
dependencies = [
"egui",
"egui_winit_platform",
"find_folder",
"lazy_static",
"libloading 0.7.0",
"notify 5.0.0-pre.6",
"tracing",
"veloren-client",
"veloren-common",
]
[[package]]
name = "veloren-voxygen-egui-dyn"
version = "0.9.0"
dependencies = [
"veloren-voxygen-egui",
]
[[package]]

View File

@ -21,6 +21,7 @@ members = [
"voxygen/anim/dyn",
"voxygen/i18n",
"voxygen/egui",
"voxygen/egui/dyn",
"world",
"network",
"network/protocol",

20
common/src/debug_info.rs Normal file
View File

@ -0,0 +1,20 @@
use std::time::Duration;
use crate::comp;
pub struct DebugInfo {
pub tps: f64,
pub frame_time: Duration,
pub ping_ms: f64,
pub coordinates: Option<comp::Pos>,
pub velocity: Option<comp::Vel>,
pub ori: Option<comp::Ori>,
pub num_chunks: u32,
pub num_lights: u32,
pub num_visible_chunks: u32,
pub num_shadow_chunks: u32,
pub num_figures: u32,
pub num_figures_visible: u32,
pub num_particles: u32,
pub num_particles_visible: u32,
}

View File

@ -77,6 +77,7 @@ pub mod uid;
#[cfg(not(target_arch = "wasm32"))] pub mod vol;
#[cfg(not(target_arch = "wasm32"))]
pub mod volumes;
pub mod debug_info;
#[cfg(not(target_arch = "wasm32"))]
pub use cached_spatial_grid::CachedSpatialGrid;

View File

@ -23,7 +23,7 @@ buildInputs = ["xorg.libxcb"]
[features]
hot-anim = ["anim/use-dyn-lib"]
hot-egui = ["egui/use-dyn-lib"]
hot-egui = ["voxygen-egui/use-dyn-lib"]
singleplayer = ["server"]
simd = ["vek/platform_intrinsics"]
tracy = ["profiling", "profiling/profile-with-tracy", "common/tracy", "common-ecs/tracy", "common-frontend/tracy", "common-net/tracy", "common-systems/tracy", "common-state/tracy", "client/tracy"]

23
voxygen/egui/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
authors = ["Ben Wallis <atomyc@gmail.com>"]
name = "veloren-voxygen-egui"
edition = "2018"
version = "0.9.0"
[features]
use-dyn-lib = ["libloading", "notify", "lazy_static", "tracing", "find_folder"]
be-dyn-lib = []
[dependencies]
client = {package = "veloren-client", path = "../../client"}
common = {package = "veloren-common", path = "../../common"}
egui = "0.11"
egui_winit_platform = "0.6"
# Hot Reloading
find_folder = {version = "0.3.0", optional = true}
lazy_static = {version = "1.4.0", optional = true}
libloading = {version = "0.7", optional = true}
notify = {version = "5.0.0-pre.2", optional = true}
tracing = {version = "0.1", optional = true}

View File

@ -0,0 +1,17 @@
[package]
authors = ["Imbris <imbrisf@gmail.com>"]
edition = "2018"
name = "veloren-voxygen-egui-dyn"
version = "0.9.0"
[lib]
# Using dylib instead of cdylib increases the size 3 -> 13 mb
# but it is needed to expose the symbols from the anim crate :(
# effect on compile time appears to be insignificant
crate-type = ["dylib"]
[features]
be-dyn-lib = ["veloren-voxygen-egui/be-dyn-lib"]
[dependencies]
veloren-voxygen-egui = { path = "../" }

View File

@ -0,0 +1,14 @@
//! This crate hacks around the inability to dynamically specify the
//! `crate-type` for cargo to build.
//!
//! For more details on the issue this is a decent starting point: https://github.com/rust-lang/cargo/pull/8789
//!
//! This crate avoids use building the dynamic lib when it isn't needed and the
//! same with the non dynamic build. Additionally, this allows compilation to
//! start earlier since a cdylib doesn't pipeline with it's dependencies.
//!
//! NOTE: the `be-dyn-lib` feature must be used for this crate to be useful, it
//! is not on by default becaue this causes cargo to switch the feature on in
//! the anim crate when compiling the static lib into voxygen.
#[cfg(feature = "be-dyn-lib")]
pub use veloren_voxygen_egui::*;

258
voxygen/egui/src/dyn_lib.rs Normal file
View File

@ -0,0 +1,258 @@
use lazy_static::lazy_static;
use libloading::Library;
use notify::{immediate_watcher, EventKind, RecursiveMode, Watcher};
use std::{
process::{Command, Stdio},
sync::{mpsc, Mutex},
time::Duration,
};
use find_folder::Search;
use std::{env, path::PathBuf};
use tracing::{debug, error, info};
#[cfg(target_os = "windows")]
const COMPILED_FILE: &str = "veloren_voxygen_egui_dyn.dll";
#[cfg(target_os = "windows")]
const ACTIVE_FILE: &str = "veloren_voxygen_egui_dyn_active.dll";
#[cfg(not(target_os = "windows"))]
const COMPILED_FILE: &str = "libveloren_voxygen_anim_dyn.so";
#[cfg(not(target_os = "windows"))]
const ACTIVE_FILE: &str = "libveloren_voxygen_anim_dyn_active.so";
// This option is required as `hotreload()` moves the `LoadedLib`.
lazy_static! {
pub static ref LIB: Mutex<Option<LoadedLib>> = Mutex::new(Some(LoadedLib::compile_load()));
}
/// LoadedLib holds a loaded dynamic library and the location of library file
/// with the appropriate OS specific name and extension i.e.
/// `libvoxygen_anim_dyn_active.dylib`, `voxygen_anim_dyn_active.dll`.
///
/// # NOTE
/// DOES NOT WORK ON MACOS, due to some limitations with hot-reloading the
/// `.dylib`.
pub struct LoadedLib {
/// Loaded library.
pub lib: Library,
/// Path to the library.
pub lib_path: PathBuf,
}
impl LoadedLib {
/// Compile and load the dynamic library
///
/// This is necessary because the very first time you use hot reloading you
/// wont have the library, so you can't load it until you have compiled it!
fn compile_load() -> Self {
#[cfg(target_os = "macos")]
error!("The hot reloading feature does not work on macos.");
// Compile
if !compile() {
panic!("Animation compile failed.");
} else {
info!("Animation compile succeeded.");
}
copy(&LoadedLib::determine_path());
Self::load()
}
/// Load a library from disk.
///
/// Currently this is pretty fragile, it gets the path of where it thinks
/// the dynamic library should be and tries to load it. It will panic if it
/// is missing.
fn load() -> Self {
let lib_path = LoadedLib::determine_path();
// Try to load the library.
let lib = match unsafe { Library::new(lib_path.clone()) } {
Ok(lib) => lib,
Err(e) => panic!(
"Tried to load dynamic library from {:?}, but it could not be found. A potential \
reason is we may require a special case for your OS so we can find it. {:?}",
lib_path, e
),
};
Self { lib, lib_path }
}
/// Determine the path to the dynamic library based on the path of the
/// current executable.
fn determine_path() -> PathBuf {
let current_exe = env::current_exe();
// If we got the current_exe, we need to go up a level and then down
// in to debug (in case we were in release or another build dir).
let mut lib_path = match current_exe {
Ok(mut path) => {
// Remove the filename to get the directory.
path.pop();
// Search for the debug directory.
let dir = Search::ParentsThenKids(1, 1)
.of(path)
.for_folder("debug")
.expect(
"Could not find the debug build directory relative to the current \
executable.",
);
debug!(?dir, "Found the debug build directory.");
dir
},
Err(e) => {
panic!(
"Could not determine the path of the current executable, this is needed to \
hotreload the dynamic library. {:?}",
e
);
},
};
// Determine the platform specific path and push it onto our already
// established target/debug dir.
lib_path.push(ACTIVE_FILE);
lib_path
}
}
/// Initialise a watcher.
///
/// The assumption is that this is run from the voxygen crate's root directory
/// as it will watch the relative path `egui` for any changes to `.rs`
/// files. Upon noticing changes it will wait a moment and then recompile.
pub fn init() {
// Make sure first compile is done by accessing the lazy_static and then
// immediately dropping (because we don't actually need it).
drop(LIB.lock());
// TODO: use crossbeam
let (reload_send, reload_recv) = mpsc::channel();
// Start watcher
let mut watcher = immediate_watcher(move |res| event_fn(res, &reload_send)).unwrap();
// Search for the anim directory.
let anim_dir = Search::Kids(1)
.for_folder("egui")
.expect("Could not find the egui crate directory relative to the current directory");
watcher.watch(anim_dir, RecursiveMode::Recursive).unwrap();
// Start reloader that watcher signals
// "Debounces" events since I can't find the option to do this in the latest
// `notify`
std::thread::Builder::new()
.name("voxygen_egui_watcher".into())
.spawn(move || {
let mut modified_paths = std::collections::HashSet::new();
while let Ok(path) = reload_recv.recv() {
modified_paths.insert(path);
// Wait for any additional modify events before reloading
while let Ok(path) = reload_recv.recv_timeout(Duration::from_millis(300)) {
modified_paths.insert(path);
}
info!(
?modified_paths,
"Hot reloading egui because files in `egui` modified."
);
hotreload();
}
})
.unwrap();
// Let the watcher live forever
std::mem::forget(watcher);
}
/// Event function to hotreload the dynamic library
///
/// This is called by the watcher to filter for modify events on `.rs` files
/// before sending them back.
fn event_fn(res: notify::Result<notify::Event>, sender: &mpsc::Sender<String>) {
match res {
Ok(event) => match event.kind {
EventKind::Modify(_) => {
event
.paths
.iter()
.filter(|p| p.extension().map(|e| e == "rs").unwrap_or(false))
.map(|p| p.to_string_lossy().into_owned())
// Signal reloader
.for_each(|p| { let _ = sender.send(p); });
},
_ => {},
},
Err(e) => error!(?e, "egui hotreload watcher error."),
}
}
/// Hotreload the dynamic library
///
/// This will reload the dynamic library by first internally calling compile
/// and then reloading the library.
fn hotreload() {
// Do nothing if recompile failed.
if compile() {
let mut lock = LIB.lock().unwrap();
// Close lib.
let loaded_lib = lock.take().unwrap();
loaded_lib.lib.close().unwrap();
copy(&loaded_lib.lib_path);
// Open new lib.
*lock = Some(LoadedLib::load());
info!("Updated egui.");
}
}
/// Recompile the anim package
///
/// Returns `false` if the compile failed.
fn compile() -> bool {
let output = Command::new("cargo")
.stderr(Stdio::inherit())
.stdout(Stdio::inherit())
.arg("build")
.arg("--package")
.arg("veloren-voxygen-egui-dyn")
.arg("--features")
.arg("veloren-voxygen-egui-dyn/be-dyn-lib")
.output()
.unwrap();
output.status.success()
}
/// Copy the lib file, so we have an `_active` copy.
///
/// We do this for all OS's although it is only strictly necessary for windows.
/// The reason we do this is to make the code easier to understand and debug.
fn copy(lib_path: &PathBuf) {
// Use the platform specific names.
let lib_compiled_path = lib_path.with_file_name(COMPILED_FILE);
let lib_output_path = lib_path.with_file_name(ACTIVE_FILE);
// Get the path to where the lib was compiled to.
debug!(?lib_compiled_path, ?lib_output_path, "Moving.");
// Copy the library file from where it is output, to where we are going to
// load it from i.e. lib_path.
std::fs::copy(&lib_compiled_path, &lib_output_path).unwrap_or_else(|err| {
panic!(
"Failed to rename dynamic library from {:?} to {:?}. {:?}",
lib_compiled_path, lib_output_path, err
)
});
}

78
voxygen/egui/src/lib.rs Normal file
View File

@ -0,0 +1,78 @@
use client::Client;
use egui_winit_platform::Platform;
use common::debug_info::DebugInfo;
#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");
#[cfg(feature = "use-dyn-lib")] pub mod dyn_lib;
#[cfg(feature = "use-dyn-lib")]
pub use dyn_lib::init;
use std::ffi::CStr;
#[cfg(feature = "use-dyn-lib")]
const MAINTAIN_EGUI_FN: &'static [u8] = b"maintain_egui_inner\0";
pub fn maintain(platform: &mut Platform,
client: &Client,
debug_info: &Option<DebugInfo>) {
#[cfg(not(feature = "use-dyn-lib"))]
{
maintain_egui_inner(platform, client, debug_info);
}
#[cfg(feature = "use-dyn-lib")]
{
let lock = dyn_lib::LIB.lock().unwrap();
let lib = &lock.as_ref().unwrap().lib;
let maintain_fn: libloading::Symbol<
fn(
&mut Platform,
&Client,
&Option<DebugInfo>,
)
> = unsafe {
//let start = std::time::Instant::now();
// Overhead of 0.5-5 us (could use hashmap to mitigate if this is an issue)
let f = lib.get(MAINTAIN_EGUI_FN);
//println!("{}", start.elapsed().as_nanos());
f
}
.unwrap_or_else(|e| {
panic!(
"Trying to use: {} but had error: {:?}",
CStr::from_bytes_with_nul(MAINTAIN_EGUI_FN)
.map(CStr::to_str)
.unwrap()
.unwrap(),
e
)
});
maintain_fn(platform, client, debug_info);
}
}
#[cfg_attr(feature = "be-dyn-lib", export_name = "maintain_egui_inner")]
pub fn maintain_egui_inner(platform: &mut Platform,
client: &Client,
debug_info: &Option<DebugInfo>) {
platform.begin_frame();
egui::Window::new("Test Window X")
.default_width(200.0)
.default_height(200.0)
.show(&platform.context(), |ui| {
ui.heading("My egui Application z");
ui.horizontal(|ui| {
ui.label(format!("Ping: {}", debug_info.as_ref().map_or(0.0, |x| x.ping_ms)));
ui.text_edit_singleline(&mut "hello".to_owned());
});
ui.add(egui::Slider::new(&mut 99, 0..=120).text("age"));
if ui.button("Click each year").clicked() {
println!("button clicked");
}
ui.label(format!("Hello '{}', age {}", "Ben", 99));
});
}

View File

@ -108,6 +108,7 @@ use std::{
time::{Duration, Instant},
};
use vek::*;
use common::debug_info::DebugInfo;
const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
const TEXT_VELORITE: Color = Color::Rgba(0.0, 0.66, 0.66, 1.0);
@ -345,23 +346,6 @@ pub struct BlockFloater {
pub timer: f32,
}
pub struct DebugInfo {
pub tps: f64,
pub frame_time: Duration,
pub ping_ms: f64,
pub coordinates: Option<comp::Pos>,
pub velocity: Option<comp::Vel>,
pub ori: Option<comp::Ori>,
pub num_chunks: u32,
pub num_lights: u32,
pub num_visible_chunks: u32,
pub num_shadow_chunks: u32,
pub num_figures: u32,
pub num_figures_visible: u32,
pub num_particles: u32,
pub num_particles_visible: u32,
}
pub struct HudInfo {
pub is_aiming: bool,
pub is_first_person: bool,

View File

@ -151,6 +151,10 @@ fn main() {
#[cfg(feature = "hot-anim")]
anim::init();
// Initialise watcher for egui hotreloading
#[cfg(feature = "hot-egui")]
voxygen_egui::init();
// Setup audio
let mut audio = match settings.audio.output {
AudioOutput::Off => AudioFrontend::no_audio(),

View File

@ -35,7 +35,7 @@ use common_net::{
use crate::{
audio::sfx::SfxEvent,
hud::{DebugInfo, Event as HudEvent, Hud, HudInfo, LootMessage, PromptDialogSettings},
hud::{Event as HudEvent, Hud, HudInfo, LootMessage, PromptDialogSettings},
key_state::KeyState,
menu::{char_selection::CharSelectionState, main::seconds_since_midnight},
render::Renderer,
@ -47,6 +47,7 @@ use crate::{
use egui_wgpu_backend::epi::App;
use hashbrown::HashMap;
use settings_change::Language::ChangeLanguage;
use common::debug_info::DebugInfo;
/// The action to perform after a tick
enum TickAction {

View File

@ -2,7 +2,7 @@ use egui_winit_platform::{Platform, PlatformDescriptor};
use crate::window::Window;
use egui::FontDefinitions;
use client::Client;
use crate::hud::DebugInfo;
use common::debug_info::DebugInfo;
pub struct EguiState {
pub platform: Platform,
@ -26,22 +26,6 @@ impl EguiState {
pub fn maintain(&mut self,
client: &Client,
debug_info: &Option<DebugInfo>) {
self.platform.begin_frame();
egui::Window::new("Test Window")
.default_width(200.0)
.default_height(200.0)
.show(&self.platform.context(), |ui| {
ui.heading("My egui Application");
ui.horizontal(|ui| {
ui.label("Your name: ");
ui.text_edit_singleline(&mut "hello".to_owned());
});
ui.add(egui::Slider::new(&mut 99, 0..=120).text("age"));
if ui.button("Click each year").clicked() {
println!("button clicked");
}
ui.label(format!("Hello '{}', age {}", "Ben", 99));
});
voxygen_egui::maintain(&mut self.platform, client, debug_info);
}
}