The clock
There is one clock: NativeCore.nowNs(). It is a monotonic count of nanoseconds from
std::time::Instant inside activerse_core. The frame loop, the input ring, and the
body step all use that value. GLFW is the window. It is not the timer.
One frame
GameLoop does this, in order, on the main thread:
- Poll GLFW. Mouse and key callbacks stamp samples with
nowNs()and push them into the Rust ring. - Read
nowNs()once. That reading isframeNs. - Elapsed time for this frame is
frameNs - previous, capped at 0.25 s. - Apply look in Java from the summed mouse delta. Look is still once per drawn frame.
- Call
world.prepareSimulation(). If the world publishes a character, the Rust step turns on. integrateTo(frameNs)runs every 1 ms step up to that same timestamp.world.pullSimulation()copies the body back when a step actually ran.- Gameplay ticks drain the same elapsed time at
1 / fps. Each tick callsworldAct(). - Stream meshes, draw, swap buffers. Vsync still decides when the picture is shown.
The first integrateTo only anchors the clock and returns 0. A gap longer than 250 ms is
clamped. A stall does not replay a quarter-second of old input as if it were live.
Two rates
| Rate | What runs | Clock |
|---|---|---|
| 1 ms | Player body: wish velocity, gravity, jump, voxel collision | frameNs |
fps in toml (default 60) | Actor.act, weapons, menus, saves | the same elapsed |
| Display refresh | The picture | vsync, not a second timer |
A key edge 3 ms into a 16 ms frame moves the body on that 1 ms step. The gun and the pixels still
update on the gameplay tick and the swap. Record experiment times with NativeCore.nowNs().
Do not call glfwGetTime or System.nanoTime for anything that must line up with input
or the body.
Publishing a character
The default prepareSimulation does nothing, so the Rust step stays off and
FirstPersonController.update or ThirdPersonController.update still performs one Java
step of the dt you pass. Sandbox and OptiShoot publish only while play is active: not during
loading, not while a menu is open, and not while Sandbox chat or the inventory is open.
@Override
public void prepareSimulation() {
if (!playing()) {
return;
}
player.publishToSim(getInput(), getCamera());
}
@Override
public void pullSimulation() {
if (!NativeCore.characterSteppedThisFrame() || player == null) {
return;
}
player.pullFromSim(getCamera());
}
publishToSim copies settings, bindings, yaw, and the body into Rust, then enables the sim.
characterSteppedThisFrame() stays true for that frame even when less than 1 ms elapsed and
zero steps ran, so update does not also step the body. The loop sets the sim disabled at the
start of every frame. Forgetting to publish freezes the character. Publishing during a menu lets them walk
through the UI.
What is not on this clock yet
- Look is one summed delta per drawn frame. The 1 ms steps share the yaw from that sample. They do not each get their own yaw.
- Swap time is not written down. Vsync chooses when the pixel appears. That instant is not yet a
nowNsrow. - An outside clock (a recorder, a headset, a physical device) is not converted into
nowNs. Until you write that conversion, it is a second clock. - Diagnostic memory samples in
MemoryTrackerstill useSystem.nanoTime. They are not frame time.