v2026.9.4
All Bundles
Bundle OpenGL 3.3 core rendering on top of SDL2. Builds on sdl2.obl for the window and context; this bundle is the GL side. Compile with -lib sdl2. Targets the **3.3 core profile**, forward-compatible. That is the highest common denominator across the platforms Objeck ships: Windows, Linux and macOS desktop all reach it, and macOS caps at 4.1 core so nothing above 4.1 is portable there. GLES-only hardware (Raspberry Pi and similar) is out of scope by construction. Layers, from thinnest to friendliest -- all public, so nothing is capped: * GLWindow -- a window and a 3.3 core context, opened and paced for you. * GL -- static state operations. Thin, but never per-vertex. * Shader -- a linked program; Flat/Textured/TexturedFog/Normals built in, so the common cases need no GLSL at all. * Mesh -- geometry uploaded once into buffer objects, drawn many times, with Cube/Plane/Quad/Sphere built in, OBJ loading, and instancing. * Texture2D -- a texture, from a file or generated; Solid/Checker built in. * Light -- one directional light plus ambient, written into a shader. * RenderTarget -- draw into a texture instead of the window, then sample it. * Material -- a surface: its texture, tint and shininess. * Frustum -- the six planes of a view, for skipping what is off screen. * ShadowMap -- shadows from a directional or spot light, depth pass and all. * PointShadow -- shadows from a point light, in every direction, via a cube map. * Overlay -- text and rectangles over the scene, positioned in pixels. * Transform -- position, rotation and scale, with a cached matrix. * Box, Scene -- a world of boxes: draws itself, and answers collision. ## Why the API is shaped this way The native call boundary is expensive: the VM resolves each native symbol by string on EVERY call (GetProcAddress/dlsym) and boxes every argument into a fresh holder. A 1:1 mapping of OpenGL onto that would be thousands of lookups and allocations per frame. So each call here does real work -- 'compile a program from two sources' is one call, not the five GL calls it decomposes into -- and bulk data crosses as whole arrays, never element by element. This happens to be exactly what GL 3.3 core wants anyway: upload geometry once, then draw with few calls. ## Adding a call One `void fn(VMContext&)` in the OpenGL section of core/lib/sdl/sdl.cpp, and one method here that names it. See that file's header for the two rules that matter (positional slot indices, and keeping each call coarse).

GL

OpenGL state operations. Every method needs a current context (Window->GLCreateContext), and GL state is per-context, so these all act on whichever context is current on this thread. ## Shutting down, in this order 1. Cursor->SetRelativeMouseMode(false), if it was ever set -- while the window still exists. 2. **Every Shader, Mesh and Texture2D, while the context is still current.** Their Free methods issue glDeleteProgram/glDeleteBuffers/glDeleteTextures, and those need a current context; after the context is gone they are a no-op at best and a driver crash at worst. 3. Any Surface. Order-independent -- Texture2D->New copies the pixels, so a surface can in fact be freed as soon as the texture is built. 4. GLContext->Delete() 5. Window->Destroy() 6. Core->Quit() Getting step 2 wrong leaks GPU memory silently, and process exit reclaims it, so a program that frees nothing at all looks and behaves identically to a correct one. It only becomes a bug in something long-running that rebuilds meshes -- at which point it is an unbounded VRAM leak with no symptom pointing back here.

Operations

BeginTransparency # function

Enable ordinary alpha transparency: blending on, factors set, depth writing off. The three calls that go together, because getting one of the three wrong is what makes transparency look broken rather than absent. Call EndTransparency when the transparent pass is done. CLEAR THE FRAME BEFORE CALLING THIS. Turning depth writing off also disables clearing the depth buffer -- see SetDepthWrite -- so a clear afterwards leaves stale depth behind.

function : BeginTransparency() ~ Nil

Clear # function

Clear the given buffers.

function : Clear(mask:Int) ~ Nil

Parameters

NameTypeDescription
maskIntClearBit values combined with 'or'

ClearColor # function

Set the color the next Clear fills the color buffer with. Components are clamped to 0.0-1.0.

function : ClearColor(r:Float, g:Float, b:Float, a:Float) ~ Nil

Parameters

NameTypeDescription
rFloatred
gFloatgreen
bFloatblue
aFloatalpha

ClearColorBuffer # function

Clear the color buffer only. Convenience for the common 2D case.

function : ClearColorBuffer() ~ Nil

