Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gl

OpenGL 3.3 core bindings for Milo, plus a safe layer over them. No dependencies beyond the standard library.

milo add github.com/milo-language/milo-gl            # latest release
milo add github.com/milo-language/milo-gl@v0.2.0     # or pin a specific tag
from "gl" import { Gpu, Shader, Mesh }

var sh = Shader.compile(VERT, FRAG)!
var quad = Mesh.fullscreenQuad()
Gpu.clear(0.0, 0.0, 0.0, 1.0, true)
sh.bind()
sh.uniformF("time", t)
quad.draw()

gl owns the shader, buffer, texture and framebuffer lifecycles and keeps the raw pointers out of your program. Drop to gl/raw for an entry point it does not wrap.

The context owns its objects

Nothing here has a Drop, and that is not the usual Milo answer — Vec and string free themselves, and no other package makes you call anything.

GL is the exception because glDelete* requires the context that made the object to still be current, on the thread it was made on. A destructor is exactly the thing whose timing you do not control. Rust libraries solve this by giving every GL object an Arc<Context>, so the context is provably alive whenever an object drops. Milo cannot express that: references are second-class, so a struct can never store a &GlContext, and an object therefore cannot keep its context alive.

So ownership runs the other way. A GlContext records every name it hands out and deletes whatever is left, once, where you put the call:

var gl = GlContext.new()                              // after the window system's context
var tex = Texture2D.rgba8(gl, w, h, pixels, false)
var sh  = Shader.compile(gl, VERT, FRAG)!
// ... draw ...
gl.free()                                             // sweeps anything still outstanding

You can still free an object the moment you are done, and should for anything replaced mid-run — a texture swapped every time the scene changes should not wait for teardown. free takes the context so the name can be handed back:

let t = Texture2D.rgba8(gl, w, h, pixels, false)
t.free(gl)
t.bind(0)   // error: use of moved variable 't'

@noCopy is what makes that a compile error rather than a driver-level mystery, and it is why these types are not Copy — they are integers, and the all-fields-Copy rule would otherwise make free consume nothing.

The two checks cover different failures and neither subsumes the other. @noCopy catches use-after-free and double-free, which the context cannot see. The context catches forgetting, which @noCopy cannot see. GlContext.live() reports how many objects are outstanding, so a leak in a frame loop is a number you can assert on rather than a slow climb in a memory graph.

Uploads are bounds-checked too — the driver reads w * h elements off a pointer with no idea how long your Vec is, so a short one would be a heap over-read from a call with no unsafe at the call site.

Textures for 3D, not just for full-frame passes

The constructors all start clamped, unfiltered and mip-less, which is right for a texture that is a picture of the whole frame. A texture laid over 3D geometry wants the other three:

let ground = Texture2D.srgb8(w, h, bytes, true)
ground.setWrap(Wrap.Repeat)      // UV is world position over a period, not 0..1
ground.generateMipmaps()         // after the upload — it derives the chain from level 0
ground.setAnisotropy(16.0)       // no-op without GL_EXT_texture_filter_anisotropic

srgb8 takes three sRGB bytes per pixel — what a PNG decodes to — and the sampler decodes to linear in hardware, before filtering. Doing it afterwards in the shader is both slower and wrong: a bilinear tap averages four sRGB bytes, and the average of two sRGB values is not the sRGB of their linear average, so edges come out too dark. A lookup table per fetch has the same flaw and costs a dependent read.

Mipmaps are not optional for anything tiled across a 3D surface. Without them a distant pattern samples one texel out of the dozen the pixel covers, and which one changes as the camera moves — the ground crawls and glitters. Anisotropy then fixes what mips alone get wrong at a grazing angle, where trilinear picks one level from the pixel's widest axis and blurs the direction that was not compressed.

darwin and linux only

milo.json declares "targets": ["darwin", "linux"], and the compiler enforces it — building for Windows names the package rather than failing on a missing symbol. opengl32.dll exports GL 1.1 only, so every 3.3 entry point here would be undefined.

3.3 core is the floor on purpose: it is the highest version macOS ships, and old enough that every Mesa and every driver of the last decade has it.

A context is your job

Every call needs a current GL context, and creating one belongs to the window system, not here — the library deliberately depends on nothing but the standard library. With SDL2 that is SDL_GL_SetAttribute + SDL_WINDOW_OPENGL + SDL_GL_CreateContext, which the sdl package's sdl/gl module provides; examples/ and tests/ use it, and they carry that dependency in their own manifests so the published package does not. Calling into GL with no context bound is undefined behaviour, not an error return.

Verified bindings

Every declaration carries @cSig, so the signature is checked against the real GL header at build time on any machine that has one — including each pointer parameter's pointee width, which is what an out-param's contract actually is. A machine with neither OpenGL/gl3.h nor GL/glcorearb.h gets a named warning, not a silent pass.

License

MIT

About

OpenGL 3.3 core bindings and a safe layer, for Milo

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors