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 knowledege 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.ArcadeContext.gc_mode. In Arcade we normally do garbage collection of OpenGL objects once per frame by calling gc().

Warning

If you are loading an increasing amount of textures you might need to clean up the texture cache. This only caches arcade.Texture objects. See cleanup_texture_cache(). This might also involve removing them from the global texture atlas if you are using these textures on sprites.

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()
        self.sprite.draw()

        # 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.