GL
June 2, 2024 ยท View on GitHub
A thin wrapper over OpenGL 4.6 with the bindless textures extension, plus some helpers to manage a context/GLFW window.
Important note, all counting and indices in GL functions are 1-based, to stay consistent with the rest of Julia. They are converted to 0-based under the hood. For example, if doing instanced rendering with N instances, you want to pass the instancing range IntervalU(min=1, max=N), not IntervalU(min=0, max=N-1). Of course, GPU-side data such as a mesh's index buffer is still 0-based, so be careful when generating mesh data or indirect drawing data.
Context
An OpenGL rendering context is a thread-local singleton, and is attached to a specific window. This is encapsulated in the mutable struct Context. Within your application code, you can retrieve the context for the current thread with get_context().
Contexts manage several important things:
- A reference to the
GLFW.Windowand its current vsync setting. - The current render state, in the field
state::RenderState.- For convenience, all the fields in
RenderStateare exposed as properties ofContext. For example, instead of doingget_context().state.viewport, you can just doget_context().viewport.
- For convenience, all the fields in
- Which of your requested OpenGL extensions was actually available (by default, a few bindless-texture extensions are requested)
- Call
extension_supported("GL_ARB_my_extension_name")to check
- Call
- Any services that are attached to this context (see Services).
- The specific GPU device limits as reported through OpenGL, in the field
device::Device.- Some of these statistics are not always reported reliably by every driver, so beware when using them.
- The currently-bound resources -- shader, mesh, UBO slots, SSBO slots.
- GLFW callbacks. The core GLFW library only allows one subscriber per event; Context allows you to add as many as you want.
Initialization
Managing the life of a Context by yourself is not easy, due to Julia's threading model. For simple use-cases, you should just use the @game_loop macro to run all your code within the life of a Context.
If @game_loop doesn't provide enough flexibility, you can use the lower-level function bp_gl_context() to construct the context, run your custom application code, and then clean up the context automatically at the end. This function takes your code as the first parameter, which is an extremely common Julia idiom -- see the official docs about do blocks. The rest of the parameters go straight into the Context constructor, so refer to that for information on them.
Render State
VSync (including adaptive sync) is normally managed by GLFW. In B+ it's represented by the VsyncModes enum. You can set it on Context creation. You can also change it afterwards with set_vsync(mode::E_VsyncModes). You can get the current value with get_context().vsync.
You can easily get the GLFW window's size with get_window_size().
The render state managed by the Context is specified by the RenderState struct. For any given piece of state defined in this struct, you can interact with it through the Context in several ways:
- Get its current value using properties:
get_context().cull_mode - Set its current value using properties:
get_context().cull_mode = FaceCullModes.off - Sets its current value using a more traditional function:
set_culling(FaceCullModes.off). - Temporarily set its value, run some code, then set it back:
with_culling(FaceCullModes.off) do ... end.
The following render state data is available:
color_write_mask::vRGBA{Bool}toggles the ability of shaders to write to each individual color channel.set_color_writes(...)is the function.with_color_writes(...)is the wrapper.
depth_write_mask::Booltoggles the ability of shaders to write to the depth buffer at all.set_depth_writes(...)is the function.with_depth_writes(...)is the wrapper.
cull_mode::E_FaceCullModeschanges back-face vs front-face culling.set_cull(...)is the function.with_culling(...)is the wrapper.
viewport::Box2Dicontrols the range of pixels that can be rendered to.- More precisely, it maps the NDC output from a vertex shader (positions in the range -1 to +1 along each axis, written to
gl_Position), to the given range of pixels. set_viewport(...)is the function.with_viewport(...)is the wrapper.
- More precisely, it maps the NDC output from a vertex shader (positions in the range -1 to +1 along each axis, written to
scissor::Optional{Box2Di}limits the range of pixels that can be affected by draw calls. Pixels outside this box are not affected.- Unlike
viewport, this setting does not change the coordinate math in mapping triangles to screen space. It just hides pixels. set_scissor(...)is the function.with_scissor(...)is the wrapper.
- Unlike
blend_mode::@NamedTuple{rgb::BlendStateRGB, alpha::BlendStateAlpha}controls how newly-drawn pixels are added to the color already in the render target or framebuffer.- See the
BlendState_struct for info on how to make custom blend modes. There are some presets, for example:- Opaque blending of color with
make_blend_opaque(BlendStateRGB) - Additive blending of alpha with
make_blend_additive(BlendStateAlpha) - Alpha blending of all 4 channels with
make_blend_alpha(BlendStateRGBA)
- Opaque blending of color with
set_blending(...)is the function.with_blending(...)is the wrapper.
- See the
depth_test::E_ValueTestscontrols the test used against the depth buffer to draw or occlude pixels.set_depth_test(...)is the function.with_depth_test(...)is the wrapper.
stencil_test::Union{StencilTest, @NamedTuple{front::StencilTest, back::StencilTest}}controls the test used against the stencil buffer to draw or discard pixels. You may optionally use a different test for front-facing geometry vs back-facing geometry.set_stencil_test(...),set_stencil_test_front(...), andset_stencil_test_back(...)are the functions.with_stencil_test(...),with_stencil_test_front(...), andwith_stencil_test_back(...)are the wrappers.
stencil_result::Union{StencilResult, @NamedTuple{front::StencilResult, back::StencilResult}}controls the effect of a pixel on the stencil buffer. You may optionally use a different test for front-facing vs back-facing geometry.set_stencil_result(...),set_stencil_result_front(...), andset_stencil_result_back(...)are the functions.with_stencil_result(...),with_stencil_result_front(...), andwith_stencil_result_back(...)are the wrappers.
stencil_write_mask::Union{GLuint, @NamedTuple{front::GLuint, back::GLuint}}controls which bits of the stencil buffer can be affected by newly-drawn pixels. You may optionally use a different mask for front-facing vs back-facing geometry.set_stencil_write_mask(...),set_stencil_write_mask_front(...), andset_stencil_write_mask_back(...)are the functions.with_stencil_write_mask(...),with_stencil_write_mask_front(...), andwith_stencil_write_mask_back(...)are the functions.
render_state::RenderStatecontrols the entire state at once. This is not more or less efficient than individually setting the states you care about; just different.set_render_state(...)is the function.with_render_state(...)is the wrapper.
Refreshing the context
If external code makes OpenGL calls and modifies the driver state, you can call refresh(context) to force the Context to acknowledge these changes. This could be a very slow operation, so avoid using if at all possible!
Resources
For low-level OpenGL work (which you usually don't need to do), OpenGL handle typedefs are provided in GL/handles.jl. These handles are all prefixed with Ptr_. For example, Ptr_Program, Ptr_Texture, Ptr_Mesh. These types aren't exported by default.
Managed OpenGL objects are instances of AbstractResource. All resource types implement the following interface (see doc-strings for more details):
get_ogl_handle(resource)is_destroyed(resource)::BoolBase.close(resource)to clean it up
Resources cannot be copied and their fields cannot be directly set; instead you should use whatever interface that resource provides.
For more info on each type of resource, see this document. The types of resources are as follows:
- Program : A compiled shader
- Texture : A GPU texture (1D, 2D, 3D, cubemap)
- Buffer : A GPU-side array of data
- Mesh : What OpenGL calls a "Vertex Array Object".
- Target : A render target, or what OpenGL calls a "FrameBuffer Object".
- TargetBuffer : Used internally when you don't need part of a Target to be sampleable (e.x. you need a depth buffer for depth-testing but not for custom effects).
Sync
- To make sure that a specific future action can see the results of previous actions (for example, sampling from a texture after writing to it with a compute shader), call
gl_catch_up_before().- This is only for "incoherent" actions, which OpenGL can't predict, as opposed to coherent actions like rendering into a target.
- The variant
gl_catch_up_renders_before()is potentially more efficient when you are specifically considering actions taken in fragment shaders, concerning only a subset of the Target that you just rendered to.
- To force all submitted OpenGL commands to finish right now, call
gl_execute_everything(). This isn't very useful, except in a few scenarios:- You are sharing resources with another context (not natively supported in B+ anyway), to ensure the shared resources are done being written to by the source context.
- You are tracking down a driver bug, and want to call this after every draw call to track down the one that crashes.
- If you are ping-ponging within a single texture, by reading from and rendering to separate regions, you should call
gl_flush_texture_writes_in_place()before reading from the region you just wrote to.
Services
Sometimes you want your own little singleton associated with a window or rendering context, providing easy access to utilities and resources. These singletons are called "Services". You can define a new service using the @bp_service() macro. Its doc-string describes how to make and use it.
The following servces are built into B+:
Input, from the *BplusApp.Input module, provides a high-level view into GLFW keyboard, mouse, and gamepad inputs. Automatically managed for you if you use the@game_loopmacro.GUI, from the *BplusApp.GUI module, takes care of Dear ImGUI integration. Automatically managed for you if you use the@game_loopmacro.BasicGraphics, from the *BplusApp module, provides simple shaders and meshes. Automatically managed for you if you use the@game_loopmacro.SamplerProvideris an internal service that allocates sampler objects as needed. Separate textures with the same sampler settings will reference the same sampler.ViewDebugginghelps catch some uses of bindless texture handles without properly activating them. It's only enabled if the BplusApp.GL module is in debug mode, or the GL Context is started in debug mode.
Clearing
The screen can be cleared with clear_screen(values...):
- To clear color, pass a 4D vector of component type
GLfloat(you can use the aliasvRGBAf).- If the back buffer is somehow in a non-normalized integer format, provide a component type of either
GLint(vRGBAi) orGLuint(vRGBAu) as appropriate.
- If the back buffer is somehow in a non-normalized integer format, provide a component type of either
- To clear depth, pass a single
GLfloat(a.k.a.Float32). - To clear stencil, pass a single
GLuint(a.k.a.UInt32). - To clear a hybrid depth-stencil buffer, pass two arguments: the depth as
GLfloatand the stencil asGLuint.
Drawing
Draw a Mesh resource m, using a Program resource p, by calling:
render_mesh(m, p; args...)
There are numerous named parameters to customize the draw call. Important note, as with the rest of the BplusApp.GL module, all counting and indices in these paramters are 1-based, to stay consistent with the rest of Julia. They are converted to 0-based under the hood. For example, if doing instanced rendering with N instances, you want to pass the range IntervalU(min=1, size=N), not IntervalU(min=0, size=N).
shape::E_PrimitiveTypeschanges the type of primitive (line strip, triangle list, etc). By default, this information is taken from theMeshbeing drawn.indexed_params::Optional{DrawIndexed}controls indexed drawing. By default, if theMeshhas indices, it will draw in indexed mode using all availble indices.elements::Union{IntervalU, Vector{IntervalU}, ConstVector{IntervalU}}controls which parts of the mesh are drawn. By default, the mesh's entire available data is drawn. You can pass a different contiguous range of elements to darw with. You can also do "multidraw", by providing a list of contiguous ranges.instances::Optional{IntervalU}allows you to optionally do instanced rendering, where the the mesh is re-drawnNtimes, providing a different "instance index" to the shader each time. Note that you can pass any contiguous interval of instances; it doesn't have to start at 1.known_vertex_range::Optional{IntervalU}can provide the driver with an optimization hint about the range of vertices being referenced by the indices you are drawing. This is mainly for if you are using indexed rendering and passing a custom range forelements.
Important note: Three of the parameters above are mutually-exclusive: multi-draw with elements, instancing with instances, and the optimization hint known_vertex_range. If you attempt to use more than one at once, you will get an error.
NOTE: There is one small, esoteric OpenGL feature that is not included, because adding it would probably require separating this function into 3 different overloads: indexed multi-draw is allowed to use different index offsets for each subset of elements.
NOTE: Indirect draw calls are not yet implemented. If you dispatch them yourself, remember to leave the OpenGL state unchanged, or else refresh the Context with refresh(get_context()).
Compute Dispatch
Dispatch a compute shader with dispatch_compute_threads() or dispatch_compute_groups().
Debugging
For now, B+ uses the older form of OpenGL error catching. Call pull_gl_logs() to get a list, in chronological order, of all messages/errors that have occurred since the last time you called pull_gl_logs(). This list may drop messages if it's not checked for too long, so it's recommended to call it at least once a frame in debug builds of your game.
For convenience, you can use the macro @check_gl_logs(msg...) to pull log events and immediately print them along with the file and line number, and any information you provide.