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, messageCreate 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, messageAsk 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 | nilThe 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, messageThe 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, messageThe 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, messageWhich 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, messageHow 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
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
The view_control_mode option of renderer_create.
VIEW_CONTROL_MODE_ACTIVE, VIEW_CONTROL_MODE_REACTIVE
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
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_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