Skip to content

Instantly share code, notes, and snippets.

@m3m0r7
Created June 22, 2026 07:25
Show Gist options
  • Select an option

  • Save m3m0r7/162ea5c5ca5ce491286b6ec2b10a07e1 to your computer and use it in GitHub Desktop.

Select an option

Save m3m0r7/162ea5c5ca5ce491286b6ec2b10a07e1 to your computer and use it in GitHub Desktop.
How to use SDL with pnl
<?php
declare(strict_types=1);
/*
* A "DVD screensaver" in a real native window, powered by pnl — written against
* the global-function API (features.use_functions = true).
*
* A logo drifts and ricochets off the walls; every bounce swaps the artwork
* (PHP logo <-> elePHPant). The sprites are SVGs rasterised with a real alpha
* channel by SDL2_image's IMG_Load.
*
* SDL2 and SDL2_image are two separate pnl packages. Runtime::compose() fuses
* them into ONE shared FFI scope (adopted by both member entities), so the
* per-package global helpers below — \Pnlx\Func\Libsdl\* and \Pnlx\Func\Sdlimage\*
* — dispatch through that one scope and an IMG_Load surface flows straight into
* SDL's renderer.
*
* Env knobs (for headless checks): DVD_FRAMES=N auto-exits after N frames;
* DVD_START=1 starts on the elePHPant.
*/
require_once __DIR__ . '/@pnlx/autoload.php';
use Pnlx\FFI\NativeLibraryRegistry;
use Pnlx\Libsdl\Enums\SDL_bool;
use Pnlx\Libsdl\Libsdl;
use Pnlx\Libsdl\Types\SDL_Rect;
use Pnlx\Libsdl\Types\SDL_Renderer;
use Pnlx\Libsdl\Types\SDL_Texture;
use Pnlx\Runtime;
use Pnlx\Sdlimage\Sdlimage;
use function Pnlx\Func\Libsdl\SDL_CreateRenderer;
use function Pnlx\Func\Libsdl\SDL_CreateTextureFromSurface;
use function Pnlx\Func\Libsdl\SDL_CreateWindow;
use function Pnlx\Func\Libsdl\SDL_Delay;
use function Pnlx\Func\Libsdl\SDL_DestroyRenderer;
use function Pnlx\Func\Libsdl\SDL_DestroyTexture;
use function Pnlx\Func\Libsdl\SDL_DestroyWindow;
use function Pnlx\Func\Libsdl\SDL_FlushEvents;
use function Pnlx\Func\Libsdl\SDL_FreeSurface;
use function Pnlx\Func\Libsdl\SDL_GetError;
use function Pnlx\Func\Libsdl\SDL_HasEvent;
use function Pnlx\Func\Libsdl\SDL_Init;
use function Pnlx\Func\Libsdl\SDL_PumpEvents;
use function Pnlx\Func\Libsdl\SDL_QueryTexture;
use function Pnlx\Func\Libsdl\SDL_Quit;
use function Pnlx\Func\Libsdl\SDL_RaiseWindow;
use function Pnlx\Func\Libsdl\SDL_RenderClear;
use function Pnlx\Func\Libsdl\SDL_RenderCopy;
use function Pnlx\Func\Libsdl\SDL_RenderPresent;
use function Pnlx\Func\Libsdl\SDL_SetRenderDrawColor;
use function Pnlx\Func\Libsdl\SDL_SetTextureBlendMode;
use function Pnlx\Func\Libsdl\SDL_ShowWindow;
use function Pnlx\Func\Sdlimage\IMG_Load;
use function Pnlx\Util\is_null;
use const Pnlx\Libsdl\SDL_BLENDMODE_BLEND;
use const Pnlx\Libsdl\SDL_FIRSTEVENT;
use const Pnlx\Libsdl\SDL_INIT_VIDEO;
use const Pnlx\Libsdl\SDL_LASTEVENT;
use const Pnlx\Libsdl\SDL_QUIT;
use const Pnlx\Libsdl\SDL_RENDERER_ACCELERATED;
use const Pnlx\Libsdl\SDL_RENDERER_PRESENTVSYNC;
use const Pnlx\Libsdl\SDL_WINDOW_SHOWN;
use const Pnlx\Libsdl\SDL_WINDOWPOS_CENTERED;
const WIN_W = 900;
const WIN_H = 640;
const ASSETS = __DIR__ . '/assets';
const BG = [18, 18, 24]; // window background (R, G, B)
const FRAME_MS = 16; // ~60 fps
const MIN_SPEED = 3.0; // per-axis pixels/frame; re-rolled on each bounce so
const MAX_SPEED = 6.0; // the path never locks into a corner-to-corner orbit
/** A bouncing sprite: its texture and on-screen size. */
final class Sprite
{
public function __construct(
public readonly SDL_Texture $texture,
public readonly int $width,
public readonly int $height,
) {
}
}
/** Print the current SDL error and exit. */
function fail(string $what): never
{
fwrite(STDERR, $what . ': ' . SDL_GetError() . PHP_EOL);
exit(1);
}
/** A fresh per-axis speed, so a bounce changes the trajectory's slope. */
function roll_speed(): float
{
return MIN_SPEED + (MAX_SPEED - MIN_SPEED) * (mt_rand(0, 1000) / 1000.0);
}
/**
* Rasterise an SVG (Sdlimage) into an alpha-blended GPU texture (Libsdl), scaled
* to `$height` px tall — both via the shared-scope global helpers.
*/
function load_sprite(SDL_Renderer $renderer, string $svg, int $height): Sprite
{
$surface = IMG_Load(ASSETS . '/' . $svg);
if (is_null($surface)) {
fail("IMG_Load($svg)"); // image errors surface through SDL_GetError()
}
$texture = SDL_CreateTextureFromSurface($renderer, $surface);
SDL_FreeSurface($surface);
if (is_null($texture)) {
fail("SDL_CreateTextureFromSurface($svg)");
}
SDL_SetTextureBlendMode($texture, SDL_BLENDMODE_BLEND);
// Ask the texture its rasterised size (by-reference out-params), then scale.
$format = 0;
$access = 0;
$w = 0;
$h = 0;
SDL_QueryTexture($texture, $format, $access, $w, $h);
$scale = $h > 0 ? $height / $h : 1.0;
return new Sprite($texture, (int) round($w * $scale), $height);
}
// Fuse SDL2 + SDL2_image into one shared FFI scope and adopt it into both member
// entities, so the \Pnlx\Func\Libsdl\* and \Pnlx\Func\Sdlimage\* global helpers all
// dispatch through that one scope (and an IMG_Load surface works with SDL).
$composite = Runtime::compose([Libsdl::class, Sdlimage::class]);
NativeLibraryRegistry::boot($composite::class);
if (SDL_Init(SDL_INIT_VIDEO)->toInt() !== 0) {
fail('SDL_Init');
}
$window = SDL_CreateWindow(
'pnl × SDL — DVD elePHPant',
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
WIN_W,
WIN_H,
SDL_WINDOW_SHOWN,
);
if (is_null($window)) {
fail('SDL_CreateWindow');
}
SDL_ShowWindow($window);
SDL_RaiseWindow($window); // otherwise it can spawn behind the editor
$renderer = SDL_CreateRenderer(
$window,
-1,
SDL_RENDERER_ACCELERATED->toInt() | SDL_RENDERER_PRESENTVSYNC->toInt(),
);
if (is_null($renderer)) {
fail('SDL_CreateRenderer');
}
$sprites = [
load_sprite($renderer, 'php.svg', 150), // classic blue-oval PHP logo
load_sprite($renderer, 'elephpant.svg', 200), // the elePHPant mascot
];
$current = ((int) (getenv('DVD_START') ?: 0)) & 1;
// Start mid-window with a random diagonal heading.
$x = (WIN_W - $sprites[$current]->width) / 2.0;
$y = (WIN_H - $sprites[$current]->height) / 2.0;
$vx = (mt_rand(0, 1) ? 1 : -1) * roll_speed();
$vy = (mt_rand(0, 1) ? 1 : -1) * roll_speed();
$dst = new SDL_Rect(); // reused each frame as the destination rectangle
$maxFrames = (int) (getenv('DVD_FRAMES') ?: 0);
for ($frame = 0; $maxFrames === 0 || $frame < $maxFrames; $frame++) {
// Pump the OS event queue and stop on the window-close request. The SDL_Event
// union is opaque to FFI, so we inspect by type only.
SDL_PumpEvents();
if (SDL_HasEvent(SDL_QUIT) === SDL_bool::SDL_TRUE) {
break;
}
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
$sprite = $sprites[$current];
$x += $vx;
$y += $vy;
// Bounce off each wall, re-rolling that axis' speed so the slope keeps changing
// and the logo visits every wall instead of pinging between two corners.
$bounced = false;
if ($x <= 0) {
$x = 0;
$vx = roll_speed();
$bounced = true;
} elseif ($x + $sprite->width >= WIN_W) {
$x = WIN_W - $sprite->width;
$vx = -roll_speed();
$bounced = true;
}
if ($y <= 0) {
$y = 0;
$vy = roll_speed();
$bounced = true;
} elseif ($y + $sprite->height >= WIN_H) {
$y = WIN_H - $sprite->height;
$vy = -roll_speed();
$bounced = true;
}
if ($bounced) {
$current ^= 1; // swap artwork
$sprite = $sprites[$current];
// Keep the freshly swapped (possibly larger) sprite on-screen.
$x = max(0.0, min($x, (float) (WIN_W - $sprite->width)));
$y = max(0.0, min($y, (float) (WIN_H - $sprite->height)));
}
SDL_SetRenderDrawColor($renderer, BG[0], BG[1], BG[2], 255);
SDL_RenderClear($renderer);
$dst->setX((int) round($x))
->setY((int) round($y))
->setW($sprite->width)
->setH($sprite->height);
SDL_RenderCopy($renderer, $sprite->texture, null, $dst);
SDL_RenderPresent($renderer);
SDL_Delay(FRAME_MS);
}
foreach ($sprites as $sprite) {
SDL_DestroyTexture($sprite->texture);
}
SDL_DestroyRenderer($renderer);
SDL_DestroyWindow($window);
SDL_Quit();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment