This article was pulled from here for archiving purposes.
In this wiki page, I explain how the two halves of holoocean (the client and engine) communicate.
The Two Halves of holoocean
What we call “holoocean” is actually two seperate projects.
Known as: - “python side” - “client” - but initializes the “server”
Is a pip-installable python package
User interacts with this exclusively
Mostly shuffles information to and from the engine
Responsible for initializing the engine and training scenarios
Known as: - “c++ side” - “engine”
Unreal Engine project (
Compiled binaries are downloaded and installed by holoocean package
Requires Unreal Developer account to install Unreal Editor and build/package (see https://www.unrealengine.com/)
Simple Usage Example
In this walkthrough, we are going to explain what communication between
holoocean-engine needs to happen to make this example work:
import holoocean # (1). start up the engine env = holoocean.make("SimpleUnderwater-Hovering") for i in range(10): # intitialize the level and the main agent inside of it env.reset() # prepare a command to be sent to the main agent command = [0, 0, 2, 1000] for _ in range(1000): # (2). send the command to the agent, step the simulation, and return # information from the engine state = env.step(command)
holoocean.make() function is mostly a helper function to instantiate a
.make() loads a configuration file and passes
the appropriate paramaters to the
__init__() function does three main things:
holoocean-engineprocess and tells it the minimum it needs to load
Creates HoloOceanClient instance
Creates synchronization semaphores
Provides malloc() function for allocating shared memory on the client
Sensors, agents, etc use this function
Instantiates agents, sensors, which use malloc() to allocate buffers
Creating Loading Semaphore
A “loading semaphore” is created by the client and signaled by the engine.
After starting the server process, the client will wait for the server to signal it so that the client knows the server has initialized.
Next, client will create the engine subprocess. It will pass a UUID on the engine’s command line that will be used to create unique semaphore names.
A note on UUIDs
The names for semaphores and shared memory (eg
global for all processes in the entire operating system.
To avoid collisions between different instances of holodeck,
generates a UUID for each environment it makes and sends it to the engine as
command line argument, eg
The UUID is appended to semaphore/shared memory names to allow multiple instances to run, eg
--HolodeckUUID is provided, it defaults to
This proves very useful for debugging.
Waiting for engine to load
Now that the engine is initializing itself, the client waits on the
Engine is done loading
Once the engine finishes loading, the engine will wait on another semaphore while the client does more stuff.
At this point, the client spawns agents, sensors, tasks, by sending a series of commands.
This isn’t covered in this page, but for our purposes, the important part is that each agent and sensor allocates shared memory buffers to allow communication between the engine and the client.
Main Synchronization Semaphores
At this point of the
HoloOceanEnvironment, it creates a
HolodeckClient object, which makes two important synchronization semaphores.
These semaphores allow the engine and the client to work in lock-step and
alternate back and forth (see HolodeckServer.cpp / holooceanclient.py)
Who came up with this name??
The engine waits on this semaphore while the client does whatever it wants to do
Blocks the main game loop!
The engine window will appear to be locked up while it is waiting on this semaphore
You can’t close the window, resize, or move it
The client waits on this semaphore while the engine simulates a tick
When the client is ready for the engine to simulate another tick, the client will signal
We will see how these semaphores are used below.
Now that we have a running environment, how do we get data back and forth?
We will analyze what happens for
state = env.step([0, 0, 2, 1000])
1. Action for Agent
First, we copy the provided action (
[0, 0, 2, 1000]) into the agent’s action
2. Signal Server
Next, the client signals
/HOLODECK_SEMAPHORE_SERVER and wakes the server up.
3. Client waits, server processes
4. Server samples sensor data, copies it into buffers
5. Wake up client
6. Server blocks and waits for client to signal it again
Some interesting things to note.
Data copied into shared buffer persists. If an action is written, that same action will be executed until another action is written. Same with sensor data.
The engine’s default UUID is
"". This means that if you launch the engine from the editor or Visual Studio, you can attach to it with a python client if you specify the UUID is
""when creating the