Skip to content

Technical design

Kane Rogers edited this page Feb 10, 2022 · 6 revisions

Technical architecture

Want to peek under the hood of Hotham and see how it's working? Well come on in and get your hands dirty!

Design philosophy

TL;DR 😴

In sum, Hotham aims to be laser focussed on the goal of being a high performance, easy to use game engine for standalone VR headsets.

Goals 🎯

Hotham has a couple of high level goals, or "guiding lights". Sometimes they're in alignment, sometimes they contradict one another, but they generally take the following order of precedence:

  1. Optimised for maximum possible performance on mobile VR headsets
  2. Boring, simple to reason about
  3. Easy to build simple VR applications quickly; possible to build complex things with more effort
  4. Support the Rust ecosystem and open standards
  5. Ergonomic API

Non goals 🙅🏻

In addition to these goals, Hotham also has several "non goals" - things that are not considered within the scope of the project:

  1. Device interoperability
  2. PCVR support
  3. Non-VR support
  4. Non-Rust scripting language
  5. Large, fully featured editor
  6. Exotic, unique design

High level design 🗺️

At a high level, Hotham is just glue between OpenXR, Vulkan, the Rapier physics engine and the Oddio audio library, using hecs, an Entity Component System as its "central store of truth". In another lens, Hotham is essentially a "synchronisation" system between a simulation (eg. you game or application) and "the outside world", eg the headset display, user input, speakers, etc.

With this lens in mind, main loop of a Hotham application is as follows:

  1. Collect state about input from the external (from the engine's point of view) environment, eg. user input, audio state, physics engine, etc. In Hotham we wrap up these interfaces to the external environment in Resources. A function that updates the World based on a Resource is called a system.
  2. With the external state in hand, update the state of the world World; that is, a database of Entitys representing all the live data inside the engine.
  3. Execute the "game logic", a function that takes the current, updated state of the World, runs a simulation and then emits the new, updated state of the World.
  4. Synchronise the updated World back to the external environment, eg. rendering engine, audio state, etc. by running systems that read the World and write to some Resource.

Hotham Sync Loop

It should go without saying that software is complex (game engines, doubly so) and this diagram represents a high level "idealised view" of things. There are instances where a system may read or write to multiple Resources, or a system just updates the World without any reference to an external Resource.

NOTE We currently call an interface to the external environment Resource, but they're all named <X>Context, eg. XrContext, which makes no sense. This should be fixed - please feel to suggest better names!

Tech stack 🔨

Vulkan wrapper: ash

Why?

We use ash because we believe that in order to achieve our goal of high performance and boring system design, it's easiest to use Vulkan directly. Vulkan has first class support on our target device (the Oculus Quest 2), and is a well designed, open standard.

Why not x?

Alternatives to ash would either be a safe, high level wrapper like vulkano or an even higher level api like wgpu-rs. We shy away from high level wrappers as they add extra abstraction between us and the raw hardware. This is doubly so for wgpu, which while a fantastic project, is less suited for the task of "high performance on this specific hardware".

In addition, the majority of Vulkan documentation/resources assume use of the C Vulkan API. As ash is almost a direct 1:1 mapping with the C API, it makes it much easier to take advantage of existing knowledge than having to translate this into a higher level Rust API.

ECS: hecs

Why?

The Rust ecosystem has seen a flurry of development around Entity Component Systems, APIs that help applications manage their representation of the simulation. Some great background on why the ECS architecture is so popular in Rust is outlined in the article Specs and Legion, two very different approaches to ECS by the wonderful Coraline Sherratt.

So why hecs? Well, first, it's very fast. We like that. Second, it has a small API footprint, and follows the principle of "do one thing and do it well". We also like that. It's also well maintained by the fabulous @Ralith, which we very much like.

Why not x?

The obvious contenders for hecs would be bevy-ecs or legion. Interestingly, Hotham originally used legion as it does have an (initially) simpler, more user friendly API that's easier to hit the ground running with. As a person who was not as familiar with the ECS architecture, hecs seemed a bit more intimidating and harder to get starter with.

However, legion appears to be abandoned, and its API actually introduces quite a bit of friction when working with more complex systems.

bevy-ecs is (rightly so) very tightly coupled with the bevy ecosystem, and so isn't suitable for our purposes. However it is inspired by work done on hecs, which is a strong endorsement.

VR integration: openxr

Why OpenXR?

At first glance our use of openxr may be bizarre. On the one hand, we've said "we want to be laser focussed on the hardware we support", and then on the other, we use this very high level, abstract API for interfacing with the VR headset.

The answer comes down to openxr is just a better API. It's a very well designed open standard. The specification is well defined and Facebook, the manufacturers of our target hardware, the Oculus Quest 2, are "all in on OpenXR". This makes use of openxr the obvious choice for the platform.

Finally, openxrs, the Rust wrapper around openxr is another excellent @Ralith creation.

Why not x?

Given we're interacting with very specific hardware, the only alternative would be Facebook's deprecated, proprietary libovr API. But this is a non-starter for the above reasons.

Spatialised audio: oddio

Why?

Spatialised audio is a hard requirement for any 3D game, but it's especially important for VR to provide a feeling of immersion. There aren't many good spatial audio libraries in Rust, except for oddio, which does pretty much exactly what we want. It is, again, another great @Ralith library.

Why not x?

There aren't any other alternatives for spatialised audio in Rust that I'm aware of. However, another alternative might be to wrap Oculus' proprietary Oculus Audio libraries. This is a case of conflicting goals: on the one hand we want to support the Rust ecosystem, on the other hand the Oculus Audio library is extremely advanced, and would bring a lot of benefits to users. This is a decision that will need to be revisited in future.

Maths library: nalgebra

Why?

There are a number of maths libraries available in Rust, but we've chosen nalgebra. The reasons being that it's got fairly good performance characteristics, a straightforward API, and most importantly is the maths library used by rapier.

Why not x?

Hotham initially used cgmath but rapier's hard dependency on nalgebra made it a no-brainer switch.

glam is also a well supported and widely used library, however given rapier's hard dependency on nalgebra it would be very difficult to justify having two completely separate maths libraries.

Physics engine: rapier

Why?

Physics is essentially mandatory in any 3D game; realistic, responsive physics is absolutely essential to create an immersive, believable VR experience. rapier is the most fully fledged physics engine written in pure Rust, and is the physics engine of choice for the bevy project. It has good performance characteristics and is well supported.

Why not x?

The only other viable alternative would be a Rust wrapper around NVidia's physx, but this would clash with our goal of supporting the Rust ecosystem.

Asset format: gltf

Why?

glTF is a well defined, open standard for asset interchange. This means we get to leverage widespread existing support for exporters/importers and other kinds of tooling.

We use the gltf-rs crate to do all the messy gltf parsing for us.

Why not?

Asset interchange types are never exactly what you want. Many engines have their own proprietary formats that makes it easier for them to support many interchange formats eg. FBX, OBJ, etc. However, there's a good case to be made that this really is a legacy proposition: with the advent of a robust, open standard interchange format, there's very little to gain from creating Yet Another Asset Format. However there are very real concerns about how glTF scales, particularly with large scenes or scenes with many interdependent assets. Time will tell.