SDL and OpenGL
The standalone php-sdl-wasm package includes
SDL2, SDL_image, SDL_mixer, SDL_ttf and OpenGL shader bindings.
It supports PHP 8.0–8.5 with static, shared and dynamic library
profiles. Each versioned entry includes its matching runtime and
required native libraries. SDL PHP bindings are built into that
runtime. Neither php-sdl-wasm nor
php-wasm depends on the other package.
To see it running, open the SDL Cube demo in the embedded PHP editor, or follow Run the textured cube to use it on your own page.
Select the runtime and canvas
Create a canvas before constructing PhpSdl. This
example assumes a bundler:
<canvas id="sdl" width="640" height="480" tabindex="0"
style="image-rendering: pixelated"></canvas>import {PhpSdl} from 'php-sdl-wasm/php8.4-sdl.mjs';
const php = new PhpSdl({
canvas: document.querySelector('#sdl'),
});
php.addEventListener('output', event => console.log(...event.detail));
php.addEventListener('error', event => console.error(...event.detail));
await php.run(`<?php
var_dump(function_exists('SDL_Init'), function_exists('glCreateShader'));
`);Serve all generated package files together. The versioned
entry loads any shared codec libraries and preload data required
by its build. You do not need separate GD or zlib packages just
to start SDL. Additional PHP extensions can still be supplied
through sharedLibs.
Replace
new PhpWeb({version, variant: '_sdl', ...options})
with new PhpSdl(options) using the corresponding
versioned import. Nonempty variant options now
report a migration error. The former php-wasm-sdl
shim’s empty getLibs() API is replaced by the
dedicated runtime package.
Build options
Use the existing Make targets in the php-wasm checkout:
make sdl-mjs
# Keep only core SDL:
make sdl-mjs \
WITH_SDL_IMAGE=0 WITH_SDL_MIXER=0 \
WITH_SDL_TTF=0 WITH_OPENGL=0The same flags can be set in .php-wasm-rc
for php-wasm-builder build sdl mjs
(php-wasm-builder 0.2.0 or later). Both commands
produce packages/php-sdl-wasm with its matching
native runtime, required libraries, and preload data. In a
source checkout, SDL_OUTPUT_DIR can override the
destination; raw native outputs stay in
.cache/sdl-raw/php<version>.
| Flag | Default | Included support |
|---|---|---|
WITH_SDL |
0 normally; enabled by the SDL target |
Compiles SDL bindings into the runtime; dynamic
is a legacy alias for 1 |
WITH_SDL_IMAGE |
Follows SDL | PECL sdl_image 0.4.0 / SDL_image 2.6.0; PNG, JPEG, BMP |
WITH_SDL_MIXER |
Follows SDL | PECL sdl_mixer 0.4.0 / SDL_mixer 2.8.0; WAV, Ogg Vorbis, MP3 |
WITH_SDL_TTF |
Follows SDL | PECL sdl_ttf 0.3.0 / SDL_ttf 2.20.2; FreeType without HarfBuzz |
WITH_OPENGL |
Follows SDL | PECL opengl 0.9.0 with PHP 8 browser shader bindings |
Add-on flags accept 0 or 1 and
require SDL. SDL_image requires enabled WITH_LIBPNG
and WITH_LIBJPEG; SDL_ttf requires
WITH_FREETYPE. Select static or
shared for those codec libraries using the existing
build flags. WITH_ZLIB=0 disables the PHP extension
while retaining the native zlib archive needed by image/font
decoding. The build reuses these codec providers without adding
duplicate Emscripten ports. MP3 uses SDL_mixer’s bundled
minimp3 decoder, without another shared library.
FLAC, MIDI, tracker decoders, and HarfBuzz are not enabled by
this profile.
The internal _sdl filename/configuration suffix
separates native outputs and configure caches. JavaScript
consumers select SDL through the PhpSdl import.
PHP configure caches are separated by full PHP version and
effective configure arguments under
.cache/php-configure/. Changing add-on flags
reconfigures PHP; repeating the same settings preserves the
configuration timestamp. make php-clean removes the
selected PHP version’s caches, and make clean
removes all configure caches. Switching PHP versions does not
require editing cached iconv results.
The package’s browser integration files in js/
are Emscripten link inputs. Editing one relinks the selected
runtime without rerunning PHP configure or recompiling unchanged
C sources. Use make sdl-mjs to refresh the package
and keep all its generated files together.
After building, verify incremental behavior from the checkout with:
ENV_FILE=.github/.env_8.4.static.ci node .github/bin/verify-sdl-incremental.mjs 8.4Use the same ENV_FILE as the build. The verifier
requires an unchanged build to preserve both native objects and
output timestamps, then uses Make’s -W option to
simulate an updated JS library and require a relink with no C
changes. CI runs this check in the PHP 8.4 static job.
After a local native rebuild, restart Vite with
--force and reload the demo. Vite can retain the
previous generated JavaScript while serving the new Wasm; both
files must come from the same build.
Input, textures, fonts and timing
php-sdl-wasm includes the bindings below.
| Area | Added support |
|---|---|
| Events | Mouse-up, precise wheel, relative motion, key state/repeat/scancode/modifiers, text/composition payloads, touch, joystick hats/balls and hotplug, controller axes/buttons/hotplug/touchpads, audio/display/drop payloads |
| Textures | Packed updates, streaming locks, color/alpha/blend/scale
settings, checked readback, and
SDL_SetRenderTarget($renderer, null) |
| Fonts | UTF-8 Solid/Blended/Shaded rendering and wrapping; text/glyph metrics; bold/italic/underline/strikeout, outline, hinting, kerning and size |
| Controllers | Standard axes/buttons, direct joystick button/hat polling, attachment and instance IDs, mappings and explicit updates |
| OpenGL | Float/int scalar and vector-array uniforms (1–4 components), square 2/3/4 matrices and WebGL2 rectangular matrices |
| Timing | SDL_GetTicks, SDL_GetTicks64,
SDL_GetPerformanceCounter,
SDL_GetPerformanceFrequency |
Events retain SDL’s field names:
$event->wheel->preciseY,
$event->key->repeat,
$event->text->text,
$event->tfinger->fingerId and
$event->cdevice->which. Device-added events
use a device index; removed/input events use an instance ID.
Empty polling leaves the previous event unchanged.
SDL_PushEvent() accepts typed input payloads;
pointer-bearing events such as file/text drops and extended IME
strings cannot be pushed from PHP. SDL_PumpEvents()
explicitly samples browser input.
Browser keyboard events reach SDL only while its canvas or owned IME field has focus. Other page controls retain normal typing, shortcuts and navigation; leaving the SDL input target releases held keys and modifiers so they cannot stick when focus moves into an editor. Returning to the canvas resumes input.
After initializing SDL video, call
SDL_StartTextInput() when entering a text field and
SDL_StopTextInput() when leaving it. A request made
before window creation activates browser editing when the window
is created; stop also cancels a pending request. Committed
Unicode arrives in $event->text->text;
in-progress composition arrives in
$event->edit->text with selection
start/length measured in Unicode
codepoints. Long commits are split into native SDL events
without splitting a UTF-8 character; native event-size limits
and filtering still apply. Physical key events remain available
separately. Ordinary keypress input works before an explicit
start; SDL’s implicit video-initialization call does not open a
browser editing surface.
Browsers with EditContext use the supplied
canvas as the editing surface, including in fullscreen. Other
browsers use an owned textarea beside the canvas. The textarea
cannot receive IME input while the canvas itself is fullscreen:
SDL_StartTextInput() still has its native void
return, and SDL_GetError() reports that fullscreen
IME requires EditContext. Ordinary keypresses
remain available, and the textarea can resume after fullscreen
exits. Start text input from a user gesture where the browser
requires one for its on-screen keyboard; SDL’s native
screen-keyboard queries do not report browser keyboard
visibility.
SDL_SetTextInputRect() positions the
candidate-window hint in SDL window coordinates, accounting for
canvas CSS scaling and shadow roots. It supplies one rectangle,
not per-character glyph layout. Stopping input cancels
unfinished composition and returns owned focus to the canvas
without taking focus from another control. Window destruction,
video shutdown and PHP refresh remove the owned editing surface
and its callbacks. Browser transport tests do not establish
physical OS IME coverage or complex-script font shaping.
Fullscreen uses the supplied canvas, including custom IDs and
shadow roots, and restores its dimensions and inline styles on
exit. SDL_SetWindowFullscreen($window, $flags)
returns -1 with an SDL error when fullscreen is
unavailable or blocked by browser policy. A 0
result accepts the request; it can still wait for a user
gesture. Observe the browser’s
fullscreenchange/fullscreenerror
events when actual browser entry matters. Destroying the window,
quitting video or refreshing PHP cancels deferred work and
removes resize callbacks before their native window data is
freed.
SDL_GetRelativeMouseMode() reports the requested
mode. The browser can exit pointer lock while that mode remains
enabled, allowing SDL to request it again on a later click. Use
document.pointerLockElement and browser
pointerlockchange/pointerlockerror
events for actual lock state. A missing pointer-lock API or
focused SDL window fails immediately with -1 and
SDL_GetError(). Browser requests accepted for
processing still return 0; an asynchronous denial sets
SDL_GetError() without an unhandled Promise
rejection. Disabling relative mode cancels queued requests.
Window/video teardown and PHP refresh release the SDL canvas
lock while preserving another element’s lock. Retired request
completions cannot set a replacement window’s error or undo its
requested mode. For a canvas inside a shadow root, read that
root’s pointerLockElement to determine the actual
locked canvas.
Browser window blur releases held keys and modifiers and sends SDL focus events. Touch positions/deltas are normalized to the canvas; the pinned backend reports pressure as 1. Reconnecting a gamepad at the same browser index gives it a new SDL instance ID; retained handles for the old instance remain detached.
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) !== 0) {
throw new RuntimeException(SDL_GetError());
}
$controller = SDL_GameControllerOpen(0);
if ($controller && SDL_GameControllerGetAttached($controller)) {
SDL_GameControllerUpdate();
$horizontal = SDL_GameControllerGetAxis($controller, SDL_CONTROLLER_AXIS_LEFTX);
$pressed = SDL_GameControllerGetButton($controller, SDL_CONTROLLER_BUTTON_A);
}
if ($controller) { SDL_GameControllerClose($controller); }
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);The browser controls gamepad visibility, often requiring a
button press first. Keep the controller open and call
SDL_GameControllerUpdate() each frame when polling
continuously. SDL_GameControllerAddMapping()
changes SDL’s mapping database. When creating a device-specific
entry from the browser’s default mapping, close and reopen the
controller once to select that entry. Subsequent edits to that
same entry update its open controller handle. Browser gamepads
expose their hats as buttons; SDL_JoystickNumHats()
is zero on this backend. Opening an unavailable device returns
null. Closed handles raise an Error on use; closing twice is
harmless. SDL_GameControllerGetJoystick() acquires
a separate reference, so closing either handle leaves the other
usable. Close that joystick too. Request shutdown and
SDL_Quit() release remaining handles.
SDL_UpdateTexture($texture, $rect, $pixels, $pitch)
accepts packed bytes or a live SDL_Pixels buffer,
including a surface view, with checked rectangle bounds, pitch
and byte length. Lock an RLE surface before accessing or
uploading its pixels.
SDL_CreateTextureFromSurface() remains available
when SDL should handle the surface format conversion. Planar
YUV/indexed formats are unsupported by this upload path.
SDL_RenderReadPixels($renderer, $rect, $format)
returns tightly packed bytes, or null on a native failure.
SDL_LockTexture($texture, $rect, $pixels, $pitch)
fills its last two arguments by reference and returns SDL’s
status code. A successful lock returns a zero-initialized,
PHP-owned SDL_Pixels buffer, accessed by byte
offset: $pixels[$y * $pitch + $x * 4] = 255 writes
red in an ABGR8888 texture on Wasm. Fill every pixel in the
locked region, then call
SDL_UnlockTexture($texture). The returned pitch
belongs to the PHP staging buffer. Unlock uploads once;
subsequent changes to a retained buffer do not affect the
texture. The buffer does not expose existing texture contents or
a native pointer.
Destroying a locked texture discards pending writes.
Destroying a renderer or window invalidates its textures,
including IMG_LoadTexture() results. Invalid
resources raise errors instead of passing stale pointers to SDL.
Retained pixel buffers remain valid after unlock or destruction.
Keep the window and renderer handles alive while drawing;
dropping their last PHP reference also releases their native
resources.
Canvas keyboard focus
Browser key forwarding requires focus on the supplied canvas or its owned IME transport. Other editors receive normal typing and shortcuts. Leaving that target releases native held keys and modifiers after DOM focus settles, without interrupting a move between the canvas and its IME field.
Surface and pixel lifetimes
$surface->pixels,
$surface->format and
$format->palette retain their PHP owner.
Dropping the original variable leaves a retained view usable.
Explicit SDL_FreeSurface() invalidates all its
views; window resize/destruction and video shutdown invalidate
wrappers for native window surfaces. Fetch the new window
surface after resizing. Freeing a borrowed format, palette or
window surface directly raises an Error; release its owner
instead.
Pixel and palette reads, writes, metadata and native calls
check the owner. Replacing a format’s palette invalidates its
earlier palette views. RLE pixel access requires a successful
SDL_LockSurface() and ends at
SDL_UnlockSurface(); views refresh their storage
address on each access. View/owner cycles participate in PHP
garbage collection. Surface, format, palette and pixel wrappers
cannot be cloned, serialized or reconstructed in place. Explicit
frees of owned surfaces, formats and palettes are
idempotent.
new SDL_Pixels($pitch, $height) allocates zeroed
storage and rounds pitch up to four-byte alignment. Dimensions
must be positive, and the aligned allocation must fit signed
int32. SDL_ConvertPixels() validates both buffers,
packed formats and row pitches, writes the destination, and
supports overlapping source/destination storage through a
temporary source copy. Indexed and planar formats need a
different conversion path.
Blit output rectangles retain their object identity. Lower blits require positive rectangles entirely inside each surface; unscaled lower blits also require matching dimensions because these native calls bypass normal clipping. Rectangle/color array counts must fit the input; zero selects all elements, and indices must start at zero without gaps. These blit and renderer calls snapshot shape fields before native pointer lookup so property callbacks cannot leave stale pointers.
Texture-lock outputs respect typed references. If an output destructor destroys or changes the lock, the call raises a catchable Error. Cleanup releases only that call’s lock, preserving a newer lock created by the callback.
TTF_*UTF8* functions accept UTF-8; the original
TTF_*Text* names retain Latin-1 behavior. Glyph
functions take numeric Unicode code points; use their
32 variants for supplementary characters. Metrics
fill output arguments by reference and return SDL’s status code.
A wrap length of zero wraps only at newlines. Font
styles/outlines are applied natively. HarfBuzz remains disabled,
so these bindings do not add complex-script shaping.
Unsigned timers and 64-bit touch IDs return integers when
they fit PHP’s integer range, otherwise exact decimal strings.
PHP-Wasm has 32-bit PHP integers; cast to float for ordinary
elapsed-time calculations and retain strings for exact
counter/identifier values. Counter units come from
SDL_GetPerformanceFrequency(); ticks are
milliseconds.
Desktop window management, threads, haptics, sensors and additional codecs remain outside this build. The browser/Emscripten backend still determines which native features can produce events.
SDL geometry and renderer state
SDL_RenderGeometry($renderer, $texture, $vertices, $indices = null)
submits colored or textured triangles through SDL’s renderer.
Each vertex is a tuple
[x, y, red, green, blue, alpha, u, v]; colors are
integers from 0 to 255 and positions/UVs are finite numbers.
Counts come from the arrays. A null texture draws vertex colors,
and null indices draw sequential triangles:
$vertices = [
[0, 0, 255, 255, 255, 255, 0, 0],
[64, 0, 255, 255, 255, 255, 1, 0],
[64, 64, 255, 255, 255, 255, 1, 1],
[0, 64, 255, 255, 255, 255, 0, 1],
];
if (SDL_RenderGeometry($renderer, $texture, $vertices, [0, 1, 2, 0, 2, 3]) !== 0) {
throw new RuntimeException(SDL_GetError());
}SDL_RenderGeometryRaw() accepts PHP strings
containing packed float32 XY/UV pairs and RGBA bytes, with
explicit byte strides and vertex/index counts. On Wasm, use
pack('g*', ...) for coordinates and
pack('C*', ...), pack('v*', ...) or
pack('V*', ...) for unsigned 1/2/4-byte indices. A
zero stride repeats one element; buffers may include padding. UV
data may be null when the texture is null. Short buffers,
incomplete triangles, invalid indices, misaligned coordinate
strides and nonfinite coordinates raise exceptions before
drawing. Neither entrypoint accepts native heap addresses.
Geometry uses vertex color/alpha modulation; SDL’s texture
color/alpha modifiers are ignored, as in native SDL. Texture or
renderer blend mode controls blending.
SDL_RenderDrawPoints,
SDL_RenderDrawLines,
SDL_RenderDrawRects and
SDL_RenderFillRects accept arrays of
SDL_Point or SDL_Rect objects. Their
F variants accept SDL_FPoint or
SDL_FRect. Empty batches succeed without drawing.
Inputs remain unchanged, and callbacks from subclass property
getters cannot leave the batch using a destroyed renderer.
Viewport, clip rectangle, logical size, integer scale and
explicit scale have the native SDL setter/getter names. Pass
null to reset a viewport or disable clipping. Getters fill
output arguments by reference. Renderer/driver info queries
return arrays with name, flags,
num_texture_formats, texture_formats,
max_texture_width and
max_texture_height; query support before selecting
a rendering path. Draw-color/blend queries and
SDL_RenderTargetSupported() are also available.
Status-returning functions retain SDL’s 0/-1 contract; inspect
SDL_GetError() on failure. See the
packages/php-sdl-wasm/core/php_sdl_geometry.stub.php
in the source checkout for exact argument and return types.
SDL_RenderWindowToLogical($renderer, $windowX, $windowY, &$logicalX, &$logicalY)
returns floating-point logical coordinates through its two
outputs.
SDL_RenderLogicalToWindow($renderer, $logicalX, $logicalY, &$windowX, &$windowY)
returns window integers, truncating toward zero as SDL does.
Both functions return void and use the renderer’s current
viewport, scale, logical resolution and target state. Positions
outside a letterboxed scene can produce negative logical
coordinates; they are not clamped to the scene. SDL already
adjusts mouse events when logical sizing is enabled; convert
positions that are still in window space, rather than applying
the transform to those events again.
Window inputs must fit int32 and logical inputs must fit
finite float32. Nonfinite results, overflowing intermediate
products and transforms too close to the int32 limits for safe
float32 conversion raise ValueError before outputs
change. The reverse conversion conservatively rejects transforms
whose inverse cannot establish safe bounds, including zero
scale. Output assignment respects typed references and stops at
the first exception. A destructor may close the renderer while
an output is replaced; both coordinates have already been
captured before any such callback.
Graphics APIs and cleanup
Create an SDL OpenGL ES 3 context before calling the GL
bindings. The browser API supports shaders, programs, uniforms,
buffers, vertex arrays, instancing, integer attributes, uniform
blocks/reflection, textures, depth/stencil and multisample
renderbuffers, multiple framebuffer outputs, and pixel readback.
Desktop immediate-mode functions such as glBegin()
are not provided. Use
packages/php-sdl-wasm/opengl/php_webgl.stub.php and
the buffer, texture, and matrix rules in
packages/php-sdl-wasm/README.md from the same
checkout when porting desktop code. Invalid sizes and offsets
raise exceptions before native memory access; GL driver errors
are available through glGetError().
Instanced draws use the same checked buffer-relative offsets
as ordinary draws. Integer attributes retain their integer
values. Uniform-buffer range offsets must satisfy
GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and fit the
allocated buffer; use the reflected block size, offsets and
array/matrix strides to construct its bytes. Unsigned
ui/uiv uniforms accept exact decimal
strings up to 4294967295, including values beyond
PHP-Wasm’s signed integer range. Missing uniform/block names
return GL_INVALID_INDEX (-1).
glGetIntegerv(), glGetFloatv() and
glGetBooleanv() return scalar or array values
according to the selector. A viewport produces four integers; a
color write mask produces four booleans. Unknown selectors raise
an exception. Query the context’s extensions and supported
sample counts before choosing formats; constant availability
does not establish backend support. Multisample color must be
resolved with glBlitFramebuffer() before sampling
or readback. Depth/stencil blits require
GL_NEAREST.
glTexStorage2D()/glTexStorage3D()
allocate immutable mip storage for 2D, cube, array and volume
textures. The 3D image/subimage calls take packed bytes. Pixel
transfers check alignment, row lengths and skipped rows/pixels;
3D uploads also account for image height and skipped images.
Required lengths include prefixes and padding. Surface uploads
reset and restore unpack layout, while readback zeroes unused
bytes. These byte-string APIs reject bound pixel buffers. Null
image allocation has no source layout; float32 depth/stencil
accepts only that allocation form in WebGL.
Compressed image/subimage calls take an explicit
imageSize, checked against the format’s block
geometry and supplied bytes. Query
glGetIntegerv(GL_COMPRESSED_TEXTURE_FORMATS) before
choosing a format; unsupported formats raise
ValueError. The binding checks S3TC, ETC1,
ETC2/EAC, RGTC, BPTC, 2D ASTC and PVRTC1 layouts. The native
browser still validates target/mip/block-offset restrictions.
WebGL2 array support does not imply support for compressed
volume textures.
Texture and sampler parameter queries return scalar integers or floats. Sampler generation, binding, integer/float setters and deletion let a texture unit override its texture’s sampling state. Samplers follow the same context ownership and checked output-reference behavior as the other GL objects.
New WebGL2 operations reject a WebGL1 context with a catchable PHP error.
Image, font, and audio loaders return null on
native load failures. Check SDL_GetError(). Free
surfaces with SDL_FreeSurface(), close fonts with
TTF_CloseFont(), and release audio with
Mix_FreeChunk() / Mix_FreeMusic().
Freed audio/font objects cannot be reused. Release GL resources
when they are no longer needed; context teardown also deletes
remaining PHP-created buffers, textures, samplers, vertex
arrays, framebuffers, renderbuffers, shaders and programs.
Keep the owning object returned by
SDL_GL_CreateContext(),
$window->GL_CreateContext() or
new SDL_GLContext($window) while using it. Dropping
the last reference deletes the native context.
SDL_GL_GetCurrentContext() returns a borrowed
alias. Deleting a context invalidates every alias, and repeated
deletion is harmless. Using a destroyed context, cloning it or
serializing it raises a catchable exception.
SDL_GL_MakeCurrent(null, null) unbinds a live
context without deleting it.
The pinned browser backend supports one live SDL GL context
per PHP runtime. Close it before creating another context or
renderer, including when the old context is unbound. A renderer
owns its GL context; destroy the renderer to release it. Window
destruction, video reinitialization, the last video subsystem
quit and SDL_Quit() invalidate related aliases and
release GL allocations. Earlier subsystem quits preserve the
context if video still has another reference. Browser context
loss invalidates GPU resources. After
webglcontextrestored, recreate them and reapply
render state; deleting the old names is harmless. The binding
resets cached SDK bindings and restores automatically enabled
extensions. It does not retain or reload your assets. GL
generation functions also clean up after typed-output errors or
destructor callbacks that destroy the context during output
assignment. Deleted shader and program names raise
ValueError in operations that require a live
object.
After halting playback and freeing music/chunks, call
Mix_CloseAudio() and Mix_Quit() to
close the device and unload initialized decoders. The cube also
performs this cleanup when audio setup fails, before offering a
retry.
With this package’s implicit SDL Asyncify sleeps disabled,
SDL_Delay() blocks the browser; it does not yield a
game loop. A PHP loop can explicitly await a JavaScript frame
Promise through vrzno_await(). Schedule frames with
the browser’s requestAnimationFrame. SDL buffer
swaps stay synchronous inside Vrzno animation callbacks.
Register idempotent cleanup in
vrzno_env('onRefresh') to cancel frames, remove
listeners, stop audio, and release native resources before PHP
request memory resets. The cube also cleans up on rerun, errors,
and page exit. Use a fresh canvas when replacing a runtime or
switching graphics context types.
Resource ownership and lifetimes
PHP objects own their native SDL resources. The rules below describe when those resources are released, which aliases stay valid, and which errors are catchable.
RWops and fonts
RWops created by SDL_RWFromFile(),
SDL_RWFromConstMem(), SDL_RWFromMem()
or SDL_RWFromFP() own their wrapper storage.
Close() and Free() release it once;
later I/O raises a catchable Error. Raw
new SDL_RWops and SDL_AllocRW()
objects can be freed safely, but cannot perform I/O without
callbacks. Native wrappers cannot be cloned, serialized or
reinitialized.
SDL_RWFromMem(&$buffer, $capacity) requires
a string and positive capacity. It preserves the existing
prefix, pads with zero bytes or truncates to capacity, and
updates that referenced string after writes. Ordinary string
copies remain unchanged. Keep the referenced value a string of
the same length while using the stream; edits to its bytes
become visible on the next RWops operation.
SDL_RWFromConstMem() owns a read-only copy.
Capacities and transfer lengths must fit int32; invalid ranges
raise ValueError instead of silently truncating.
SDL_RWFromFP($stream, $autoclose = false)
retains a PHP stream resource and supports file,
php://memory, php://temp and custom
PHP streams. Closing the RWops closes the underlying resource
only with autoclose=true; an external
fclose() invalidates subsequent I/O. Stream
callbacks cannot recursively use or close the active RWops. PHP
callback exceptions propagate to the caller. Close custom
streams explicitly when their wrapper objects form resource
cycles.
Read(&$output, $bytes) and
Write($input, $bytes) transfer bytes. Their
three-argument method forms take object size and count; explicit
zero counts perform no I/O. Reads replace the output with the
bytes read, including an empty string at EOF, and respect typed
references. Reads/writes return object counts. Sizes, positions
and unsigned endian reads use exact decimal strings when their
values exceed PHP’s integer range. BMP loaders honor
freesrc/freedst by closing the wrapper
and invalidating aliases; saved pixels are snapshotted before a
custom stream callback can destroy their source surface.
Fonts release their native storage on
TTF_CloseFont(), last-reference cleanup, or the
final TTF_Quit(). If SDL_ttf was initialized more
than once, intermediate quit calls leave fonts usable. Font
cloning and serialization are rejected. Color getters may
execute PHP, so text rendering rechecks font liveness after
reading them. Metrics stop assigning output parameters on the
first exception. TTF_SetFontSize($font, $size)
accepts sizes 1–4096 and updates later metrics and rendered
glyphs. Family/style queries return nullable strings, face
counts return integers, and
TTF_FontFaceIsFixedWidth() returns a native integer
flag: test it for nonzero rather than comparing it with
1.
Cursors and mouse queries
Initialize SDL video before selecting a cursor.
SDL_Cursor owns its native cursor.
SDL_GetCursor() returns the existing wrapper, so an
alias keeps the owner alive. Explicit Free()
invalidates every alias; later selection raises Error and
repeated frees are harmless. The SDL default cursor remains
library-owned, and freeing its wrapper is a no-op. Final video
shutdown or video reinitialization invalidates all cursor
wrappers; an intermediate balanced subsystem quit leaves them
usable.
Bitmap cursor data and masks must have exactly
width / 8 * height bytes. Dimensions must be
positive, width must be divisible by eight, the expanded pixel
buffer must fit SDL’s signed allocation limit, and the hotspot
must be inside the image. Color cursors require a live surface
and an in-bounds hotspot. Invalid sizes, hotspots and system IDs
raise ValueError. Native creation errors return null from
factories or throw from the constructor. Cursors cannot be
cloned, serialized or reinitialized, including after an explicit
free.
SDL_SetCursor(null) redraws the current cursor.
SDL_ShowCursor() accepts SDL_QUERY
(-1), SDL_DISABLE (0) and SDL_ENABLE
(1), and returns an integer. A query leaves visibility
unchanged. For a change, the pinned SDL implementation returns
the previous visibility. Mouse state outputs
honor typed references and stop at the first exception.
SDL’s default event target follows the canvas supplied to
PhpSdl, including canvases with custom IDs and
those inside a shadow root. Its DOM ID is preserved; another
element named canvas does not redirect mouse
callbacks.
The browser backend displays cursors through the canvas CSS.
It does not support mouse warping:
SDL_WarpMouseInWindow()/$window->WarpMouse()
leave that native limitation visible through
SDL_GetError(). Pointer lock and relative motion
still depend on browser gestures and focus; the cursor fixes do
not add automatic permission or gesture handling.
Mixer ownership and audio restarts
Open the mixer before loading audio. Unused
Mix_Chunk and Mix_Music objects
release native allocations when PHP drops the last reference.
Each allocated channel retains its associated chunk, including
paused playback and the last completed chunk returned by
Mix_GetChunk(). Replacing a channel, shrinking the
allocation, freeing its chunk or closing audio releases that
association. A freed chunk is never recreated from a stale
native pointer. Music stays alive during playback; completion
releases the playback reference at the next PHP mixer call.
Native audio callbacks do not run PHP.
Mix_FreeMusic() cancels that track’s playback
immediately, including a fade. Starting another track also
cancels an outstanding fade before replacing it. These
operations return while Web Audio is suspended. Freeing a
different track leaves the playing track alone. To hear a
complete fade, wait for Mix_PlayingMusic() to
return zero before freeing or replacing it. Pausing, resuming
and browser audio permission remain explicit application
decisions.
Matching Mix_OpenAudio() calls increment the
native open count; each Mix_CloseAudio() releases
one reference. The final close stops playback and invalidates
loaded chunks and music. A changed audio format, final SDL audio
subsystem shutdown, direct native audio reinitialization or PHP
refresh() also closes the mixer and invalidates its
objects. Reload assets after reopening it.
Mix_Quit() releases music before unloading
decoders; decoded chunk playback can continue.
Mix_OpenAudioDevice() accepts null for
the default device. Native driver failures return their ordinary
error value with Mix_GetError() details.
Zero-valued frequency, format, channel and buffer-size arguments
retain SDL_mixer’s native default selection. Negative volume and
channel-count query values retain their native query
behavior.
Closing an already closed mixer or halting music without an
open device is harmless and preserves the existing SDL error.
Final close clears the decoder’s old audio format. Calling
Mix_Init() while closed loads codec support without
opening audio or activating decoder lists;
Mix_QuerySpec() stays zero until another successful
open. PHP refresh after explicit audio/SDL shutdown leaves no
counted SDL allocations in the tested paths. Use
Mix_OpenAudioDevice() with
allowedChanges set to 0 when an exact
sample rate/channel layout is required;
Mix_OpenAudio() permits native frequency/channel
negotiation.
Channel operations check allocated indices and documented
special values. Invalid indices and narrowing/length errors
raise catchable exceptions; operations requiring an open device
raise an Error after it closes. Mix_Playing(-1),
Mix_Paused(-1) and
Mix_AllocateChannels(-1) return zero when closed,
and Mix_GetChunk() returns null. Channel allocation
rejects native size overflow; if the allocator returns failure,
the previous table is preserved. Audio wrappers reject cloning
and serialization.
Both RWops loaders snapshot remaining seekable input before
entering a decoder, honor freesrc, preserve PHP
callback exceptions and recheck audio state after callbacks. WAV
decoding releases the snapshot immediately; streamed music keeps
it until freed. Query outputs honor PHP types and stop after an
exception.
Window ownership and checked outputs
Window getters such as SDL_GL_GetCurrentWindow()
reuse the owning PHP SDL_Window, preserving
subclasses and keeping the native window alive while an alias
remains. Destroying it invalidates its aliases, renderer,
textures, GL contexts and surface views. Repeated
SDL_DestroyWindow() calls are harmless; native
queries and operations on a destroyed or uninitialized window
raise an Error. Window, joystick and controller handles reject
cloning and serialization. The PHP 8.0 subclass magic-method
audit and its fixes are recorded below.
A live window cannot be constructed again. If title coercion
calls the constructor recursively, the outer call fails and
preserves the inner window. A destroyed wrapper may be
initialized again. Native window-creation failures return
null from SDL_CreateWindow() and throw
from the constructor, with SDL’s error message. Native dimension
clamping is preserved. Embedded NUL characters in titles raise a
ValueError instead of silently truncating the text.
Position, size, display-mode and gamma outputs respect PHP
reference types and stop at the first exception. Size/position
outputs are optional, matching their existing signatures.
Property enumeration captures native fields before any PHP
property destructor runs; its title key is the ordinary
title. Existing typed aliases remain valid if an
assignment is rejected. Display-mode setting accepts
null for SDL’s default mode and checks the window
again after reading PHP mode properties. Unsupported
shaped-window operations retain SDL’s native failure result.
SDL_UpdateWindowSurfaceRects($window, $rectangles, $count)
checks the count before allocating and reads only that many
rectangles. Omit the count to use the complete array; an
explicit zero performs no update. Negative counts, counts larger
than the array and invalid rectangle values raise exceptions.
The input list is retained across PHP getters, and destroying or
reconstructing the target window during a getter causes an Error
before native drawing.
Window garbage collection
Window collection reports PHP references without refreshing native metadata or running property destructors during traversal. Ordinary property enumeration still refreshes the native snapshot. This separates collection from observable property updates and fixes a PHP 8.0 crash with retained aliases and cycles.
Native resource serialization
Window, cursor, RWops, surface, pixel buffer, palette, pixel format and GL context objects remain extensible, but their native handles cannot be serialized or reconstructed from serialized data. The same denial applies to final font, chunk, music, joystick and controller objects. Serialize application data such as asset paths and settings, then create fresh native resources when loading it. Ordinary SDL rectangles, points and colors remain serializable.
On PHP 8.0, these classes supply final public
__serialize(): array and
__unserialize(array $data): void guards. Subclasses
cannot override these methods; attempting to do so produces
PHP’s normal final-method declaration error. Direct calls and
valid serialized object payloads raise catchable exceptions.
Legacy Serializable and custom serialized payloads
retain their native denial. PHP 8.1 and newer keep the built-in
class flag that rejects serialization before any user hooks run.
Native subclasses’ other methods and properties continue to work
normally.
Malformed PNG input
A PNG that ends early, whether in its header, image data or CRC, fails with a libpng read error. The loader releases its decoder and surface state and returns null with an SDL error, and later valid images load normally.
Run the textured cube
Select SDL Cube in the embedded PHP editor.
It renders a rotating perspective cube using
sean-icon-32.png, a TrueType text scroller, looping
MP3 music, and a WAV sound effect. Four messages stream in from
left to right with a sine wave through the individual letters,
pause in reading order, then leave to the right. Their bold
cyan/white/sand lettering and black outline sit over the
spinning cube. Edit the example’s MESSAGES array to
change the text; long messages wrap to fit the canvas. Glyphs,
gradient and outline are baked into one cached texture atlas.
SPIN_SPEED and KEYBOARD_SPEED control
automatic and manual rotation. The track is Unreal
Superhero 3 by Kenët and rez, credited
from the supplied WOJTEK3.mp3 ID3 tags. The texture
uses GL_NEAREST without mipmaps, and the canvas
uses pixelated scaling. SDL Sine remains
available as the smaller original example.
The cube canvas fills the editor’s preview box. It updates its drawing buffer, viewport, perspective, and text overlay when the box changes size, preserving the cube’s proportions in landscape and portrait layouts. Cleanup disconnects its resize observer.
The cube requires WebGL2. Click Enable audio to satisfy the browser’s user gesture requirement, then focus the canvas for keyboard input:
| Control | Action |
|---|---|
| Arrows / WASD | Rotate the cube |
| Space | Pause or resume rotation and text |
| R | Reset rotation and restart the first message |
| M | Mute or resume enabled audio |
| Escape | Stop the demo |
Audio pauses when focus leaves the canvas. Run restarts a stopped demo; Refresh releases its native resources. Graphics context loss pauses rendering, and restoration rebuilds the GL resources.
For your own page, use
demo-web/public/scripts/sdl-cube.php and its asset
loader, demo-web/src/lib/sdlAssets.js, from the
php-wasm release that matches your runtime. Before running the
cube, stage these files under /preload/sdl in PHP’s
virtual filesystem:
| File | Source in the php-wasm checkout |
|---|---|
sean-icon-32.png |
demo-web/src/assets/icons/sean-icon-32.png |
DejaVuSansMono.ttf |
demo-web/public/sdl/DejaVuSansMono.ttf |
WOJTEK3.mp3 |
demo-web/public/sdl/WOJTEK3.mp3 |
click.wav |
demo-web/public/sdl/click.wav |
The asset loader also retains loop.ogg for code
saved in older shared cube links.
The demo fetches all assets with HTTP and empty-body checks
and a 30-second timeout, then writes them into the idle
runtime’s filesystem. A failed fetch reports its filename and
can be retried with Run. A native audio decode
failure presents Retry audio while rendering
continues. Preserve the font/audio notices in
demo-web/public/sdl/LICENSE.txt when copying
assets.
Development notes
Test coverage, size measurements, performance baselines and
per-change verification records are maintained with the package
source. See the php-sdl-wasm
README and coverage
record, including the manual device checks that remain
unverified.