gl
import gl from "gl";
import io from "io";
fn main() -> i32 {
var win = gl.open(800, 600, "triangle", gl.Options { major: 3; minor: 3; }) or {
io.println(err.desc());
return 1;
};
while (!win.closed()) {
while (true) {
let ev = win.poll() or { break; }; // null: nothing waiting
match (ev) {
gl.Event.Resize { width; height } => { gl.glViewport(0, 0, width as i32, height as i32); }
_ => {}
}
}
gl.glClearColor(0.1, 0.1, 0.12, 1.0);
gl.glClear(gl.GL_COLOR_BUFFER_BIT);
win.present();
}
win.close();
return 0;
}gl opens a window with an OpenGL core context and gives you GL itself:
every function under its Khronos name with gl.xml’s parameter names, so
docs.gl documents the tape call. What it adds on top is
small: the window and its events, shaders and programs that return the
compiler’s log as an error, check() for glGetError, and debug
messages. It works the same under tape run and in a built program.
gl runs on Windows (WGL) and Linux (GLX, on the X11 window, with the
system’s libGL.so.1: Mesa or the GPU vendor’s driver). On macOS, whose
OpenGL stops at 4.1 without compute, gl.open gives
GlError.Unsupported. It is a capability module: it opens
windows and talks to the driver.
The window
pub fn open(width: i64, height: i64, title: []const u8, opts: Options) -> Window or GlError;
pub struct Options {
major: i64 = 3;
minor: i64 = 3;
vsync: bool = true;
visible: bool = true; // false: a hidden window (tests, offscreen work)
depth_bits: i64 = 24;
stencil_bits: i64 = 8;
debug: bool = false; // a debug context, its messages through gl.messages()
}open gives a window with a core context of at least major.minor,
current on the calling thread, every function loaded, and the viewport
set to the whole window. One thread uses it: every gl call goes to the
context current on that thread.
Windows keeps a window within the desktop: one asked for larger than the
screen (a map’s full size on a laptop) opens smaller. Size what you draw
from win.size(), not from the numbers you asked for, and again on each
Resize.
| Method | Does |
|---|---|
win.poll() -> ?Event | One waiting event, null when none: drain it at the top of each frame |
win.wait() -> Event | Block until an event comes (a tool that redraws only on input) |
win.present() | Show what was drawn (swap buffers, waiting for the display when vsync is on) |
win.size() -> Size | The drawable area now, in pixels |
win.set_title(text) | The title bar’s text; any language’s letters show (frame times there are the cheapest profiler) |
win.show() | Show a window opened with visible: false |
win.focused() -> bool | The window is in front: the keyboard’s keys go to it |
win.closed() -> bool | The user closed it, or close() ran |
win.close() | Release the context and the window; twice is fine |
win.make_current() | Make its context current again, for a program with two windows |
Events
pub tagged Event {
Key { e: ui.KeyEvent; }
Pointer { e: ui.PointerEvent; }
Resize { width: i64; height: i64; }
Focus { focused: bool; }
Close;
}Keys and the pointer come as ui’s own events, filled the way a ui
window fills them: e.key == ui.Key.Escape, e.modifiers, clicks, the
wheel in e.wheel_x and e.wheel_y. Resize is yours to answer with
glViewport. Close comes once, when the user closes the window; it is
gone then, present() does nothing and closed() is true.
Focus { focused } comes when the window comes to the front (true) or
another window does (false), and win.focused() says which it is now.
A game that reads held keys with uwm.get_key_state asks focused()
first: the keyboard’s state is the same whichever window is in front, so
the camera would move while the player types elsewhere. One that tracks
held keys from Down and Up lets them all go on Focus { focused: false }: the Up of a key released in another window comes to that
window.
The events drive a ui.Screen as they are, so a ui tree can be the HUD
over a GL scene: Key { e } goes to screen.key(e.kind, e.keycode, e.codepoint, e.modifiers), a wheel to screen.scroll(e.x, e.y, e.wheel_x, e.wheel_y), any other pointer event to screen.hold(e.modifiers) then
screen.pointer(e.kind, e.x, e.y).
A gfx surface’s pixels, a Screen’s included, are premultiplied
0xAARRGGBB (gfx.Format.Argb): upload them as they are with
GL_BGRA, GL_UNSIGNED_BYTE and GL_UNPACK_ROW_LENGTH set to the
stride, and draw them with glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA).
A sprite atlas drawn with gfx, a decoded image or a HUD all go up
the same way.
poll also runs what your threads sent back, as ui’s loop does, so a
worker’s callback runs on the GL thread.
Errors
pub tagged GlError {
NoWindow;
NoPixelFormat;
NoContext { major: i64; minor: i64; got: string; }
Missing { names: string; }
Compile { kind: ShaderKind; log: string; }
Link { log: string; }
Gl { code: u32; name: string; }
Unsupported;
}err.desc() says it in a line: "compute shader: 0(4) : error C1503: undefined variable "v"", "OpenGL error GL_INVALID_ENUM (1280)".
NoContext carries what the driver gives instead (got: "OpenGL 3.1 (Intel HD Graphics 3000)"); Missing names the functions of your
version the driver lacks.
Shaders and programs
pub fn program(vert: []const u8, frag: []const u8) -> u32 or GlError;
pub fn compute(src: []const u8) -> u32 or GlError;
pub fn shader(kind: ShaderKind, src: []const u8) -> u32 or GlError;
pub fn link(shaders: []const u32) -> u32 or GlError;
pub fn shader_log(id: u32) -> string;
pub fn program_log(id: u32) -> string;Write GLSL in raw strings, or keep it in its own file: a plain literal
reads { as interpolation.
const VERT: string = r"""
#version 330 core
layout(location = 0) in vec2 pos;
void main() { gl_Position = vec4(pos, 0.0, 1.0); }
""";
const FRAG: string = @embed("./shaders/flat.frag"); // ./: beside this file
let prog = gl.program(VERT, FRAG) or {
match (err) {
gl.GlError.Compile { kind; log } => { io.println("the {kind.desc()} shader: {log}"); }
gl.GlError.Link { log } => { io.println("link: {log}"); }
_ => { io.println(err.desc()); }
}
return 1;
};
gl.glUseProgram(prog);programcompiles both shaders and links them,computeone compute shader (GL 4.3). Each deletes its shaders: you get the program only. The first failure is the error: the vertex shader, then the fragment shader, then the link.shadercompiles one;Compile’slogis the driver’s, with its line numbers.linklinks shaders you made, attaching and detaching them; they stay yours to delete (glDeleteShader).- A shader that compiles with warnings is not an error:
shader_log(id)andprogram_log(id)show them.
Compute
let prog = gl.compute(ADD_ONE) or { io.println(err.desc()); return 1; };
let buf = gl.gen_buffer();
gl.glBindBuffer(gl.GL_SHADER_STORAGE_BUFFER, buf);
gl.buffer_alloc(gl.GL_SHADER_STORAGE_BUFFER, n * 4, gl.GL_DYNAMIC_COPY);
gl.glBindBufferBase(gl.GL_SHADER_STORAGE_BUFFER, 0, buf); // binding = 0 in the shader
gl.glUseProgram(prog);
gl.glDispatchCompute(((n + 63) / 64) as u32, 1, 1); // local_size_x = 64
gl.glMemoryBarrier(gl.GL_BUFFER_UPDATE_BARRIER_BIT); // before reading it back
gl.glGetBufferSubData(gl.GL_SHADER_STORAGE_BUFFER, 0, out[0..n * 4]);Between two dispatches where the second reads what the first wrote, the
barrier is GL_SHADER_STORAGE_BARRIER_BIT; before drawing from the
buffer as vertices, GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT. Read back only
what you need: glGetBufferSubData waits for the GPU to finish.
ShaderKind is Vertex, Fragment, Geometry, Compute,
TessControl and TessEvaluation, with kind.desc() ("vertex", …).
Checking for errors
pub fn check() -> void or GlError;
pub fn messages() -> []const DebugMessage;
pub struct DebugMessage { source: u32; kind: u32; id: u32; severity: u32; text: string; }check() is glGetError as a value: Gl { code; name } for the first
error since the last check, the rest cleared. Calling it once a frame
finds a mistake within a frame:
gl.check() or { io.println(err.desc()); };With Options { debug: true } on GL 4.3 (or a driver with
GL_KHR_debug), the driver also explains: messages() gives what it
logged since the last call, oldest first, each message right after the
call that caused it. severity is GL_DEBUG_SEVERITY_HIGH, _MEDIUM,
_LOW or _NOTIFICATION. The slice is valid until the next call; without
a debug context it is empty.
for (m in gl.messages()) {
if (m.severity != gl.GL_DEBUG_SEVERITY_NOTIFICATION) { io.println(m.text); }
}The GL functions
The core functions up to GL 4.3 that a program draws and computes with (buffers, vertex arrays, shaders and uniforms, textures, framebuffers, instancing, compute, queries, debug output) are there under their Khronos names, with tape types where C has pointers:
- A buffer, a texture’s pixels and a readback are slices:
glBufferData(target, data: []const u8, usage),glTexImage2D(..., pixels: []const u8),glReadPixels(..., pixels: []u8),glGetBufferSubData(target, offset, out: []u8). Numbers become bytes without a copy throughbytes:bytes.of_f32(verts[0..n]), and for your own struct@tape { bytes.gen_of(Vertex); }makesof_Vertex.gl.buffer_alloc(target, size, usage)gives storage without data. glGenBuffers(ids: []u32)fills a slice; for one,gl.gen_buffer(),gen_texture(),gen_vertex_array(),gen_framebuffer(),gen_renderbuffer(),gen_query(), anddelete_buffer(id)and the rest.glClearBufferData(target, internalformat, format, kind, data)fills a whole buffer with one value without uploading it (zeroing counters each frame:gl.GL_R32UI, gl.GL_RED_INTEGER, gl.GL_UNSIGNED_INTand four zero bytes).- A query of one value returns it:
glGetShaderiv(shader, pname) -> i32,glGetQueryObjectui64v(query, pname) -> u64,glGetInteger(pname).glGetShaderInfoLogandglGetProgramInfoLogreturn strings,glMapBufferRangethe mapped bytes as a[]u8. - Names are slices:
glGetUniformLocation(program, "mvp"),glShaderSource(shader, source). - An offset into a bound buffer stays an
i64, as GL means it:glVertexAttribPointer(0, 2, gl.GL_FLOAT, false, 16, 0). - The types are gl.xml’s, and the constants are
u32: where gl.xml saysGLintfor an enumerant, the constant is cast. The two every program meets areglTexImage2D’s internalformat (gl.GL_RGBA8 as i32) andglTexParameteri’s value (gl.GL_NEAREST as i32).
Timing the GPU
A GL_TIME_ELAPSED query measures the GPU’s time between
glBeginQuery and glEndQuery, and
glGetQueryObjectui64v(query, gl.GL_QUERY_RESULT) gives it in
nanoseconds. Asking for a result in the frame that made it waits for the
GPU, so keep two queries and read last frame’s:
let q = queries[frame % 2];
gl.glBeginQuery(gl.GL_TIME_ELAPSED, q);
draw();
gl.glEndQuery(gl.GL_TIME_ELAPSED);
if (frame > 0) {
let ns = gl.glGetQueryObjectui64v(queries[(frame + 1) % 2], gl.GL_QUERY_RESULT);
win.set_title("gpu {ns / 1000} us");
}The constants are gl.GL_.... A function newer than your context traps
naming it (gl.glDispatchCompute: not loaded: this OpenGL context lacks it) instead of crashing; gl.has("GL_ARB_bindless_texture") asks for
an extension, and gl.version() and gl.renderer() say what you got.
Last modified: