sdl3 — core
The core artifact: lifecycle, a window, the 2D renderer, textures and surfaces, filled geometry, events, and live input state. Everything here lives in the io.github.edadma.sdl3 package object.
import io.github.edadma.sdl3.*
The core artifact: lifecycle, a window, the 2D renderer, textures and surfaces, filled
geometry, events, and live input state. Everything here lives in the
io.github.edadma.sdl3 package object.
Lifecycle
setMainReady() // tell SDL you provide main (call before init)
val ok: Boolean = init(INIT_VIDEO)
val msg: String = error // last SDL error message
delay(16) // sleep, milliseconds
setHint(HINT_RENDER_VSYNC, "1")
quit()
init takes a bitmask of subsystem flags: INIT_TIMER, INIT_AUDIO, INIT_VIDEO,
INIT_EVENTS (default INIT_VIDEO). It returns true on success; on failure read
error.
Window
val window = createWindow("title", 640, 480) // flags default to 0
val window = createWindow("title", 640, 480, WINDOW_RESIZABLE)
Window flags: WINDOW_FULLSCREEN, WINDOW_OPENGL, WINDOW_HIDDEN, WINDOW_BORDERLESS,
WINDOW_RESIZABLE, WINDOW_HIGH_PIXEL_DENSITY. A Window is an AnyVal over the SDL
handle:
window.isNull // creation failed?
window.createRenderer() // or createRenderer("metal") to pick a driver
window.setPosition(x, y) // SDL3 has no creation-time position
window.pixelFormat // Int
window.size : (Int, Int) // logical size, in points
window.sizeInPixels : (Int, Int) // backbuffer size, in pixels (≠ size on HiDPI)
window.destroy()
A window is not high-DPI unless created with WINDOW_HIGH_PIXEL_DENSITY; otherwise
sizeInPixels == size.
Renderer
The renderer is the 2D drawing context. Coordinates are Double.
val r = window.createRenderer()
r.setVSync(true) // sync present to the display refresh
r.setDrawColor(Color(247, 103, 7)) // or setDrawColor(r, g, b, a)
r.setBlendMode(BLENDMODE_BLEND) // NONE / BLEND / ADD / MOD / MUL
r.clear() // clear to the current draw colour
r.clear(Color(24, 24, 28)) // set colour and clear
r.drawPoint(x, y)
r.drawLine(x1, y1, x2, y2)
r.drawRect(x, y, w, h) // outline
r.fillRect(x, y, w, h) // filled
r.present() // show the frame
r.destroy()
Filled geometry
Shapes SDL has no primitive for are drawn as triangle meshes via SDL_RenderGeometry:
r.fillCircle(cx, cy, radius, Color.White)
r.thickLine(x1, y1, x2, y2, width = 4.0, Color(247, 103, 7))
r.fillConvexPolygon(Array(x0, y0, x1, y1, x2, y2, /* … */), Color.White)
fillConvexPolygon takes a flat Array[Double] of x, y pairs (a triangle fan from the
first vertex). The mesh builders are pure and unit-tested.
Render targets
Draw into an off-screen texture, then blit it to the window — useful for supersampling or flicker-free compositing:
val target = r.createTexture(window.pixelFormat, TEXTUREACCESS_TARGET, w, h)
r.setTarget(target)
// … draw …
r.resetTarget()
r.copy(target) // blit the whole texture across the window
r.present()
Texture access modes: TEXTUREACCESS_STATIC, TEXTUREACCESS_STREAMING,
TEXTUREACCESS_TARGET.
Textures and surfaces
A surface is CPU pixels; a texture is GPU pixels. Upload a surface (from sdl3_ttf or sdl3_image) to a texture, then draw it:
val tex = r.createTextureFromSurface(surface)
tex.setScaleMode(SCALEMODE_LINEAR) // NEAREST / LINEAR / PIXELART
val (w, h) = tex.size
r.copy(tex) // fill the whole target
r.copy(tex, x, y) // at (x, y), the texture's own size
r.copy(tex, x, y, w, h) // into a destination rect
tex.destroy()
surface.width; surface.height
surface.free()
Events
pollEvent() returns Option[Event]; drain it each frame. An Event is a view over a
reusable union buffer, so read kind first, then only the fields valid for that kind:
var e = pollEvent()
while e.isDefined do
val ev = e.get
ev.kind match
case QUIT => running = false
case KEY_DOWN => onKey(ev.keyScancode, ev.keyRepeat)
case MOUSE_BUTTON_DOWN => onClick(ev.mouseX, ev.mouseY, ev.mouseButton)
case MOUSE_MOTION => onMove(ev.mouseX, ev.mouseY)
case MOUSE_WHEEL => onScroll(ev.wheelX, ev.wheelY)
case _ => ()
e = pollEvent()
Event kinds: QUIT, KEY_DOWN, KEY_UP, MOUSE_MOTION, MOUSE_BUTTON_DOWN,
MOUSE_BUTTON_UP, MOUSE_WHEEL. Field accessors: keyScancode, keyRepeat, mouseX,
mouseY, mouseButton (1 = left, 2 = middle, 3 = right), wheelX, wheelY (positive y
= away from the user).
Event watches
Register a callback fired for every event as it is pumped (handy for resize/expose without restructuring the loop):
val id = addEventWatch { ev => if ev.kind == QUIT then save() }
removeEventWatch(id)
Live input state
Instead of (or alongside) events, read the current device state directly:
val keys = Keyboard.state
if keys(Scancode.Space) then jump()
if keys(Scancode.Escape) then running = false
val m = Mouse.state // MouseState(buttons, x, y)
if m.left then paint(m.x, m.y)
m.middle; m.right
Scancode names the physical keys: letters A–Z, digits Num0–Num9, Return,
Escape, Backspace, Tab, Space, Minus, Equals, LeftBracket, RightBracket,
and the arrows Left, Right, Up, Down. These are the standard USB-HID scancodes
SDL reports.