Core & UI

In order to create a viewer project, you need to establish a pathway between the user's UI, and the server the virtual world is running on.
One of the core design decisions about the benthic project was that the UI should be completely independent from the business logic of the project, and it should be possible for developers to easily swap out UI frameworks without much difficulty. Doing it this way has created three discrete layers in the benthic project.

User Interface

The front end, which users can interact with. All this does is receive user input to send to the core, and display output it receives from the core. All interaction between the UI and the core should be done with messages as small as possible, to ensure that the least amount of logic is dependent on non-portable UI systems.

Core

The asyncronous runtime logic of the program. This handles all of the communication between the UI and the server, the parsing and manipulating of server data, and sending messages to the UI to display. The majority of the project lives here.

Server

The server that is running the upstream metaverse server code. This is the server that manages user connections, responds to requests from clients, and informs clients about state changes.

The first thing that starts running is always the UI. The UI is the entry point to the viewer, and triggers the Core to begin accepting messages. In the debug UI, that is found in the main() function in the main.rs file.

The Bevy example UI contains a plugin, which can be implemented by other bevy projects. This plugin is responsible for message handling between the core and the UI, along with receiving and rendering mesh data, and can be used by other projects by adding the plugin in the main function. The main project is expected to handle viewer state, and events raised by the plugin.

This plugin triggers the start_core() and start_listener() functions on startup.


There are now three independent async processes running after startup. The call order looks like this.
  1. start_core(), which spawns an async task where the core will run until the UI is shut down
  2. initialize(), which instantiates the core's actix mailbox, and spawns an async task for listening to UI messages that runs until the core is shut down.
  3. listen_for_ui_messages() which listens on the port specified at the UI's startup, and notifies the now-running mailbox for events from the UI.
  4. Listen_for_core_events() which listens on the port specified at the UI's startup, and notifies the UI for events from the core.

UDP Sockets

The UI and Core communicate through local UDP sockets, which send byte data back and forth between the two components. Messages sent between the UI and core are defined in messages/ui, and are seperated into UIMessage and UIResponse objects.
UIMessages are for messages coming from the core to the UI, and UIResponses are for messages coming from the UI to the core. These types are serialized into JSON bytes bytes by serde, and deserialized in the same way. This allows the decoding to be a straightforward operation of parsing JSON bytes, which can be done easily in any language with existing libraries, allowing any UI framework to use the main rust core.

Example UI to core handling flow

Listen_for_ui_messages()

The core starts up, and begins listening for messages from the UI

Send UIResponse

The UI sends a message to the core

Retrieve the UIResponse

Parse the response bytes coming from the UI, and then send that UIResponse to the mailbox

Handle the UIResponse message in the mailbox

This UIResponse was a Login, which begins the login process, sending xml-rpc messages between the core and server.

Example Core to UI message handling flow

Listen_for_core_events()

The UI starts up, listening to events from the core

Core retrieves an event

Messages are retrieved from the server by the core. Those messages are handled as actix actors for processing. If the UI should be informed of a change, that message is sent.

Mailbox send to UI

The mailbox sends UIMessages to the UI using the UDP socket

Parse bytes as UIMessage

The UI handles the bytes, and adds it to the Sender. This is a crossbeam channel, which creates the message queue for the UI to go through. Crossbeam must be used, since this is an asyncronous program interfacing with a syncronous UI framework.

Handle queue

Handle queue is registered to run every tick of the UI. This means that once a frame, the message queue is checked, and any pending messages sent from the core are processed. In this example, a ChatFromSimulator message, which adds the chat to the messages vector, and renders it.