As mentioned in the previous article the next task is to
implement our first proper System
, the FpsCounter
.
A relatively easy, yet important, component is some sort of FPS counter. Ideally there’ll be a bit of text in the corner showing the number of
poll()
s per second and the average duration. That way we can get a better feel for our simulator’s performance characteristics.
An FpsCounter
has two responsibilities,
- Keep track of the last
n
poll times - Calculate some statistics on the poll times and display it in the browser
Creating The System Link to heading
Instead of making the FpsCounter
part of our top-level sim
application,
it should be given its own crate. This means the FpsCounter
won’t need to care
about JavaScript, and lets us maintain distinct layers of abstraction (see
Top-Level Infrastructure - Layers for more).
First up, lets create the crate.
cargo new --lib fps-counter
cd fps-counter
cargo add ../hal
We’ll also make simplest system that compiles.
// fps-counter/src/lib.rs
#![no_std]
use aimc_hal::{Clock, System};
#[derive(Debug, Clone, PartialEq)]
pub struct FpsCounter;
impl<In, Out> System<In, Out> for FpsCounter {
fn poll(&mut self, inputs: &In, outputs: &mut Out) {
unimplemented!()
}
}
Next, we want to make sure the FpsCounter
tracks the time of the last tick.
use aimc_hal::clock::DummyClock;
#[test]
fn track_time_of_last_tick() {
let mut fps = FpsCounter::default();
let should_be = Duration::new(1, 23);
let time = DummyClock(should_be);
fps.poll(&time, &mut ());
assert_eq!(fps.last_tick, should_be);
}
The aimc_hal::clock::DummyClock
type is a helper Clock
that always
returns the same Duration
.
To make this test pass, we’ll need to update our FpsCounter
to track the last
tick. It’ll also need a source of time, we constrain the In
type using
aimc_hal::clock::HasClock
. This means input
will have a getter that yields
a Clock
.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FpsCounter {
last_tick: Duration,
}
impl<In: HasClock, Out> System<In, Out> for FpsCounter {
fn poll(&mut self, inputs: &In, outputs: &mut Out) {
self.last_tick = inputs.clock().elapsed();
}
}
The next step is to calculate the corresponding FPS and send the result somewhere.
We’ll wrap the calculated result in its own Fps
struct, then make sure the
outputs
can handle it.
#[derive(Debug, Copy, Clone, Default, PartialEq)]
pub struct Fps {
pub frequency: f32,
}
pub trait FpsSink {
fn emit_fps(&mut self, fps: Fps);
}
And the corresponding test:
#[cfg(test)]
#[macro_use]
extern crate std;
use std::prelude::v1::*;
#[derive(Debug, Default)]
pub struct Sink(Vec<Fps>);
impl FpsSink for Sink {
fn emit_fps(&mut self, fps: Fps) { self.0.push(fps); }
}
#[test]
fn record_fps() {
let start = Duration::new(42, 0);
let period = Duration::from_millis(20);
let now = start + period;
let mut fps = FpsCounter::with_start_time(start);
let mut sink = Sink::default();
let time = DummyClock(now);
fps.poll(&time, &mut sink);
assert_eq!(sink.0.len(), 1);
assert_eq!(
sink.0[0],
Fps {
frequency: 1000.0 / 20.0,
}
);
assert_eq!(fps.last_tick, now);
}
Updating FpsCounter::poll()
to make the test pass is left as an exercise for
the reader.
A further extension would be to add an average_poll_duration
field to Fps
.
That way sim::App
can record when App::poll()
starts and poll the
FpsCounter
as the last thing before App::poll()
exits. Embedded systems
won’t generally have access to an allocator, so you may want to checkout
arraydeque
as a #[no_std]
alternative.
Wiring up to the Application Link to heading
Now we’ve got an FPS counter it’s time to show the FPS in our window.
First, let’s give the FPS counter somewhere to be displayed. This means adding
a dummy element to frontend/index.html
.
<body>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
- <div>
- <span id="fps-counter"></span>
- </div>
-
<script src="./bootstrap.js"></script>
</body>
The Browser
will also need updating so it takes a reference to the
#fps-counter
span in its constructor.
// sim/src/browser.rs
use fps_counter::{Fps, FpsSink};
use wasm_bindgen::JsValue;
use web_sys::Element;
#[derive(Debug, Clone)]
pub struct Browser {
fps_div: Element,
}
impl Browser {
pub fn from_element(fps_selector: &str) -> Result<Browser, &'static str> {
let document = web_sys::window()
.ok_or("Can't get a reference to the window")?
.document()
.ok_or("Can't get a reference to the document")?;
let element = document
.query_selector(fps_selector)
.map_err(|_| "Invalid selector")?
.ok_or("Can't find the FPS element")?;
Ok(Browser { fps_div: element })
}
}
While we’re at it, we should probably implement fps_counter::FpsSink
for
Browser
…
// sim/src/browser.rs
impl FpsSink for Browser {
fn emit_fps(&mut self, fps: Fps) {
let mut buffer = ArrayString::<[u8; 128]>::default();
let result = write!(buffer, "FPS: {:.1}Hz", fps.frequency);
if result.is_ok() {
self.fps_div.set_inner_html(&buffer);
} else {
self.fps_div.set_inner_html("FPS: ? Hz");
}
}
}
We’ll need to propagate possible errors now that constructing a Browser
may
fail.
The idiomatic way to do this is by returning a Result<T, JsValue>
from a
#[wasm_bindgen]
function. That way, the shims generated by wasm-bindgen
will be notified of failure and raise the JsValue
as an exception.
// sim/src/lib.rs
#[wasm_bindgen]
pub fn setup_world(fps_div: &str) -> Result<App, JsValue> {
let browser = Browser::from_element(fps_div)?;
let inputs = Inputs::default();
Ok(App::new(inputs, browser))
}
And of course setup_world()
’s signature changed, so we’ll need to pass in the
#fps-counter
selector from our JavaScript.
// frontend/index.js
function init() {
console.log("Initializing the world");
world = wasm.setup_world("#fps-counter");
requestAnimationFrame(animate);
}
With any luck, this should be everything required to wire the FpsCounter
up
to the UI.
Reloading the window shows some rapidly changing text in the top-left corner.
FPS: 55.56Hz
The label itself isn’t overly pleasing to look at, but it gets the job done.
If the text itself seems to flicker, remember that we’re updating it every
time the App
gets polled (about 60 times per second).
As an exercise for the reader, try implementing a moving average to smooth that flicker out.
The Next Step Link to heading
Now we’ve got a better feel for the work required to add new systems to the application and wire them up to the UI, the next step is probably going to be the Communications system.
The real world tends to be messy with lots of places where errors can enter the Communications system, but lots of people have solved the problem in the past so we should be okay.