Hot Reloading
Mun is able to hot reload structs, as well as arrays of structs. Both apply recursively, so a struct containing a struct member field and an array of arrays can also be hot reloaded.
To understand how we might use hot reloading, let's create the skeleton for a simulation.
Start by creating a new project called buoyancy
:
mun new buoyancy
and replace the contents of src/mod.mun
with Listing 4-13.
The new_sim
function constructs a SimContext
, which maintains the simulation's state, and the sim_update
function will be called every frame to update the state of SimContext
.
As Mun doesn't natively support logging, we'll use the extern function log_f32
to log values of the f32
type.
The subject of our simulation will be buoyancy; i.e. the upward force exerted by a fluid on a (partially) immersed object that allows it to float. Currently, all our simulation does it to log the elapsed time, every frame.
Filename: src/mod.mun
extern fn log_f32(value: f32);
pub struct SimContext;
pub fn new_sim() -> SimContext {
SimContext
}
pub fn sim_update(ctx: SimContext, elapsed_secs: f32) {
log_f32(elapsed_secs);
}
To be able to run our simulation, we need to embed it in a host language. Listing 4-14 illustrates how to do this in Rust.
extern crate mun_runtime;
use mun_runtime::{Runtime, StructRef};
use std::{env, time};
extern "C" fn log_f32(value: f32) {
println!("{}", value);
}
fn main() {
let lib_dir = env::args().nth(1).expect("Expected path to a Mun library.");
// Safety: We assume that the library that is loaded is a valid munlib
let builder = Runtime::builder(lib_dir)
.insert_fn("log_f32", log_f32 as extern "C" fn(f32));
let mut runtime = unsafe { builder.finish() }
.expect("Failed to spawn Runtime");
let ctx = runtime.invoke::<StructRef, ()>("new_sim", ()).unwrap().root();
let mut previous = time::Instant::now();
const FRAME_TIME: time::Duration = time::Duration::from_millis(40);
loop {
let now = time::Instant::now();
let elapsed = now.duration_since(previous);
let elapsed_secs = if elapsed < FRAME_TIME {
std::thread::sleep(FRAME_TIME - elapsed);
FRAME_TIME.as_secs_f32()
} else {
elapsed.as_secs_f32()
};
let _: () = runtime.invoke("sim_update", (ctx.as_ref(&runtime), elapsed_secs)).unwrap();
previous = now;
unsafe { runtime.update() };
}
}
Now that we have a runnable host program, let's fire it up and see that hot reloading magic at work! First we need to start the build watcher:
mun build --watch --manifest-path=buoyancy/mun.toml
This will create the initial mod.munlib
that we can use to run our host program in Rust:
cargo run -- buoyancy/target/mod.munlib
Your console should now receive a steady steam of 0.04... lines, indicating that the simulation is indeed running at 25 Hz. Time to add some logic.
Insert Struct Fields
Our simulation will contain a spherical object with radius r and density do that is dropped from an initial height h into a body of water with density dw.
The simulation also takes the gravity, g, into account, but for the sake of simplicity we'll only consider vertical movement.
Let's add this to the SimContext
struct and update the new_sim
function accordingly, as shown in Listing 4-15.
# pub fn main() {
# new_sim();
# new_sphere();
# }
pub struct SimContext {
sphere: Sphere,
water: Water,
gravity: f32,
}
pub struct Sphere {
radius: f32,
density: f32,
height: f32,
velocity: f32,
}
pub struct Water {
density: f32,
}
pub fn new_sim() -> SimContext {
SimContext {
sphere: new_sphere(),
water: new_water(),
gravity: 9.81,
}
}
fn new_sphere() -> Sphere {
Sphere {
radius: 1.0,
density: 250.0,
height: 1.0,
velocity: 0.0,
}
}
fn new_water() -> Water {
Water {
density: 1000.0,
}
}
Runtime Struct Field Initialization
Upon successful compilation, the runtime will hot reload the new structs. Memory of newly added structs will recursively be zero initialized. This means that all fundamental types of a newly added structs and its child structs will be equal to zero.
We can verify this by replacing the log_f32(elapsed_secs)
statement with:
log_f32(ctx.gravity);
Indeed the console now receives a stream of 0
lines.
Luckily there is a trick that we can employ to still manually initialize our memory to desired values by using this behavior to our advantage.
Let's first add token: u32
to the SimContext
:
token: u32,
and set it to zero in the new_sim
function:
token: 0,
As before, the token
value will be initialized to zero when the library has been hot reloaded.
Next, we add a hot_reload_token
function that returns a non-zero u32
value, e.g. 1
:
fn hot_reload_token() -> u32 {
1
}
Finally, we add this if
statement to the sim_update
function:
if ctx.token != hot_reload_token() {
let default = new_sim();
ctx.sphere = default.sphere;
ctx.water = default.water;
ctx.gravity = default.gravity;
ctx.token = hot_reload_token();
}
This piece of code will be triggered every time the hot_reload_token
function returns a different value, but only once - allowing us to initialize the value of SimContext
.
Edit Struct Fields
Time to add the actual logic for simulating buoyancy. The formula for calculating the buoyancy force is force = submerged volume * water density * gravity.
fn calc_submerged_ratio(s: Sphere) -> f32 {
let bottom = s.height - s.radius;
let diameter = 2.0 * s.radius;
if bottom >= 0.0 {
0.0
} else if bottom <= -diameter {
1.0
} else {
-bottom / diameter
}
}
fn calc_sphere_volume(radius: f32) -> f32 {
let pi = 3.1415926535897;
let r = radius;
3.0/4.0 * pi * r * r * r
}
fn calc_buoyancy_force(s: Sphere, w: Water, gravity: f32, submerged_ratio: f32) -> f32 {
let volume = calc_sphere_volume(s.radius);
volume * submerged_ratio * w.density * gravity
}
Next we need to convert force into acceleration using acc = force / mass.
We don't readily have the sphere's mass available, but we can derive it using the sphere's volume and density: mass = volume * density.
Instead of doing this every frame, let's replace the sphere's density
field with a mass
field:
pub struct Sphere {
radius: f32,
mass: f32, // density: f32,
height: f32,
velocity: f32,
}
and pre-calculate it on construction:
fn new_sphere() -> Sphere {
let radius = 1.0;
let density = 250.0;
let volume = calc_sphere_volume(radius);
let mass = density * volume;
Sphere {
radius,
mass,
height: 1.0,
velocity: 0.0,
}
}
To initialize the sphere's mass
field, we can employ the same trick as before; this time only initializing the sphere and incrementing hot_reload_token
to 2
:
if ctx.token != hot_reload_token() {
let default = new_sphere();
ctx.sphere = default;
ctx.token = hot_reload_token();
}
Editing a field's name is only one of three ways that you can edit struct fields in Mun. In order of priority, these are the changes that the Mun Runtime is able to detect:
- If an old field and new field have the same name and type, they must have remained unchanged. In this case, the field can be moved.
- If an old field and new field have the same name, they must be the same field. In this case, we accept a type conversion and the field can potentially be moved.
- If an old field and new field have different names but the same type, the field could have been renamed. As there can be multiple candidates with the same type, we accept the renamed and potentially moved field that is closest to the original index of the old field.
Some restrictions do apply:
- A struct cannot simultaneously be renamed and its fields edited.
- A struct field cannot simultaneously be renamed and undergo a type conversion.
In both of the above cases, the difference will be recognized as two separate changes: an insertion and a deletion of the struct/field.
Remove Struct Fields
We now have all of the building blocks necessary to finish our buoyancy simulation. If the sphere is (partially) submerged, we calculate and add the buoyancy acceleration to the velocity. We also always subtract the gravitational acceleration from the velocity to ensure that the sphere drops into the water.
One important thing to take into account when running simulations is to multiply the accelerations and velocities with the elapsed time, as we are working in discrete time.
Last but not least, let's log the sphere's height to the console, so we can verify that the simulation is running correctly.
let submerged_ratio = calc_submerged_ratio(ctx.sphere);
if submerged_ratio > 0.0 {
// Accelerate using buoyancy
let buoyancy_force = calc_buoyancy_force(
ctx.sphere,
ctx.water,
ctx.gravity,
submerged_ratio
);
let buoyancy_acc = buoyancy_force / ctx.sphere.mass;
ctx.sphere.velocity += buoyancy_acc * elapsed_secs;
}
// Accelerate using gravity
ctx.sphere.velocity -= ctx.gravity * elapsed_secs;
// Apply velocity
ctx.sphere.height += ctx.sphere.velocity * elapsed_secs;
log_f32(ctx.sphere.height);
When the simulation has been hot reloaded, the console should now log height values of the ball that are indicative of a sphere bobbing on the waves.
Now that our simulation is completed, we no longer need the token
field, hot_reload_token
function, and if
statement.
The token
field can be safely removed and the simulation hot reloaded without losing any state.
Well done! You've just had your first experience of hot reloading.