Renderer

Implemented in src/lua/luarendererapi.cpp.

By default a game never has to think about the renderer: the first call that needs a window (app_set_name, set_font, tilemap_load_map, ...) creates one with Scarab's default configuration, and a script that needs none still gets it when it finishes. renderer_create is for a game that wants to choose its own window/render settings up front.

Ordering rule — renderer_create must be the first window-needing call in the entry script. The entry script runs before any window exists, and renderer_create opens the window the moment it succeeds. Anything that needs a window before it has run creates the default renderer first, and a later renderer_create then fails with nil, "renderer already created: ..." naming the call that did it. Calls that need no window are fine before it: load_json, dofile, sound_*, sp_*, pool_*, the sprite_* handle calls, set_timer, collision_set_handler, crypto_*, pack_*, app_get_platform.

One renderer per process run. Every function here reports failure as nil, "message" — it never raises a Lua error. Handles are opaque integers.

renderer_create(options) -> renderer | nil, message

Create the process's renderer with your own settings and open its window. Call it first — see the ordering rule on this page. options is a table, and every key is optional; what you leave out keeps Scarab's default (shown below):

option meaning default
backend RENDERER_BACKEND_DEFAULT, _RAYLIB or _NULL _DEFAULT
width, height the render area, in pixels 1260, 920
title the window title "Scarab"
fps target frames per second 60
resizeable may the user resize the window true
draw_fps draw the FPS counter false
viewport { x =, y =, w =, h = } — the visible rectangle, which must fit the render area { x = 10, y = 10, w = 1240, h = 900 }
zoom the starting zoom factor (a multiple of ZOOM_STEP, within ZOOM_FACTOR_MIN..ZOOM_FACTOR_MAX) 3.8125
scroll_step { w =, h = } — how far one camera_move_* call moves { w = 1, h = 1 }
view_control_mode VIEW_CONTROL_MODE_ACTIVE or _REACTIVE _ACTIVE
use_default_key_handler sunlight's built-in scroll/zoom key bindings false
exit_key a KEY_* constant; KEY_NULL means no exit key KEY_ESCAPE
stretch_to_fill stretch the render area to fill the window false
fullscreen open the window already fullscreen (no windowed flash); ignored under --headless false
fullscreen_strategy FULLSCREEN_STRATEGY_REAL (a real OS fullscreen space) or FULLSCREEN_STRATEGY_BORDERLESS_WINDOWED; used only when fullscreen is true and accepted (and ignored) otherwise _REAL
frame_pacing FRAME_PACING_REAL_TIME or _UNLIMITED — null backend only _REAL_TIME
max_frames stop after this many frames, 0 = never — null backend only 0

The check is strict: an unknown option name, a wrong type, or an out-of-range value returns nil and the exact reason ("unknown option 'titel'", "zoom factor 3.8 is not a multiple of 0.0625 - nearest valid is 3.8125"). Whole JSON numbers loaded with load_json arrive as Lua integers, so a config file works unchanged.

Fullscreen. fullscreen = true creates the window fullscreen from the start, with exactly the result app_set_fullscreen( true, strategy ) gives afterwards (the same call, applied before the window is shown). Pick the strategy per platform — this project keeps FULLSCREEN_STRATEGY_REAL for macOS and uses FULLSCREEN_STRATEGY_BORDERLESS_WINDOWED elsewhere (see app_get_platform). app_set_fullscreen stays the way to toggle at runtime. Under --headless (the null backend) fullscreen is accepted and ignored, never an error, so one config file runs both headless and in a window; renderer_get_config then reports fullscreen = false. The render area, viewport and zoom are untouched by fullscreen.

Under --headless the null backend is forced: leave backend out (or pass RENDERER_BACKEND_NULL); asking for another one fails, and --fast/--max-frames from the command line win over frame_pacing/max_frames.

Returns an opaque renderer handle. Fails if a renderer already exists — either from an earlier renderer_create or created implicitly by an earlier window-needing call.

local config = load_json( APP_DIR .. "resources/configs/renderer.json" )
local renderer, err = renderer_create{
    title      = config.title,
    width      = config.width,
    height     = config.height,
    viewport   = { x = 10, y = 10, w = 1240, h = 900 },
    zoom       = 3.8125,
    resizeable = false,
    exit_key   = KEY_NULL,
}

