php-io-extensions / open-gl
1:1 PHP bindings of the OpenGL 4.1 core / OpenGL ES 3 calls both dialects share, with CGL on macOS and EGL on Linux
Package info
github.com/php-io-extensions/open-gl
Language:C
Type:php-ext
Ext name:ext-opengl
pkg:composer/php-io-extensions/open-gl
Requires
- php: >=8.4
Requires (Dev)
- pestphp/pest: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-08 14:16:37 UTC
README
1:1 PHP bindings of the OpenGL 4.1 core calls and the OpenGL ES 3 calls both dialects share, plus the context API that makes an offscreen context possible: CGL on macOS, EGL on Linux. Written in C against the Zend API. PHP 8.4+, NTS and ZTS.
An OpenGL context cannot exist without CGL or EGL, and the bytes path needs an
offscreen context, so those APIs ride in this extension. gl* functions are
global PHP functions under their C names. Object names (GLuint) are plain
ints. Constants are PHP constants under their C names, values taken from the
headers. CGL handles, EGL handles, and GLsync are final classes.
Requirements
- macOS: OpenGL 4.1, deprecated by Apple, present in SDK 15.4. The build
defines
GL_SILENCE_DEPRECATIONand linksOpenGL.framework. - Linux:
libegl-devandlibgles-dev(EGL 1.5 and GLESv2). The Pi measured here is Mesa 26.2, EGL 1.5, OpenGL ES 3.1 on V3D 7.1,GL_MAX_SAMPLES4.
Install
Through PIE:
pie install php-io-extensions/opengl
From a checkout of this package:
./install-macos.sh # Homebrew php@8.4 and php@8.4-zts, or pass PHP binaries ./install-debian-trixie.sh # Debian trixie, Raspberry Pi OS
Translations
Bindings are 1:1. There are no defaults and no composites. The only translations:
- A list GL takes with a count keeps the count as its own parameter:
glShaderSource($shader, $count, $string, $length),glDeleteBuffers($n, $buffers),glDrawBuffers($n, $bufs),glUniformMatrix4fv($location, $count, $transpose, $value)(16 floats a matrix),eglSwapBuffersWithDamageKHR($dpy, $surface, $rects, $n_rects). A count past the list is aValueError. - An out-parameter is a by-reference parameter:
glGenBuffers($n, $buffers)writes the list of names;glGetShaderInfoLog($shader, $bufSize, $length, $infoLog)writes at most$bufSizebytes, its NUL included. - A
const void *data argument isstring|int|null: bytes, an address, or NULL (null and 0). - A
void *destination is?int: an address, or an offset into the bound buffer object. - A pointer GL reads as a buffer offset (
glVertexAttribPointer,glDrawElements) is anint. - A C int converts as C converts it; a list element of
glUniformMatrix4fvas PHP's(float), ofglShaderSourceas PHP converts a value to a string. GLbooleanandEGLBooleanarebool.EGL_DEFAULT_DISPLAYis not a constant: the C macro is a pointer cast. Passnullor0.
Handles: pointer() is 0 once released, fromPointer(0) is null. A released handle goes to CGL, EGL or GL as NULL, which they answer with an error.
Refusals
The extension declines a call only where making it crashes the process. Each site has a probe in tests/Probes that shows the crash with the site off, on a probe build (OPENGL_CONFIGURE="--enable-opengl-probes" ./install-macos.sh, env OPENGL_PROBE_OFF=<site>):
opengl-no-context(macOS): agl*call with no current context is anError. Apple's GL dereferences the current context. Linux's libglvnd answers such a call with nothing, so there is no check there.opengl-attrib-terminator: a CGL attribute list that does not end with 0. EGL lists go to EGL as given.opengl-bytes-length: a string shorter than what GL reads. For buffers that is$size. ForglTexImage2DandglTexSubImage2Dit comes from the width, height, format and type (the GL 4.1 and ES 3.0 pixel tables) and the unpack state: row length isGL_UNPACK_ROW_LENGTHor the width, the stride is that times the pixel rounded up toGL_UNPACK_ALIGNMENT, and the size isskip rows × stride + skip pixels × pixel + stride × (height − 1) + width × pixel. A format/type pair outside the tables goes to GL, which refuses it without reading.opengl-null-address: address 0 toglBufferSubData, or toglTexSubImage2D/glReadPixelswith no pixel buffer bound.glBufferDataandglTexImage2Dtake NULL.opengl-list-count: a count past its list.opengl-wl-null-window(Linux):wl_egl_window_resizeorwl_egl_window_destroyof 0.
EGL window surfaces (Linux): eglCreateWindowSurface(display, config, int $native_window, ?array $attribs) takes an X11 window id or a wl_egl_window address from wl_egl_window_create(int $wl_surface, w, h) (wl_egl_window_resize, wl_egl_window_destroy). eglGetConfigAttrib, eglSurfaceAttrib. Constants for EGL_GL_COLORSPACE_KHR (sRGB, linear, scRGB linear, BT.2020 PQ, Display P3), float configs (EGL_COLOR_COMPONENT_TYPE_*), SMPTE 2086 / CTA 861.3 metadata, EGL_PLATFORM_WAYLAND_KHR / _X11_KHR. Pi 5 Mesa (labwc) lists EGL_KHR_gl_colorspace + EGL_EXT_pixel_format_float only; configs come deepest first, and only 8-bit ones take a colour space.
glMapBufferRange answers the mapped address (int, 0 when GL fails). glReadPixels writes into an address: memory from another extension (an ext-fb buffer's pointer()), or a mapped pixel-pack buffer.
glGetIntegerv writes every int GL writes for the pname: 4 for GL_VIEWPORT, GL_SCISSOR_BOX, GL_COLOR_WRITEMASK, GL_COLOR_CLEAR_VALUE, GL_BLEND_COLOR; 2 for the ranges and GL_MAX_VIEWPORT_DIMS; the matching GL_NUM_* count for GL_COMPRESSED_TEXTURE_FORMATS, GL_PROGRAM_BINARY_FORMATS and GL_SHADER_BINARY_FORMATS.
On OpenGL ES, glTexSubImage2D of GL_RGB into a texture created with sized internal format GL_RGBA8 is GL_INVALID_OPERATION. Desktop GL accepts that conversion. The sizing test lets the 25-byte string through, then drains that driver error so it cannot fail a later glGetError().
Coverage
SUPPORT.md lists every command of the floors this extension builds on: OpenGL 4.1 core profile, OpenGL ES 3.0 and EGL 1.5 from the Khronos registries, and every CGL function of the macOS SDK's OpenGL.framework headers, each marked bound or not; extensions are not counted. The API badge is computed from it. Regenerate both on macOS after a binding change, and check them:
php84 scripts/support.php # writes SUPPORT.md and the badge php84 scripts/support.php --check # exits 1 when either is stale
Example
if (PHP_OS_FAMILY === 'Darwin') { $attribs = [kCGLPFAOpenGLProfile, kCGLOGLPVersion_GL4_Core, kCGLPFAAccelerated, kCGLPFAColorSize, 24, kCGLPFAAlphaSize, 8, 0]; CGLChoosePixelFormat($attribs, $pixelFormat, $count); CGLCreateContext($pixelFormat, null, $context); CGLDestroyPixelFormat($pixelFormat); CGLSetCurrentContext($context); } else { $display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, null, null); eglInitialize($display, $major, $minor); eglBindAPI(EGL_OPENGL_ES_API); eglChooseConfig($display, [EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_NONE], $configs, 1, $count); $context = eglCreateContext($display, $configs[0], null, [EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE]); eglMakeCurrent($display, null, null, $context); } glClearColor(0.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); $target = new FbBuffer(new FbFormat(FB_LAYOUT_RGBA8888, channelOrder: FB_CHANNELS_RGBA), 8, 8); // ext-fb glReadPixels(0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, $target->pointer()); $pixels = $target->bytes();
Offscreen contexts need no display on either machine. The Mac context is CGL 4.1 core. The Linux context is surfaceless EGL, OpenGL ES 3.1.
Test
composer install php84 -d memory_limit=128M vendor/bin/pest # Homebrew NTS zhp -d memory_limit=128M vendor/bin/pest # Homebrew ZTS
On the Pi, copy the tree and run ./install-debian-trixie.sh, then the same Pest command with php. The gate draws a triangle by stencil-then-cover into a 4× framebuffer, resolves it with glBlitFramebuffer, and reads it back.