Created
June 22, 2026 07:25
-
-
Save m3m0r7/162ea5c5ca5ce491286b6ec2b10a07e1 to your computer and use it in GitHub Desktop.
How to use SDL with pnl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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