if not renderer then
    error( "cannot create the renderer: " .. err )
end

renderer_destroy(renderer) -> true | nil, message

Ask the renderer to stop: the frame in progress finishes, on_update is not called again, and the process exits. The same effect as app_quit(), addressed by handle. There is one renderer per process run, so a second one cannot be created afterwards.

local renderer = renderer_get_current()
if renderer then renderer_destroy( renderer ) end

renderer_get_current() -> renderer, origin | nil

The renderer, if one exists, and how it came to exist: "created" (by renderer_create) or "lazy_default" (implicitly, by the first window-needing call — the default configuration). Returns nil if no renderer exists yet. Never creates one, so it is safe to call first — use it to check whether the ordering rule has been broken.

local renderer, origin = renderer_get_current()
assert( renderer == nil, "a window call ran before renderer_create" )

renderer_get_config(renderer) -> table | nil, message

The renderer's effective settings as a table with the same names renderer_create takes: backend, width, height, title, fps, resizeable, draw_fps, viewport ({ x, y, w, h }), zoom (the current factor), scroll_step ({ w, h }), view_control_mode, use_default_key_handler, exit_key, stretch_to_fill, fullscreen, fullscreen_strategy, frame_pacing and max_frames. It reflects the current values, not a snapshot from creation — the title after app_set_name, the zoom after zoom_in, the exit key after app_set_exit_key. Use it at boot to assert that the configuration you passed was really applied. fullscreen is the window's live state (false under --headless) and fullscreen_strategy the strategy in effect while fullscreen (FULLSCREEN_STRATEGY_REAL while windowed).

local config = renderer_get_config( renderer )
assert( config.title == "My Game" )
assert( config.zoom == 3.8125 )

renderer_get_default_view(renderer) -> view | nil, message

The renderer's default view — its own camera, viewport and zoom, the one every camera_*, zoom_* and viewport_* function acts on. Always the view id 0.

local view = renderer_get_default_view( renderer )

renderer_get_backend(renderer) -> backend | nil, message

Which backend the renderer actually runs on: RENDERER_BACKEND_RAYLIB (a real window) or RENDERER_BACKEND_NULL (headless). A RENDERER_BACKEND_DEFAULT request reports as the backend it resolved to (today, raylib) — never as _DEFAULT itself — so this is how a script tells whether it is running headless.

if renderer_get_backend( renderer ) == RENDERER_BACKEND_NULL then
    print( "running headless" )
end

renderer_get_view_count(renderer) -> count | nil, message

How many views the renderer has, the default view included — so at least 1. view_create adds one, view_destroy removes one.

print( renderer_get_view_count( renderer ) )   -- 1 until view_create is used

Renderer backends

The backend option of renderer_create, and what renderer_get_backend returns. RENDERER_BACKEND_DEFAULT means whatever this build was made with; RENDERER_BACKEND_NULL is the windowless, headless backend (see --headless).

RENDERER_BACKEND_DEFAULT, RENDERER_BACKEND_RAYLIB, RENDERER_BACKEND_NULL

View control modes

The view_control_mode option of renderer_create.

VIEW_CONTROL_MODE_ACTIVE, VIEW_CONTROL_MODE_REACTIVE

Frame pacing

The frame_pacing option of renderer_create (null backend only): FRAME_PACING_REAL_TIME runs at the target FPS; FRAME_PACING_UNLIMITED runs frames as fast as the game logic allows (the virtual clock is unchanged).

FRAME_PACING_REAL_TIME, FRAME_PACING_UNLIMITED

Zoom positions

sunlight's zoom scale has 256 positions, ZOOM_POS_MIN (0) to ZOOM_POS_MAX (255, the last valid position — never 256). The factor of position p is (p + 1) * ZOOM_STEP. Zoom is set by factor everywhere in this API; these are for converting to and from a position.

ZOOM_POS_MIN, ZOOM_POS_MAX

Zoom factors

ZOOM_STEP is 0.0625; a valid zoom factor is a whole multiple of it, between ZOOM_FACTOR_MIN (0.0625, position 0) and ZOOM_FACTOR_MAX (16.0, position 255). Because the step is a power of two, every valid factor is exactly representable — 3.8125 is valid, 3.8 is not.

ZOOM_STEP, ZOOM_FACTOR_MIN, ZOOM_FACTOR_MAX