Using inside pygame
FastObjects can render inside a window created by another library. The host (pygame here) owns the window, the event loop, input, and sound; FastObjects owns object insertion, update, removal, and rendering. Classic pygame drawing composites on top.
The one requirement: an OpenGL window
pygame must create its window with an OpenGL context — that is the only change to a normal pygame setup:
import pygame
import fastobjects as fo
pygame.init()
pygame.display.set_mode((1280, 720), pygame.OPENGL | pygame.DOUBLEBUF)
ext = fo.attach(view_size=(1280, 720))
fo.attach() connects FastObjects to the host's current OpenGL context and
registers it as the current window, so batches created afterwards work
exactly as in native mode. Call it once per window. If there is no GL
context, it raises a RuntimeError telling you to add the OPENGL flag.
Who owns what
| Concern | Owner |
|---|---|
Window, event loop, flip() |
pygame |
| Keyboard, mouse, sound | pygame |
| Sprite/shape state, update, render | FastObjects |
Classic 2D drawing (pygame.draw, pygame.font) |
pygame → composited via SurfaceLayer |
ExternalWindow (returned by attach) intentionally exposes only .ctx,
.width, .height, .clear(r, g, b), and .close() — no frame loop, no
input, because those belong to the host.
Compositing pygame drawing: SurfaceLayer
Draw your HUD, text, or vector art onto a normal pygame.Surface, then
composite it on the GPU alongside the batches:
hud_surface = pygame.Surface((1280, 720), pygame.SRCALPHA)
hud = fo.SurfaceLayer(hud_surface)
font = pygame.font.Font(None, 28)
# each frame:
hud_surface.fill((0, 0, 0, 0))
hud_surface.blit(font.render("score: 42", True, (255, 255, 255)), (10, 10))
hud.update() # upload the surface to the GPU (one upload)
hud.draw() # composite it (one draw call)
update() needs pygame installed (it is imported lazily, so pygame never
becomes a dependency of FastObjects itself).
Complete loop
import pygame
import fastobjects as fo
pygame.init()
pygame.display.set_mode((1280, 720), pygame.OPENGL | pygame.DOUBLEBUF)
ext = fo.attach(view_size=(1280, 720))
batch = fo.SpriteBatch("player.png", capacity=200_000)
groups = []
hud_surface = pygame.Surface((1280, 720), pygame.SRCALPHA)
hud = fo.SurfaceLayer(hud_surface)
font = pygame.font.Font(None, 28)
clock = pygame.time.Clock()
running = True
while running:
dt = clock.tick() / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
groups.append(batch.spawn(100, x=float(event.pos[0]), y=float(event.pos[1])))
elif event.type == pygame.KEYDOWN and event.key == pygame.K_d and groups:
batch.despawn(groups.pop()) # real removal, frees capacity
hud_surface.fill((0, 0, 0, 0))
hud_surface.blit(font.render(f"sprites: {batch.count}", True, (255, 255, 255)), (10, 10))
ext.clear(0.08, 0.08, 0.10)
batch.draw()
hud.update()
hud.draw()
pygame.display.flip()
pygame.quit()
The full runnable version is
examples/pygame_interop.py.
Why not blit FastObjects over classic pygame?
An OpenGL window takes over presentation — SDL's software surface no longer reaches the screen (this is true of any GPU API, Vulkan included). So the composition goes the other way: pygame draws onto a Surface and FastObjects composites it. You keep every pygame drawing tool; you just present through OpenGL.
Other hosts
fo.attach() works with any current OpenGL context, so pygame is just the
documented example. These hosts were each validated with a real window (the
spike
is in the repo, results in benchmarks/RESULTS.md):
| Host | Status |
|---|---|
| pygame (OpenGL) | ✅ Supported |
| pyglet | ✅ Supported |
| arcade | ✅ Supported |
| raylib (pyray) | ❌ Not supported |
pyglet — a pyglet window is already OpenGL, so attach just works:
import pyglet
import fastobjects as fo
win = pyglet.window.Window(900, 600)
ext = fo.attach(view_size=(900, 600))
batch = fo.SpriteBatch("player.png", capacity=10_000)
batch.spawn(1000, x=450, y=300)
while not win.has_exit:
win.switch_to()
win.dispatch_events()
ext.clear(0.1, 0.1, 0.1)
batch.draw() # FastObjects objects
win.flip()
See examples/pyglet_interop.py
(bouncing bunnies + a native pyglet.text.Label HUD).
arcade — draw the FastObjects batches inside on_draw; native arcade
drawing (arcade.Text, shapes) composites alongside them:
import arcade
import fastobjects as fo
class Demo(arcade.Window):
def __init__(self):
super().__init__(900, 600, "fastobjects + arcade")
self.ext = fo.attach(view_size=(900, 600))
self.batch = fo.SpriteBatch("player.png", capacity=10_000)
self.batch.spawn(1000, x=450, y=300)
def on_draw(self):
self.clear()
self.batch.draw() # FastObjects objects
Demo()
arcade.run()
See examples/arcade_interop.py.
raylib is not supported
attach connects to raylib's GL context (a clear works), but the
FastObjects instanced draw produces no output: raylib's rlgl layer owns
the GL state (its own matrix stack, shader, VAO, and batch system), and a
second rendering pipeline sharing the same context does not render.
Reconciling the two would require patching raylib internals, which is out
of scope. Use pygame, pyglet, or arcade as the host instead.