Disable # function

Disable a capability.

function : Disable(capability:Int) ~ Nil

Parameters

NameTypeDescription
capabilityIntthe capability to disable

DrainErrors # function

Empty the GL error queue, returning everything that was in it.

function : DrainErrors() ~ Int[]

Return

TypeDescription
Intthe queued error codes, empty when there were none

Enable # function

Enable a capability, e.g. Capability->GL_DEPTH_TEST.

function : Enable(capability:Int) ~ Nil

Parameters

NameTypeDescription
capabilityIntthe capability to enable

EndTransparency # function

Undo BeginTransparency.

function : EndTransparency() ~ Nil

GetError # function

Take one error off the GL error queue. GL queues errors and each call removes only one, so a single check can hide others -- use DrainErrors to empty the queue.

function : GetError() ~ Int

Return

TypeDescription
Intan ErrorCode; GL_NO_ERROR when the queue is empty

GetExtensions # function

Every extension this context reports. Needed because a non-null proc address does NOT mean a feature is usable. SDL falls back to GetProcAddress on opengl32.dll on Windows, and on macOS the lookup goes into OpenGL.framework regardless of what the current context actually supports -- so a pointer can come back for something the driver will refuse. The extension string is the authority. In a core profile this is the only way to ask: glGetString(GL_EXTENSIONS) returns nothing and raises GL_INVALID_ENUM there.

function : GetExtensions() ~ String[]

Return

TypeDescription
Stringthe extension names, empty when there is no context

GetLastError # function

Why the last call silently did nothing. GetError only reports what GL itself considers an error, and the most confusing failures in this library are not GL errors at all: the entry points were never loaded, a mat4 was the wrong length, a uniform name does not exist in the program. Each of those leaves the error queue clean and renders a black window, so DrainErrors finds nothing and this is the only thing that can tell you why. Reading clears it, so the answer belongs to the call you just made.

function : GetLastError() ~ String

Return

TypeDescription
Stringthe reason, or an empty string when nothing has silently failed

Example

shader->SetMatrix4("mpv", mvp);   # typo
reason := GL->GetLastError();
if(reason->Size() > 0) {
reason->ErrorLine();          # 'no uniform named "mpv" ...'
};

GetMaxAnisotropy # function

The largest anisotropy level this driver offers. 0 when GL_EXT_texture_filter_anisotropic is absent, in which case asking for anisotropy is silently ignored rather than an error. Typically 16 where it is supported. This bundle has had HasExtension since the beginning, with this very extension as its documentation example, and nothing to set once the answer came back yes. This is the setter half.

function : GetMaxAnisotropy() ~ Float

Return

TypeDescription
Floatthe maximum, or 0.0 if unsupported

GetOptionalMissing # function

Optional entry points that could not be resolved. Separate from LoadFunctions, which reports the ones that are REQUIRED: a missing required function disables the whole layer, because nothing here can work without it. A missing optional one disables only itself. Keeping the two apart is what stops one absent extension from taking the renderer down with it.

function : GetOptionalMissing() ~ String

Return

TypeDescription
Stringa comma-separated list, empty when everything resolved

HasExtension # function

Whether this context reports an extension.

function : HasExtension(name:String) ~ Bool

Parameters

NameTypeDescription
nameStringthe full name, e.g. "GL_EXT_texture_filter_anisotropic"

Return

TypeDescription
Booltrue when it is present

IsEnabled # function

Is a capability currently on? Asked rather than assumed by anything that turns a capability off and has to put it back -- Overlay->Begin/End do exactly this with culling, and a program may legitimately have had it off already.

function : IsEnabled(capability:Int) ~ Bool

Parameters

NameTypeDescription
capabilityInta Capability value

Return

TypeDescription
Booltrue if enabled

LoadFunctions # function

Resolve the OpenGL 2.0+ entry points. Call once, after the context is current and before creating any Shader, Mesh or Texture2D. Everything above GL 1.1 has to be looked up at runtime; that happens in native code and is cached, so this is a one-off cost rather than per call.

function : LoadFunctions() ~ String

Return

TypeDescription
Stringan empty string on success, or a comma-separated list of the functions that could not be resolved -- which is what a context older than 3.3 looks like from here

ReadPixel # function

Read one pixel from the current framebuffer. The point of this is assertion: it lets a program (or a test) check that something was actually DRAWN, rather than only that nothing crashed. Read before swapping buffers, since the swap may discard the back buffer. GL's origin is bottom-left, so y counts up from the bottom of the drawable.

function : ReadPixel(x:Int, y:Int) ~ Int

Parameters

NameTypeDescription
xIntpixel column
yIntpixel row, counted from the bottom

Return

TypeDescription
Intthe pixel packed as 0xAARRGGBB

ReportErrors # function

Drain the error queue and print whatever was in it. Exists because every program was writing the same four lines: drain, count, test the count, print. Errors are worth checking at least once per run -- they are queued, so one raised in frame 2 is still there at exit.

function : ReportErrors(during:String) ~ Int

Parameters

NameTypeDescription
duringStringwhat was happening, included in the message

Return

TypeDescription
Inthow many errors there were, so a caller can also act on it

Scissor # function

Restrict drawing to a rectangle. Only has an effect while Capability->GL_SCISSOR_TEST is enabled. That constant has been in this bundle since the beginning with nothing to bind it to, so enabling it clipped to whatever rectangle the driver happened to start with -- usually the whole window, which made it look like it worked. Pixels, not points, and measured from the BOTTOM-left like the viewport -- not the top-left like Overlay. A split-screen viewport or a UI panel that must not be drawn over is what this is for.

function : Scissor(x:Int, y:Int, width:Int, height:Int) ~ Nil

Parameters

NameTypeDescription
xIntleft edge in pixels from the left
yIntbottom edge in pixels from the bottom
widthIntwidth in pixels
heightIntheight in pixels

SetBlend # function

How a fragment's colour combines with what is already in the buffer. Enabling Capability->GL_BLEND is not enough on its own, and that is the trap: GL's default factors are ONE and ZERO, which means "replace", so blending appears to be on and nothing changes. This is the other half. The two worth knowing: * **Straight alpha** -- SRC_ALPHA, ONE_MINUS_SRC_ALPHA. Ordinary transparency: glass, foliage, a fade. Needs the transparent things drawn AFTER the opaque ones and back-to-front among themselves, because a fragment can only blend with what is already there. * **Additive** -- ONE, ONE. Light: sparks, glows, fire. Order-independent, since addition commutes, which is why particle systems reach for it. With straight alpha, also turn depth WRITING off for the transparent pass while leaving the test on -- see SetDepthWrite. A transparent surface that writes depth hides whatever transparent thing is behind it.

function : SetBlend(source:Int, destination:Int) ~ Nil

Parameters

NameTypeDescription
sourceInthow much of the incoming fragment to use
destinationInthow much of what is already there to keep

SetCullMode # function

Which side of a triangle GL_CULL_FACE discards. GL_BACK by default, which is what every mesh in this bundle is wound for.

function : SetCullMode(mode:Int) ~ Nil

Parameters

NameTypeDescription
modeInta CullMode

SetDepthFunc # function

Which depth comparison counts as passing. GL_LESS is the default and is what a normal scene wants. GL_LEQUAL is what lets a skybox draw at the far plane and a decal draw exactly on the surface it decorates -- both need to pass at a depth that is already written.

function : SetDepthFunc(func:Int) ~ Nil

Parameters

NameTypeDescription
funcInta CompareFunc

SetDepthWrite # function

Whether drawing writes depth. The test is unaffected. Off for a transparent pass: those fragments should be hidden BY opaque geometry but should not hide each other, and the only way to have both is to test depth without writing it. **Clearing the depth buffer while this is off does nothing.** glClear of the depth buffer is subject to the same mask, so a frame that turns depth writing off and then clears keeps the previous frame's depth values -- and everything at the same distance as something drawn last time silently fails the test. Clear first, then turn writing off. This is not hypothetical: it is how the check for this in the regression suite first came out reading zero instead of a blend.

function : SetDepthWrite(enabled:Bool) ~ Nil

Parameters

NameTypeDescription
enabledBoolfalse to stop writing depth

SetLineWidth # function

How thick GL_LINES draws, in pixels. Treat anything above 1.0 as a REQUEST, not a setting. A core profile is allowed to refuse it outright with GL_INVALID_VALUE, and the two renderers this bundle is tested on disagree: Mesa refuses 3.0, desktop NVIDIA accepts it silently. A program that needs thick lines reliably has to draw them as quads. A refusal is REPORTED through GL->GetLastError and the GL error is consumed here rather than left standing. That second part matters more than it looks: GL errors persist until something reads them, so without it a single wide-line request at startup would surface as a failure in the next unrelated GetError call, anywhere in the program. This exact thing turned one CI leg red while the check that failed was three blocks away from the call that caused it.

