cinder/platform guide
cinder/platform is the thin Mojo layer over SDL3 and the native shim's surface helpers. It loads
libcinder_native exactly once, initialises SDL, creates windows, drains SDL's event queue into
engine-level Events, opens and closes gamepads as they appear, creates the wgpu surface for a window
and, when the debug UI asks for it, forwards every raw SDL event to the ImGui backend. It sits directly
above cinder/core and below cinder/gfx; it is the only engine module with per-operating-system
branches (library file names, surface creation inside the shim). Everything above it is
platform-neutral.
Source: cinder/platform/native.mojo, events.mojo, window.mojo. Reference:
docs/api/platform.md. Tests: tests/platform/test_platform.mojo, tests/platform/test_swapchain.mojo
(both need a display server; the driver is spikes/window/run_window_spike.sh).
Key types #
| Type | Role | Ownership and lifetime |
|---|---|---|
NativeLibrary |
Owns the one loaded copy of libcinder_native (the generated CinderNative struct: library handle plus every resolved symbol) at a stable heap address. |
Construct first, destroy last. __deinit__ destroys the CinderNative and frees the allocation, which closes the library. |
NativePtr |
Pointer[CinderNative, MutUntrackedOrigin], from NativeLibrary.ptr(). How every subsystem refers to the library. |
Borrowed; valid while the NativeLibrary lives. |
default_library_path() |
CINDER_NATIVE_LIB if set, otherwise native/build/release/lib/libcinder_native.{so,dylib} relative to the repository root. |
Free function. |
Platform |
SDL initialised for video, events and gamepads; owns the SDL log queue, the open gamepads (gamepads: List[GamepadSlot]), SDL diagnostics and the forward_to_imgui flag. |
One per process, created on the main thread before any Window. __deinit__ closes gamepads, calls SDL_Quit, detaches and destroys the log queue. |
Window |
An SDL window (handle, id). Creates and destroys wgpu surfaces for itself. |
Holds platform.lib, so the Platform must outlive it. __deinit__ calls SDL_DestroyWindow. |
GamepadSlot |
SDL joystick id plus the open SDL_Gamepad handle. |
Owned by Platform.gamepads. |
Event, EventKind |
One translated platform event; a tagged record whose fields are read per kind. |
Plain copyable values. |
translate_event(ev: SDL_Event) -> Event |
The SDL union to Event translation. Unknown SDL events become EventKind.none(). |
Free function. |
Loading the library #
from cinder.platform import NativeLibrary, Platform, Window
var lib = NativeLibrary() # default_library_path(), raises if missing or ABI mismatch
var platform = Platform(lib.ptr())
var window = Window(platform, String("cinder platform test"), 640, 480)
...
keep(window)
keep(platform)
keep(lib)NativeLibrary(path) resolves every bound symbol and compares cn_native_abi_version() with the
bindings' CN_NATIVE_ABI_VERSION; a mismatch raises
libcinder_native ABI version N does not match the bindings' M. The library is loaded and checked
before the heap cell that holds it is allocated, so a raise (missing file or symbol, ABI mismatch)
unloads it and leaks nothing. The keep calls at the end of a
function are not optional: OwnedDLHandle is destroyed right after its last syntactic use, and a
subsystem that still holds a NativePtr then calls into unmapped memory (docs/MOJO_NOTES.md).
Function pointers live only inside CinderNative; nothing copies them out.
Windows and events #
Platform.poll(mut events) clears events (capacity is retained, so a warmed-up list does not
allocate), pumps SDL with SDL_PollEvent, translates each event, opens or closes gamepads on
gamepad_added / gamepad_removed, appends every event whose kind is not none(), and then drains the
shim's SDL log queue into diagnostics.
var events = List[Event]()
platform.poll(events)
for ev in events:
if ev.kind == EventKind.window_resized() and ev.window_id == window.id:
swapchain.resize(Int(ev.a), Int(ev.b)) # a, b: new pixel width and height
elif ev.kind == EventKind.key():
# ev.scancode, ev.keycode, ev.mods, ev.down, ev.repeat
...
elif ev.kind == EventKind.quit() or ev.kind == EventKind.window_close():
running = False
for record in platform.take_diagnostics():
logger.log(record.level, record.category, record.message) # category "sdl"Field meanings per kind (from the EventKind docstrings):
| Kind | Fields |
|---|---|
quit, window_close, window_minimized, window_restored, window_focus_gained, window_focus_lost |
window_id |
window_resized |
window_id, a = width, b = height in pixels (SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) |
key |
scancode, keycode, mods, down, repeat |
mouse_motion |
x, y absolute; dx, dy relative |
mouse_button |
button (1 left, 2 middle, 3 right), down, x, y |
mouse_wheel |
dx, dy scroll amounts; x, y mouse position |
gamepad_added, gamepad_removed |
device (SDL joystick id) |
gamepad_button |
device, button (an SDL_GamepadButton value), down |
gamepad_axis |
device, axis (an SDL_GamepadAxis value), value in [-1, 1] (Sint16 / 32768) |
SDL log priorities map to LogLevel: 1 and 2 to debug, 3 to info, 4 to warn, 5 and above to
error. The records carry the category sdl and no fields.
Window methods: pixel_size() (raises when SDL fails), set_size(w, h) (raises on failure, then
SDL_SyncWindow so the size change is applied before returning), set_title, show (for windows
created with hidden=True), set_relative_mouse(enabled) -> Bool. Windows are created with
SDL_WINDOW_HIGH_PIXEL_DENSITY and, by default, SDL_WINDOW_RESIZABLE; pixel_size therefore may
differ from the requested logical size on a high-DPI display.
Injecting events #
Platform.push_event(ev: SDL_Event) -> Bool pushes a raw SDL event (tests, input replay). The test
packs a variant into the 128-byte union and reads it back translated:
var key = SDL_KeyboardEvent(SDL_EVENT_KEY_DOWN, 0, 0, window.id, 1, SDL_SCANCODE_W, 119, 0, 0, True, False)
assert_true(platform.push_event(_blob(key))) # _blob copies the variant into an SDL_Event
platform.poll(events)
for ev in events:
if ev.kind == EventKind.key():
assert_equal(Int(ev.scancode), Int(SDL_SCANCODE_W))
assert_true(ev.down and not ev.repeat)Creation events (focus, shown, resized) arrive on the first poll after Window construction; the
test discards them with one poll before injecting.
Surfaces #
Window.create_surface(instance: WGPUInstance) raises -> WGPUSurface calls
cn_platform_create_surface, which picks X11, Wayland or Metal inside the shim from SDL's window
properties and fills a 256-byte error string on failure (raised as
cn_platform_create_surface failed: ...). Window.destroy_surface(surface) releases it. There are
two ways to pair a window with a device:
# 1. Gfx owns the surface: it is created with the device and destroyed in Gfx.__deinit__.
var gfx = Gfx(lib.ptr(), window)
var swapchain = Swapchain(_ptr(gfx), gfx.surface, size[0], size[1], True, True)
# 2. Headless device, surface managed by hand (tests of the platform layer).
var gfx = Gfx(lib.ptr(), WGPUSurface())
var surface = window.create_surface(gfx.instance)
...
window.destroy_surface(surface)In form 1 Gfx stores a raw pointer to the Window (surface_window) and calls destroy_surface
at teardown; the window must therefore outlive the Gfx and must not move while it exists. Requesting
an adapter with a surface makes wgpu return one compatible with presentation to that surface.
ImGui event forwarding #
Platform.forward_to_imgui is False until the debug UI (cinder/tools) sets it. While it is set,
poll hands every raw SDL_Event to cn_imgui_process_event before translating it, so ImGui sees the
same events the engine does (there is no capture test: both receive everything). The ImGui context
itself is owned by the tools layer; the platform only forwards.
Threading and error handling #
- Everything in this module runs on the main thread: SDL requires its event pump and window calls
there (
docs/THREADING.md).Platform.pollis called once per frame. - SDL's log callback runs in C and writes into a
CnEventQueueof capacity 256;polldrains it. No Mojo code runs inside the callback (ADR-0003). - Text from SDL (log lines,
last_error,video_driver, error messages) is UTF-8 and is decoded withcinder.core.cstring.string_from_c; invalid sequences become U+FFFD. - Raises (
Errorwith a message that includesSDL_GetError()where SDL has one):NativeLibraryconstruction,Platformconstruction (cn_event_queue_create failed,SDL_Init failed: ...),Windowconstruction (SDL_CreateWindow failed: ...),pixel_size,set_size,create_surface. On a failedSDL_Initthe log queue is detached and destroyed before raising. - Returns a status instead of raising:
push_event(SDL refused the event) andset_relative_mouse.set_titleandshowignore SDL's return value. - Nothing in this module aborts on its own. Memory-safety preconditions (
_as[T]bitcasts of theSDL_Eventunion) rely on the layout test incinder/ffimatching SDL's struct layouts. translate_eventis pure: unknown event types produceEventKind.none()and are dropped bypoll.
Pitfalls #
- Declare the
NativeLibrarylast in any struct that also holds objects whose destructors call into it; struct fields are destroyed in declaration order (docs/MOJO_NOTES.md). In a function,keepthe library, the platform and every window after their last use, on every return path. Windowcopiesplatform.libat construction, not a reference to thePlatform; destroying thePlatform(which callsSDL_Quit) before theWindowleavesSDL_DestroyWindowrunning after SDL has shut down.pollclears the caller's list. Keep oneList[Event]field for the frame loop rather than building a new list each frame (allocation-free after warm-up).window_resizedreports pixel size; a minimised window reportswindow_minimized, not a zero resize. The swapchain handles both (Swapchain.resize(0, 0)skips frames).- Gamepad axis values from SDL are
Sint16;-32768maps to exactly-1.0and32767to just under1.0. SDL_Eventis a 128-byte opaque union in the bindings (SDL_Event(Array[UInt8, 128](fill=0))); read variants only throughtranslate_event, which checks thetypefield first.- Surface creation inside a container works only under a display server (Xvfb or Weston headless in
CI, ADR-0007). Headless tests use
Gfx(lib.ptr(), WGPUSurface())and never touchPlatform. set_sizeon Wayland may be honoured asynchronously;SDL_SyncWindowis called so the followingpollsees thewindow_resizedevent in the test.
Related documents #
docs/ARCHITECTURE.md(module map; platform is the only per-OS module),docs/THREADING.md.- ADR-0003
docs/adr/0003-native-shim-pull-model.md: callbacks (SDL log included) are pulled from shim queues. - ADR-0007
docs/adr/0007-dev-environment-container.md: Linux container, Xvfb/Weston for windowed tests, no macOS host. - ADR-0009
docs/adr/0009-gfx-layer.md: swapchain over the surface created here. docs/MOJO_NOTES.md:OwnedDLHandlelifetime,keep, early-return hazards.- Tests:
tests/platform/test_platform.mojo(window lifecycle, resize event, surface, injected events, non-ASCII SDL log lines and window titles),tests/platform/test_swapchain.mojo(window +Gfx(lib, window)+Swapchain).