Headless Arcade
For some applications, it may be that we want to run Arcade, but not open up a window. We might want to draw to a buffer and save an image to be used in a server or data science visualization. In remote cloud operations, we might not even have a monitor for the computer. Running Arcade this way is called headless mode.
Arcade can render in headless mode on Linux servers with EGL installed. This should work both in a desktop environment and on servers and even in virtual machines. Both software and hardware rendering should be acceptable depending on your use case.
We are leveraging the headless mode in pyglet. If you are seeking knowledge about the inner workings of headless, that’s the right place to look.
Enabling headless mode
Headless mode needs to be configured before Arcade is imported. This can be done in the following ways:
# Before Arcade is imported
import os
os.environ["ARCADE_HEADLESS"] = "True"
# The above is a shortcut for
import pyglet
pyglet.options.headless = True
This of course also means you can configure headless externally.
$ export ARCADE_HEADLESS=True
To quickly check the enviroment such as renderer and versions:
$ python -m arcade
Arcade 2.6.12
-------------
vendor: AMD
renderer: AMD Radeon(TM) Vega 11 Graphics (RAVEN, DRM 3.41.0, 5.13.0-37-generic, LLVM 12.0.0)
version: (4, 6)
python: 3.9.9 (main, Dec 20 2021, 08:19:16)
[GCC 9.3.0]
platform: linux
How is this affecting my code?
In headless mode we don’t have any window events or inputs events.
This means events like on_key_press
and on_mouse_motion
will never be called. A project not created for a headless setting
will need some tweaking.
In headless mode the arcade Window
will extend
pyglet’s headless window instead. We’ve added a property
arcade.Window.headless
(bool) that can be used to separate
headless logic.
Note that the window itself still has a framebuffer you can render
to and read pixels from. The size of this framebuffer is the size
you specify when creating the window. More framebuffers can be
created through the ArcadeContext
if needed.
Warning
If you are creating and destroying a lot of Arcade objects
you might want to look into arcade.gl.Context.gc_mode
.
In Arcade we normally do garbage collection of OpenGL objects
once per frame by calling gc()
.
Examples
There are two recommended approaches: Simple headless mode and Headless mode while extending the Arcade Window.
Simple headless mode
For simpler applications we don’t need to subclass the window.
# Configure headless before importing arcade
import os
os.environ["ARCADE_HEADLESS"] = "true"
import arcade
# Create a 100 x 100 headless window
window = arcade.open_window(100, 100)
# Draw a quick rectangle
arcade.draw_rectangle_filled(50, 50, 50, 50, color=arcade.color.AMAZON)
# Dump the framebuffer to a png
image = arcade.get_image(0, 0, *window.get_size())
image.save(f"framebuffer.png")
You are free to clear()
the window and render
new contents at any time.
Headless mode while extending the Arcade Window
For Arcade users extending the window, this method makes more sense.
The run()
method supports headless
mode and will emulate Pyglet’s event loop by calling
on_update
, on_draw
and flip()
(swap buffers)
in a loop until you close the window.
import os
os.environ["ARCADE_HEADLESS"] = "true"
import arcade
class App(arcade.Window):
def __init__(self):
super().__init__(200, 200)
self.frame = 0
self.sprite = arcade.Sprite(
":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png",
center_x=self.width / 2,
center_y=self.height / 2,
)
def on_draw(self):
self.clear()
arcade.draw_sprite(self.sprite)
# Dump the window framebuffer to disk
image = arcade.get_image(0, 0, *self.get_size())
image.save("framebuffer.png")
def on_update(self, delta_time: float):
# Close the window on the second frame
if self.frame == 2:
self.close()
self.frame += 1
App().run()
You can also split your code into arcade.View
classes
if needed. Doing it this way might make it simpler to work
with headless and non-headless mode during development. You just
need to programmatically close the window and switch views.
We can easily separate logic with the arcade.Window.headless
flag. When calling run()
we also garbage collect OpenGL
resources every frame.
Advanced
The lower level rendering API is of course still available
through arcade.Window.ctx
. It exposes methods
to create framebuffers, textures, shaders (including compute shaders)
and other higher level wrappers over OpenGL types.
When working in a multi-gpu environment you can also select a specific device id. This is 0 by default and must be set before the window is created. These device ids usually refers to a physical device (graphics card) or a virtual card/device.
# Default setting
pyglet.options.headless_device = 0
# Use the second gpu/device
pyglet.options.headless_device = 1
Issues?
If you run into issues or have questions please create an issue on github or join our discord server.