function : SetLineWidth(width:Float) ~ Nil

Parameters

NameTypeDescription
widthFloatthickness in pixels; only 1.0 is guaranteed

SetPointSize # function

How large a GL_POINTS vertex draws, in pixels. Without this every point is one pixel, which is most of the reason DrawMode->GL_POINTS was not worth reaching even once it drew points at all.

function : SetPointSize(size:Float) ~ Nil

Parameters

NameTypeDescription
sizeFloatdiameter in pixels

SetPolygonOffset # function

Shift filled polygons in depth, without moving them on screen. The fix for two surfaces at the same depth flickering against each other -- a decal on a wall, a shadow on a floor, a highlight over a tile. Both are at the same z, the winner is decided by floating-point noise, and it changes as the camera moves, which is what makes it read as flickering rather than as a mistake. Enable Capability->GL_POLYGON_OFFSET_FILL first; this call on its own does nothing. GL->Enable(Capability->GL_POLYGON_OFFSET_FILL); GL->SetPolygonOffset(-1.0, -1.0); ... draw the decal ... GL->Disable(Capability->GL_POLYGON_OFFSET_FILL); NEGATIVE values pull towards the viewer, which is what a decal wants. Positive values push away, which is the other use: offsetting shadow casters to trade one artefact for another. Both terms matter and they are not interchangeable. `units` is a constant nudge in depth-buffer steps; `factor` scales with how steeply the polygon is sloped, which is the term that fixes a surface seen at a grazing angle where a constant offset is not enough. -1.0, -1.0 is the usual starting point. Before this existed the only way to lift a decal off a surface was to move it geometrically along the normal -- which works, and then breaks at distance where the offset is smaller than a depth step, and looks like a floating sticker up close.

function : SetPolygonOffset(factor:Float, units:Float) ~ Nil

Parameters

NameTypeDescription
factorFloatscaled by the polygon's depth slope
unitsFloatconstant offset in depth-buffer units

SetStencilFunc # function

Which stencil values pass the test. Only has an effect while Capability->GL_STENCIL_TEST is enabled. The window has requested 8 stencil bits since it learned to; this is the half that was missing, and without it the buffer could be cleared and nothing more -- no portal, mirror, outline or decal mask. The usual two-pass shape: write a mask, then draw only where it is. GL->Enable(Capability->GL_STENCIL_TEST); GL->SetStencilFunc(CompareFunc->GL_ALWAYS, 1, 0xFF); GL->SetStencilOp(StencilOp->GL_KEEP, StencilOp->GL_KEEP, StencilOp->GL_REPLACE); ... draw the mask ... GL->SetStencilFunc(CompareFunc->GL_EQUAL, 1, 0xFF); GL->SetStencilOp(StencilOp->GL_KEEP, StencilOp->GL_KEEP, StencilOp->GL_KEEP); ... draw what the mask reveals ...

function : SetStencilFunc(func:Int, reference:Int, mask:Int) ~ Nil

Parameters

NameTypeDescription
funcInta CompareFunc
referenceIntthe value to compare against
maskIntANDed with both values before comparing; 0xFF for all bits

SetStencilMask # function

Which stencil bits are writable.

function : SetStencilMask(mask:Int) ~ Nil

Parameters

NameTypeDescription
maskInt0xFF for all of them, 0x00 to make the buffer read-only

SetStencilOp # function

What to write to the stencil buffer in each of the three outcomes.

function : SetStencilOp(on_fail:Int, on_depth_fail:Int, on_pass:Int) ~ Nil

Parameters

NameTypeDescription
on_failIntwhen the stencil test fails
on_depth_failIntwhen stencil passes but depth fails
on_passIntwhen both pass -- usually GL_REPLACE for a mask

Viewport # function

Set the drawable region, in pixels. Use Window->GLGetDrawableSize rather than the size passed to Window->New: on a high-DPI display the drawable is larger than the window, and using the window size renders to a corner.

function : Viewport(x:Int, y:Int, width:Int, height:Int) ~ Nil

Parameters

NameTypeDescription
xIntleft
yIntbottom (GL's origin is bottom-left, not top-left)
widthIntwidth in pixels
heightIntheight in pixels