GLWindow
A window with a GL 3.3 core context, ready to draw into. This exists because opening one by hand takes about thirty lines that are the same in every program: initialise SDL, set six context attributes in the right order and before the window, create the window with the GL flag, create the context, check you actually got 3.3 core, resolve the entry points, and set the viewport from the DRAWABLE size rather than the window size. Across the four programs in this repo that block was 105 lines, and the only things that varied were the title, the size, one flag and the wording of an error message. ## What it decides for you A depth buffer is always requested and GL_DEPTH_TEST is enabled, because the two are separate calls sixty lines apart and forgetting the attribute makes the enable silently do nothing. BeginFrame clears colour and depth together for the same reason. Vsync is requested, and whether it was actually granted is remembered -- so EndFrame adds a delay only when it was not. Sleeping on top of a vsynced swap is a double wait, which is why frame pacing is worth owning rather than leaving to each program to hand-roll differently. ## What it does not take away Nothing is capped. GetEvent hands back the SDL Event if you would rather drive the queue yourself, Keyboard->GetState is unaffected by any of this, and every GL, Shader, Mesh and Texture2D call remains available exactly as before.
Example
window := GLWindow->New("Cube", 720, 540);
if(<>window->IsOk()) {
window->GetError()->ErrorLine();
return;
};
while(<>window->PollEvents()) {
window->BeginFrame();
# ... draw ...
window->EndFrame();
};
window->Free();Operations
- New
- ApplyDeadzone
- BeginFrame
- ButtonBit
- ClosePad
- EndFrame
- Free
- GetAspect
- GetContext
- GetDelta
- GetDrawableGeneration
- GetError
- GetEvent
- GetFps
- GetHeight
- GetMouseDeltaX
- GetMouseDeltaY
- GetMouseX
- GetMouseY
- GetOwnedCount
- GetPad
- GetPadAxis
- GetPadAxisRaw
- GetPadDeadzone
- GetPadName
- GetSamples
- GetVersion
- GetWheelX
- GetWheelY
- GetWidth
- GetWindow
- HasAudio
- HasPad
- HasVsync
- IsOk
- KeyHeld
- MouseHeld
- MousePressed
- MouseReleased
- OpenPad
- Own
- PadHeld
- PadPressed
- PadReleased
- PollPad
- RequestQuit
- SetDepthTest
- SetPadDeadzone
- SetQuitOnEscape
- SetTargetFps
- WasPressed
- WasReleased
ApplyDeadzone # function
The dead-zone curve on its own, as a pure function. Split out from GetPadAxis so it can be TESTED. The rest of the controller path needs hardware to exercise and neither CI nor a build machine has any, so the one piece with actual logic in it is the one piece that must be reachable without a pad plugged in. It is public because a program reading a stick through some other route wants the same curve.
function : ApplyDeadzone(raw:Float, deadzone:Float) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| raw | Float | -1..1, as GetPadAxisRaw returns |
| deadzone | Float | fraction of full travel to ignore |
Return
| Type | Description |
|---|---|
| Float | -1..1, rescaled so it ramps from 0 at the edge of the dead zone |
BeginFrame #
Start a frame: measure elapsed time, then clear colour and depth. Depth is cleared as well as colour because a depth buffer is always requested and depth testing is always on -- clearing only colour leaves last frame's depth values in place, and the second frame then renders with everything mysteriously behind everything else.
method : public : BeginFrame() ~ NilButtonBit #
Buttons as a bit, so held state is one Int rather than an array.
method : private : ButtonBit() ~ IntParameters
| Name | Type | Description |
|---|---|---|
EndFrame #
Present the frame. Read any pixels BEFORE calling this -- GL->ReadPixel reads the back buffer, and a swap is allowed to discard it. Sleeps only when vsync was refused. With vsync on, the swap already blocks until the display is ready and a second wait would halve the frame rate.
method : public : EndFrame() ~ NilFree #
Close everything, in the order that works. Relative mouse mode first while the window still exists, then the context, then the window, then SDL. GL objects you created -- Shader, Mesh, Texture2D -- are yours to free, and must be freed BEFORE this, while the context is still current. See the shutdown order on the GL class. Safe to call twice.
method : public : Free() ~ NilGetAspect #
method : public : GetAspect() ~ FloatReturn
| Type | Description |
|---|---|
| Float | width divided by height, for Matrix4->Perspective |
GetContext #
The GL context.
method : public : GetContext() ~ GLContextReturn
| Type | Description |
|---|---|
| GLContext | the context |
GetDelta #
Seconds since the previous BeginFrame, clamped to 0.1. Multiply movement by this and the program behaves the same at 30 and at 240 frames a second. The first frame reports 0.0, so nothing jumps on startup.
method : public : GetDelta() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the frame time in seconds |
GetDrawableGeneration #
A number that changes whenever the drawable changes size. PollEvents keeps the viewport right on its own, but a projection matrix is the program's, and a stale one stretches the scene while a HUD drawn in pixels stays correct. Compare this against the last value you saw and rebuild what depends on the size when it moves (see the example). For the projection specifically there is nothing to compare -- call Camera->SetAspect(window->GetAspect()) every frame and it does nothing unless the value actually changed.
method : public : GetDrawableGeneration() ~ IntReturn
| Type | Description |
|---|---|
| Int | a counter, not a size; only equality is meaningful |
Example
if(window->GetDrawableGeneration() <> @seen) {
@seen := window->GetDrawableGeneration();
target->Free(); target := RenderTarget->New(window->GetWidth(), ...);
};GetError #
Why opening failed.
method : public : GetError() ~ StringReturn
| Type | Description |
|---|---|
| String | the reason, or an empty string when IsOk is true |
GetEvent #
The event this window polls with. Here so a program with its own event needs can drive the queue directly instead of calling PollEvents -- do one or the other, not both, since whichever drains the queue first is the only one that sees each event.
method : public : GetEvent() ~ EventReturn
| Type | Description |
|---|---|
| Event | the event |
GetFps #
Frames per second as actually measured, not the target.
method : public : GetFps() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the rate, or 0.0 before the second frame |
GetHeight #
The drawable height in pixels.
method : public : GetHeight() ~ IntReturn
| Type | Description |
|---|---|
| Int | the height in pixels |
GetMouseDeltaX #
Mouse movement accumulated by the last PollEvents, in pixels.
method : public : GetMouseDeltaX() ~ FloatReturn
| Type | Description |
|---|---|
| Float | horizontal movement this frame |
GetMouseDeltaY #
method : public : GetMouseDeltaY() ~ FloatReturn
| Type | Description |
|---|---|
| Float | vertical movement this frame, positive downward as SDL reports it |
GetMouseX #
Where the mouse was, in window coordinates. POINTS, not pixels -- SDL reports event coordinates in the window's coordinate space, which on a high-DPI display is not the drawable size GetWidth returns. Multiply by GetScale if you need pixels.
method : public : GetMouseX() ~ IntReturn
| Type | Description |
|---|---|
| Int | distance from the left edge |
GetMouseY #
method : public : GetMouseY() ~ IntReturn
| Type | Description |
|---|---|
| Int | distance from the top edge, in points |
GetOwnedCount #
method : public : GetOwnedCount() ~ IntReturn
| Type | Description |
|---|---|
| Int | how many objects this window will release on the way out |
GetPad #
The controller itself, for everything this wrapper does not cover -- rumble, the LED, touchpads, the gyro. Do NOT close it; the window owns it and closes it on Free.
method : public : GetPad() ~ GameControllerReturn
| Type | Description |
|---|---|
| GameController | the controller, or Nil |
GetPadAxis #
A controller axis as -1..1, with the dead zone taken out. Raw SDL axes are -32768..32767 and a stick at rest does not read zero: it wanders by a few hundred, and a camera driven straight off the raw value drifts on its own. Every program that uses a stick has to solve this, so it is solved once here. The dead zone is RESCALED rather than clamped. Clamping gives a stick that does nothing and then jumps to 0.15; rescaling ramps from 0 at the edge of the dead zone to 1 at full deflection, so slow movement stays possible. That difference is the whole reason to do it here rather than leave callers an `if` to write. Triggers are axes too, and they rest at -1 rather than 0 -- so read a trigger with GetPadAxisRaw if a resting value of -1 matters to you.
method : public : GetPadAxis(axis:GameControllerAxis) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| axis | GameControllerAxis | a GameControllerAxis |
Return
| Type | Description |
|---|---|
| Float | -1..1, or 0.0 when there is no pad |
GetPadAxisRaw #
A controller axis as -1..1 with NO dead zone applied.
method : public : GetPadAxisRaw(axis:GameControllerAxis) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| axis | GameControllerAxis | a GameControllerAxis |
Return
| Type | Description |
|---|---|
| Float | -1..1, or 0.0 when there is no pad |
GetPadDeadzone #
method : public : GetPadDeadzone() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the dead zone as a fraction of full travel |
GetPadName #
The controller's name, e.g. "Xbox Series Controller".
method : public : GetPadName() ~ StringReturn
| Type | Description |
|---|---|
| String | the name, or an empty string when there is no pad |
GetSamples #
A second GLWindow is not really supported, and this is why. Creating a context MAKES IT CURRENT, so opening a second window silently redirects every later GL call to it -- textures, draws and pixel readback alike, with no error anywhere. And Free calls Core->Quit, which takes SDL down for the whole process, so freeing either one kills both. If you must open a second window, put the first one's context back afterwards: first->GetWindow()->GLMakeCurrent(first->GetContext()); and free only at exit. Both facts were found by a test that opened a second window to compare pixel formats: it hung on the first Free, and once that was fixed, three unrelated checks elsewhere in the suite went red because they were sampling the wrong framebuffer. How many multisample samples this window actually got. Asked for is not granted: a driver that cannot honour the request makes the window fail to create, and rather than not opening at all this drops the request and retries without it. So a program that cares has to ask, not assume.
method : public : GetSamples() ~ IntReturn
| Type | Description |
|---|---|
| Int | the sample count, or 0 for no multisampling |
GetVersion #
The driver's version string for this context, e.g. "3.3.0 Core Profile Context 26.7.1". Read while opening, so this costs nothing to ask for.
method : public : GetVersion() ~ StringReturn
| Type | Description |
|---|---|
| String | the version string, or empty when the context never opened |
GetWheelX #
Horizontal wheel movement accumulated by the last PollEvents.
method : public : GetWheelX() ~ IntReturn
| Type | Description |
|---|---|
| Int | notches, positive to the right |
GetWheelY #
Wheel movement accumulated by the last PollEvents.
method : public : GetWheelY() ~ IntReturn
| Type | Description |
|---|---|
| Int | notches, positive away from the user |
GetWidth #
The drawable width in PIXELS -- what GL->Viewport and GL->ReadPixel work in. On a high-DPI display this is LARGER than the width that was asked for, typically double, because the window is requested in points and GL works in pixels. That is not hypothetical: high-DPI is requested, so on a Retina Mac a 960-point window reports 1920 here.
method : public : GetWidth() ~ IntReturn
| Type | Description |
|---|---|
| Int | the width in pixels |
GetWindow #
The SDL window, for anything this class does not wrap.
method : public : GetWindow() ~ WindowReturn
| Type | Description |
|---|---|
| Window | the window |
HasAudio #
Did the audio device open? False when audio was not asked for, and also when it was asked for and the machine has no working output -- which is common on a CI runner and is not a reason to refuse to run. Sound is the thing to make optional here, not the program.
method : public : HasAudio() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | true when the mixer is usable |
HasPad #
Is a controller attached? False when none is plugged in AND when the controller subsystem could not start at all -- a program should treat both the same way, since in either case there is nothing to read.
method : public : HasPad() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | true when GetPad returns something usable |
HasVsync #
method : public : HasVsync() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | whether the swap interval was actually granted |
IsOk #
method : public : IsOk() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | true when the window and context are usable |
KeyHeld #
Is this key held down right now? A STATE, not an edge -- true for as long as the key is down, which is what movement wants. WasPressed is the edge, true for one frame per press. The bounds check is the point. Reading Keyboard->GetState directly means writing this every time: if(keys <> Nil & scancode >= 0 & scancode < keys->Size()) { ... } and both examples in this repo that take keyboard input had written it, byte for byte identically, because held MOUSE was framework state and held keys were not. Reads a snapshot taken once per PollEvents, so asking about eight keys costs eight array reads rather than eight native calls.
method : public : KeyHeld(scancode:Int) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| scancode | Int | a Scancode value |
Return
| Type | Description |
|---|---|
| Bool | true while the key is down; false before the first PollEvents |
KeyHeld #
Is either of these keys held? For the case that actually recurs: one action on two keys -- left shift or right shift, W or Up. An array would have read better, but an Objeck array literal takes literal expressions only, so `[Scancode->SDL_SCANCODE_W, ...]` does not compile and the caller would have to build and fill an Int[] to ask a one-line question.
method : public : KeyHeld(first:Int, second:Int) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| first | Int | a Scancode value |
| second | Int | another |
Return
| Type | Description |
|---|---|
| Bool | true if either is down |
MouseHeld #
Is this mouse button down now? A state rather than an edge, tracked across frames from the button events -- so unlike the key case there is no separate query to reach for.
method : public : MouseHeld(button:Int) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| button | Int | 1 left, 2 middle, 3 right |
Return
| Type | Description |
|---|---|
| Bool | true while it is held |
MousePressed #
Did this mouse button go down during the last PollEvents?
method : public : MousePressed(button:Int) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| button | Int | 1 left, 2 middle, 3 right |
Return
| Type | Description |
|---|---|
| Bool | true if it was pressed this frame |
MouseReleased #
Did this mouse button come up during the last PollEvents?
method : public : MouseReleased(button:Int) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| button | Int | 1 left, 2 middle, 3 right |
Return
| Type | Description |
|---|---|
| Bool | true if it was released this frame |
New # constructor
Open a visible window and a GL 3.3 core context.
New(title:String, width:Int, height:Int)Parameters
| Name | Type | Description |
|---|---|---|
| title | String | the window title |
| width | Int | width in points |
| height | Int | height in points |
New # constructor
Open a window with multisampling. Antialiasing at the framebuffer, which costs nothing in the program: it is two pixel-format attributes, and every edge in the scene stops being a staircase. It has to be asked for BEFORE the window exists, which is why it is a constructor argument rather than a setter -- SDL attributes select a pixel format and setting one afterwards is silently ignored. Ask for 4. If the driver cannot provide it the window still opens, without it -- so check GetSamples if you care, rather than assuming.
New(title:String, width:Int, height:Int, samples:Int)Parameters
| Name | Type | Description |
|---|---|---|
| title | String | the window title |
| width | Int | width in points |
| height | Int | height in points |
| samples | Int | 0 for none, or 2, 4, 8; anything else is clamped |
New # constructor
Open a window with sound. Game.OpenGL has no audio of its own and does not need any: Game.SDL2's Mixer, MixChunk and MixMusic need no SDL_Renderer, so a 3D program can use them directly. What was missing was the initialisation -- nothing opened the audio device, and nothing said so, which left a working mixer looking like an absent feature. This asks for the audio subsystem alongside video and opens the device. After it, Game.SDL2's mixer is usable as-is: window := GLWindow->New("game", 1024, 640, 4, true); chime := MixChunk->New("pickup.wav"); chime->PlayChannel(-1, 0); Free the chunks before the window: GLWindow->Free calls Core->Quit, which takes the audio device with it.
New(title:String, width:Int, height:Int, samples:Int, audio:Bool)Parameters
| Name | Type | Description |
|---|---|---|
| title | String | the window title |
| width | Int | width in points |
| height | Int | height in points |
| samples | Int | multisample count, 0 for none |
| audio | Bool | true to open the audio device |
New # constructor
Open a window that may be hidden. A hidden window still has a real context and still renders, so this is how a test or a headless tool draws and reads pixels back without a window being flashed at anyone.
New(title:String, width:Int, height:Int, visible:Bool)Parameters
| Name | Type | Description |
|---|---|---|
| title | String | the window title |
| width | Int | width in points |
| height | Int | height in points |
| visible | Bool | false to keep the window off screen |
OpenPad #
Adopt the first attached controller, if there is one and we have none. Cheap when nothing is plugged in -- one call for the count and then nothing. Called at startup and again whenever SDL reports a device arriving, rather than every frame.
method : private : OpenPad() ~ NilOwn #
Hand something to the window to release on the way out. Every example in this repo hand-writes the same teardown: a chain of `if(x <> Nil) { x->Free(); }` in a fixed order, up to fifteen objects long, ending with the window. It is mechanical, it is easy to get one line wrong, and the one rule that matters -- everything GL before the context goes -- is invisible in it. Own returns what it was given, so it wraps a constructor (see the example) and Close() disappears. There is no ordering to get right WITHIN the owned set: glDelete* is order-independent and nothing here references another object, so one list is enough. What matters is that all of it happens before the context is destroyed, which is what this guarantees. Owning something does NOT stop you freeing it yourself -- every Free in this bundle nulls its handle and is safe to call twice, so Own can be adopted a line at a time. Do not Own a borrowed texture from RenderTarget->GetTexture or ShadowMap->GetTexture: those are views of an attachment their target still owns. Freeing one is already a no-op, so this is a waste rather than a hazard, but it says nothing true about ownership.
method : public : Own(thing:Freeable) ~ FreeableParameters
| Name | Type | Description |
|---|---|---|
| thing | Freeable | anything with a Free |
Return
| Type | Description |
|---|---|
| Freeable | the same thing, so this can wrap a constructor |
Example
@shader := window->Own(Shader->LitTextured())->As(Shader);
@cube := window->Own(Mesh->Cube())->As(Mesh);PadHeld #
Is a controller button down right now?
method : public : PadHeld(button:GameControllerButton) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| button | GameControllerButton | a GameControllerButton |
Return
| Type | Description |
|---|---|
| Bool | true while it is held |
PadPressed #
Did a controller button go down during the last PollEvents? The edge, not the level -- a menu that advances once per press wants this, and PadHeld would advance it every frame the button was down.
method : public : PadPressed(button:GameControllerButton) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| button | GameControllerButton | a GameControllerButton |
Return
| Type | Description |
|---|---|
| Bool | true on the frame it was pressed |
PadReleased #
Did a controller button come up during the last PollEvents?
method : public : PadReleased(button:GameControllerButton) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| button | GameControllerButton | a GameControllerButton |
Return
| Type | Description |
|---|---|
| Bool | true on the frame it was released |
PollPad #
Read the buttons and work out this frame's edges. Polled rather than event-driven, which is the same choice the keyboard makes: Event exposes GetJButton for raw joysticks but has no accessor for SDL's CONTROLLER button events, so the payload is unreachable from here. The event TYPE is reachable, which is enough to notice a device arriving or leaving -- so hot-plug is event-driven and state is polled. Costs nothing at all when no pad is attached, which is the common case.
method : private : PollPad() ~ NilRequestQuit #
Ask PollEvents to report a quit on the next call, e.g. from a menu.
method : public : RequestQuit() ~ NilSetDepthTest #
Turn depth testing on or off. On by default. On is the right default for 3D -- without it, whatever was drawn last is in front, regardless of where it is -- and the depth buffer is always requested, so enabling it can never silently do nothing the way it can when the SDL_GL_DEPTH_SIZE attribute was forgotten. The cost of that default: a frame cleared with GL_COLOR_BUFFER_BIT alone leaves stale depth values, and geometry then fails the test for no visible reason. BeginFrame clears both. Turn this off for a purely 2D pass rather than clearing colour alone.
method : public : SetDepthTest(enabled:Bool) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| enabled | Bool | false to draw in submission order |
SetPadDeadzone #
How far a stick must move before it counts, as a fraction of full travel. 0.15 by default. Raise it for a worn pad that drifts; 0 turns it off and makes GetPadAxis the same as GetPadAxisRaw.
method : public : SetPadDeadzone(fraction:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| fraction | Float | 0.0 to 1.0; values outside are clamped |
SetQuitOnEscape #
Whether Escape quits. On by default, which is what every example wants. Turn it off for a program where Escape should close a menu instead -- until now that meant abandoning PollEvents entirely and re-implementing quit, resize and mouse delta by hand, because Escape-quits was not optional.
method : public : SetQuitOnEscape(on:Bool) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| on | Bool | false to leave Escape to the program |
SetTargetFps #
The frame rate EndFrame paces to when there is no vsync. Ignored while vsync is on, since the swap does the waiting.
method : public : SetTargetFps(fps:Int) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| fps | Int | frames per second; values below 1 are ignored |
WasPressed #
Did this key go down during the last PollEvents? An EDGE, not a state: true for exactly one frame per physical press, and auto-repeat does not count. For "is it held right now", read Keyboard->GetState, which this does not disturb.
method : public : WasPressed(scancode:Int) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| scancode | Int | a Scancode value |
Return
| Type | Description |
|---|---|
| Bool | true if it was pressed this frame |