Add rlgl vertex draw bindings#537
Conversation
Add Go bindings for rlgl vertex buffer management and draw array functions in both CGO and purego backends. New functions: - LoadVertexBuffer / LoadVertexBufferElement - UpdateVertexBuffer / UpdateVertexBufferElements - SetVertexAttribute / SetVertexAttributeDefault - DrawVertexArray / DrawVertexArrayElements - DrawVertexArrayInstanced / DrawVertexArrayElementsInstanced
Demonstrates low-level rlgl vertex buffer bindings with an animated quad (indexed draw) and a static triangle (non-indexed draw).
|
Pretty neat feature addition. func LoadVertexBuffer(buffer unsafe.Pointer, size int32, dynamic bool) uint32 {}I'm not familiar with the feature you provide, but is there anything that prevents from defining a I'm a bit confused by the example, here is a snippet that I want to refer to: quadPositions := []float32{
// x, y, z
150, 150, 0, // 0: top-left
450, 150, 0, // 1: top-right
450, 450, 0, // 2: bottom-right
150, 450, 0, // 3: bottom-left
}
quadPosVBO := rl.LoadVertexBuffer(unsafe.Pointer(&quadPositions[0]), int32(len(quadPositions)*4), false)Why does I may don't know what I say, so from the perspective of |
|
The reason for the multiplication is because the GPU/VBO needs to know the size of the data you're passing in bytes. This is because a VBO can hold any arbitrary type of data (it's just a buffer of bytes). So for the Float32[] you copied I had to multiple by 4 as float 32's are 4 byts long. So if you look a little lower in the code you will see that the Quads index buffer is using uint16 which are only 2 bytes long so I multiple the array length by 2 // CCW winding in NDC (with Y-flip ortho: reverse the original CW order)
quadIndices := []uint16{
0, 2, 1, // first triangle (CCW in NDC)
0, 3, 2, // second triangle (CCW in NDC)
}
// Index buffer (EBO)
quadIBO := rl.LoadVertexBufferElement(unsafe.Pointer(&quadIndices[0]), int32(len(quadIndices)*2), false)
defer rl.UnloadVertexBuffer(quadIBO)For example, in my C# OpenGL engine I made 6 years ago, I pass uints and custom types like Vector2d: https://github.com/TyGrze/GrzeEngine/blob/master/GrzeEngine/Engine/Render/VAOs/SpriteVAO.cs The C# OpenGL bindings I used back then used generics to handle arbitrary types. return uint32(C.rlLoadVertexBuffer(cbuffer, csize, cdynamic))I think it would make more sense from an API perspective to use generics and to not use unsafe pointers and programmatically figure out the size in bytes but unfortunately I just don't have the knowledge in Go to do that. (And dont even know if its possible as I hear Golangs generics are a bit weird, but IDK) |
|
@gethiox Looks like you know a thing or two about Go generics https://github.com/gethiox/rotational-velocidensity-buffer?tab=readme-ov-file |
|
Thanks for the explanation, I roughly got an idea what's going on.
Silly me, now it is obvious. Certainly accepting whatever sequence of bytes makes sense and maybe generics could be a nice solution, here is an example: type DataType interface {
float32 | uint16 | byte | rl.Vector2 | rl.Vector3 | rl.Vector4
}
func UpdateVertexBuffer[T DataType](data []T) {
var z T
typeSize := unsafe.Sizeof(z)
fmt.Printf("size: %d, value: %#v\n", typeSize, data)
}
func main() {
UpdateVertexBuffer([]float32{1, 2, 3, 4})
UpdateVertexBuffer([]uint16{1, 2, 3, 4})
UpdateVertexBuffer([]byte{1, 2, 3, 4})
UpdateVertexBuffer([]rl.Vector2{{1, 2}})
UpdateVertexBuffer([]rl.Vector3{{1, 2, 3}})
UpdateVertexBuffer([]rl.Vector4{{1, 2, 3, 4}})
}The downside is you have to specify a concrete list of accepted types. In the case of |
|
If you have specify all the types then it wont work as I am already planning to use custom structs to pass to my VBO so I can clean up this mess where im trying to encoded extra info in the rl struct of Matrix4x4 because rl.DrawMeshInstances only takes a mat4v4. AKA I want to just have a struct that holds cellsize, the vec2 Pos, and an int for sprite index, pack that up into a VBO and then unpack it on the shader side and compute the transform matrix on the GPU. Also this will reduce how much data im sending to the CPU by almost 75% // Direct matrix construction - equivalent to MatrixMultiply(Scale, Translate)
// but avoids 2 matrix builds + a full 4x4 multiply per entity.
*buf = append(*buf, rl.Matrix{
M0: CellSize,
M5: float32(sprite.Y*GridCols + sprite.X), // sprite index encoded for shader
M10: CellSize,
M12: pos.X + CellSize/2,
M14: pos.Y + CellSize/2,
M15: 1,
}) |
|
Oh I just read the last part of you post "In the case of rl.Vector2 | rl.Vector3 | rl.Vector4 types, I'm not sure if it is safe to pass it further, is data alignment and bit order guaranteed to be the same across different platform targets? I'm not even sure if is it a case for basic types." Thats a very good question that I do not know the answer to... |
|
func UpdateVertexBuffer[T any](data []T) {
var z T
typeSize := unsafe.Sizeof(z)
fmt.Printf("size: %d, value: %#v\n", typeSize, data)
}
func main() {
UpdateVertexBuffer([]rl.Matrix{{
M0: 0, M4: 4, M8: 8, M12: 12,
M1: 1, M5: 5, M9: 9, M13: 13,
M2: 2, M6: 6, M10: 10, M14: 14,
M3: 3, M7: 7, M11: 11, M15: 15,
}})
}But I assume |
|
For the generic and loading the data into the VBO it looks like And for dealing with padding So we could remove the unsafe pointer in the rlLoadVertexBuffer but the user will still have to use unsafe code when telling the GPU how it needs to interperate that buffer and tell the GPU that the data is not titly packed by using the Exampletype Vertex struct {
Pos [3]float32
ID uint8 // padding may be inserted after this
UV [2]float32
}
stride := int(unsafe.Sizeof(Vertex{}))
rl.RlSetVertexAttribute(0, 3, rl.Float, false, int32(stride), int(unsafe.Offsetof(Vertex{}.Pos)))
rl.RlSetVertexAttribute(1, 1, rl.UnsignedByte, false, int32(stride), int(unsafe.Offsetof(Vertex{}.ID)))
rl.RlSetVertexAttribute(2, 2, rl.Float, false, int32(stride), int(unsafe.Offsetof(Vertex{}.UV)))But this will need to be testing as i'm probably making some assumptions that I don't even know im making Note: Looks like rlgl go only has two constants for // GL equivalent data types
UnsignedByte = 0x1401 // GL_UNSIGNED_BYTE
Float = 0x1406 // GL_FLOAT |
|
From what I just read it seems Go preserves field layout, it was designed to interoperate with C, so it should be safe to use these types directly as long as these doesn't contain types such as Perhaps example implementation could look like: func LoadVertexBuffer[T any](buffer []T, dynamic bool) uint32 {
if len(buffer) == 0 {
return 0
}
var z T
cbuffer := unsafe.Pointer(&buffer[0])
csize := C.int(int(unsafe.Sizeof(z)) * len(buffer))
cdynamic := C.bool(dynamic)
return uint32(C.rlLoadVertexBufferElement(cbuffer, csize, cdynamic))
}@TyGrze Would it work for you? suggestion for a reflection check: typ := reflect.TypeOf(buffer[0])
if typ.Kind() == reflect.Ptr || typ.Kind() == reflect.Slice || typ.Kind() == reflect.String {
panic("LoadVertexBuffer: Type cannot contain pointers or variable sized data")
}but I don't think it is necessary. |
|
Well, this is going beyond my expertise, I would like to have an API that is as simple as possible but maybe that is just not the case for Vertex Buffer related things. |
|
@gethiox Ya I like that generic implementation for the But unfortunately, I think overall if we're getting into touching the VBOs, and VAOs ourselves we have to accept that we will be dealing with unsafe code and the programmer using the API needs to proceed with their own caution. But I wounder what @gen2brain thinks about all of this? |
Types can be found here: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glVertexAttribPointer.xhtml And here: https://registry.khronos.org/OpenGL/api/GL/glcorearb.h I didnt add GL_HALF_FLOAT, GL_FIXED, GL_INT_2_10_10_10_REV, GL_UNSIGNED_INT_2_10_10_10_REV and GL_UNSIGNED_INT_10F_11F_11F_REV Because I dont really know what they are
Changed function `LoadVertexBuffer` `LoadVertexBufferElements` `UpdateVertexBuffer` and `UpdateVertexBufferElements` to use generics instead of an unsafe pointer
|
You can add a field called structs.HostLayout to make your struct match the C abi. This will make sure that padding is handled properly by the compiler. https://pkg.go.dev/structs#HostLayout HostLayout marks a struct as using host memory layout. A struct with a field of type HostLayout will be laid out in memory according to host expectations, generally following the host's C ABI. HostLayout does not affect layout within any other struct-typed fields of the containing struct, nor does it affect layout of structs containing the struct marked as host layout. By convention, HostLayout should be used as the type of a field named "_", placed at the beginning of the struct type definition. the only change needed is type Person struct{
_ structs.HostLayout // makes sure struct matches C ABI layout.
Height float64
} |
|
Reflection can be used to make the API simple. On my potato i7 third gen laptop, checking fields is under 100 nanoseconds. it sounds like a lot but uploading data to the GPU takes multiple milliseconds You can interpret the attribute type using reflection aswell. package an_test
import (
"reflect"
"testing"
"unsafe"
)
// MyStruct is the struct we want to inspect
type MyStruct struct {
A int32
B [10]float64
C byte
}
// BenchmarkStructFieldInfo measures the performance of reflecting over struct fields
func BenchmarkStructFieldInfo(b *testing.B) {
var s MyStruct
t := reflect.TypeFor[MyStruct]()
// Reset timer to only measure the loop
for b.Loop() {
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
_ = f.Type.Size() // size of the field
_ = f.Offset // offset within the struct
}
_ = unsafe.Sizeof(s) // total struct size
}
}the offsets and their types can be cached inside a sync.Map. and can be accessed in O(1) time. This takes away the complexity away from the user into the library backend. |
I would say, expose the unsafe methods, but also provide generic/reflection based wrappers on top of them. for eg specifying multiple attributes. Let me see if I can come up with one as an example. |
|
Here is an example I came up with. I implemented type Vertex struct {
Pos rl.Vector3
Color rl.Color
}
// --- define vertices ---
vertices := []Vertex{
{Pos: rl.NewVector3(400, 100, 0), Color: rl.NewColor(255, 0, 0, 255)}, // top (red)
{Pos: rl.NewVector3(200, 500, 0), Color: rl.NewColor(0, 255, 0, 255)}, // left (green)
{Pos: rl.NewVector3(600, 500, 0), Color: rl.NewColor(0, 0, 255, 255)}, // right (blue)
}
// --- Create VAO + VBO ---
vao := rl.LoadVertexArray()
rl.EnableVertexArray(vao)
// Upload struct vertices
vbo := rl.LoadVertexBuffer(vertices, false)
defer rl.UnloadVertexBuffer(vbo)
SetVertexAttributes(vertices, []VertexAttributesConfig{
{Field: "Pos", Normalized: false},
{Field: "Color", Attribute: 3, Normalized: true},
})Implementation (expand)// VertexAttributesConfig is used by SetVertexAttributes to specify VAO bindings for a slice of structs.
type VertexAttributesConfig struct {
Field string // Name of the field in the struct
Attribute uint32 // OpenGL attribute index (layout location)
Normalized bool // Whether the attribute should be normalized
}
// vertexAttributesCacheData stores cached info for a struct type
type vertexAttributesCacheData struct {
compSize int32 // Number of components (e.g., 3 for vec3)
attrType int32 // OpenGL type (rl.Float, rl.UnsignedByte, etc.)
normalized bool // Whether the attribute is normalized
stride int32 // Byte stride between vertices
offset int32 // Byte offset of the field in the struct
attribute uint32 // VAO attribute index
attributes []VertexAttributesConfig // Original attribute configs
}
// Cache of computed vertex attribute data per struct type
var vertexAttributesCache = map[reflect.Type]vertexAttributesCacheData{}
// SetVertexAttributes can automatically define VAO bindings for a slice of structs, arrays, or supported primitives.
// NOTE: bind VAO and VBO before calling this.
func SetVertexAttributes[T any](vertices []T, attributes []VertexAttributesConfig) {
if len(vertices) == 0 {
return
}
// Get reflect.Type of the struct
t := reflect.TypeFor[T]()
// Check if we already cached the attribute setup for this type and same attribute config
if cache, isCached := vertexAttributesCache[t]; isCached && slices.Equal(cache.attributes, attributes) {
// Use cached OpenGL calls to set VAO attributes
rl.SetVertexAttribute(cache.attribute, cache.compSize, cache.attrType, cache.normalized, cache.stride, cache.offset)
rl.EnableVertexAttribute(cache.attribute)
return
}
// Compute stride (size of one vertex in bytes)
var zero T
stride := int32(unsafe.Sizeof(zero))
kind := t.Kind()
switch kind {
case reflect.Struct:
// Iterate over each attribute configuration
for _, attr := range attributes {
// Find the field by name
field, ok := t.FieldByName(attr.Field)
if !ok {
panic(fmt.Sprintf("struct %s does not have a field of the name %s", t.String(), attr.Field))
}
// Check if the field is a primitive type (float32, uint8, etc.)
attrType, isPrimitiveType := glType(field.Type.Kind())
if isPrimitiveType {
compSize := int32(1)
offset := int32(field.Offset)
// Cache and call OpenGL to define this vertex attribute
cache := vertexAttributesCacheData{
compSize: compSize,
attrType: attrType,
normalized: attr.Normalized,
stride: stride,
offset: offset,
attribute: attr.Attribute,
attributes: attributes,
}
vertexAttributesCache[t] = cache
rl.SetVertexAttribute(cache.attribute, cache.compSize, cache.attrType, cache.normalized, cache.stride, cache.offset)
rl.EnableVertexAttribute(cache.attribute)
continue
}
// Handle arrays or child structs
switch field.Type.Kind() {
case reflect.Array:
// Array of primitive types
elemKind := field.Type.Elem().Kind()
compSize := int32(field.Type.Len())
offset := int32(field.Offset)
attrType, isPrimitiveType := glType(elemKind)
if !isPrimitiveType {
panic(fmt.Sprint("Only array of primitive types is supported. Got ", elemKind.String(), " for field ", attr.Field))
}
// Cache and call OpenGL
cache := vertexAttributesCacheData{
compSize: compSize,
attrType: attrType,
normalized: attr.Normalized,
stride: stride,
offset: offset,
attribute: attr.Attribute,
attributes: attributes,
}
vertexAttributesCache[t] = cache
rl.SetVertexAttribute(cache.attribute, cache.compSize, cache.attrType, cache.normalized, cache.stride, cache.offset)
case reflect.Struct:
// Child struct: ensure all fields are the same primitive type
prevType := field.Type.Field(0).Type
attrType, isPrimitiveType := glType(prevType.Kind())
if !isPrimitiveType {
panic(fmt.Sprintf("child struct must have a primitive type for every field in %s %s", attr.Field, prevType.String()))
}
compSize := int32(field.Type.NumField())
offset := int32(field.Offset)
// Check that all child struct fields have the same type
for i := 1; i < field.Type.NumField(); i++ {
if prevType != field.Type.Field(i).Type {
panic(fmt.Sprintf("child struct must have the same type for every field in %s %s", attr.Field, field.Type.String()))
}
}
// Cache and call OpenGL
cache := vertexAttributesCacheData{
compSize: compSize,
attrType: attrType,
normalized: attr.Normalized,
stride: stride,
offset: offset,
attribute: attr.Attribute,
attributes: attributes,
}
vertexAttributesCache[t] = cache
rl.SetVertexAttribute(cache.attribute, cache.compSize, cache.attrType, cache.normalized, cache.stride, cache.offset)
rl.EnableVertexAttribute(cache.attribute)
}
}
}
}
func glType(k reflect.Kind) (t int32, ok bool) {
switch k {
case reflect.Int8:
return rl.Byte, true
case reflect.Uint8:
return rl.UnsignedByte, true
case reflect.Int16:
return rl.Short, true
case reflect.Uint16:
return rl.UnsignedShort, true
case reflect.Int32:
return rl.Int, true
case reflect.Uint32:
return rl.UnsignedInt, true
case reflect.Float32:
return rl.Float, true
case reflect.Float64:
return rl.Double, true
default:
return -1, false
}
}@TyGrze would you like it if I sent a pull request to your branch with this addition + example? The one here is missing a few things. mainly passing a slices of primitive arrays as vertices eg. [][4]float32 |
|
@BrownNPC I really like this solutions. It makes the API as simple as the C# OpenGL bindings example I gave earlier and still allows the user to use SetVertexAttribute if they want. Feel free to send a PR and I'll take a closer look and test it before merging into my PR. |
|
Looking at the cache, I see when you are going over the struct you are ranging over attributes which makes sense. case reflect.Struct:
// Iterate over each attribute configuration
for _, attr := range attributes {
...
rl.SetVertexAttribute(cache.attribute, cache.compSize, cache.attrType, cache.normalized, cache.stride, cache.offset)
rl.EnableVertexAttribute(cache.attribute)
}But in the cache check there is no loop // Check if we already cached the attribute setup for this type and same attribute config
if cache, isCached := vertexAttributesCache[t]; isCached && slices.Equal(cache.attributes, attributes) {
// Use cached OpenGL calls to set VAO attributes
rl.SetVertexAttribute(cache.attribute, cache.compSize, cache.attrType, cache.normalized, cache.stride, cache.offset)
rl.EnableVertexAttribute(cache.attribute)
return
}So does the cache only work for structs with a single attribute, or am I just missing something here? |
|
Yeah this code was incomplete when I wrote it, the array branch doesnt work properly either. I'll properly test it and then send a PR |
add SetVertexAttributes
This example demonstrates instanced drawing with low-level rlgl bindings: - LoadVertexBufferElements: upload index data (EBO) to GPU - DrawVertexArrayElementsInstanced: instanced indexed draw - SetVertexAttributeDivisor: per-instance attribute advancement - Camera3D with BeginMode3D/EndMode3D for orbital 3D camera control 625 quads are rendered in a 25x25 grid with animated Y-offsets (sine wave).
|
Merged in @BrownNPC's reflixtion wrapper for And created a instancing example to test the remaining API endpoints @gen2brain I think this feature is done. Example
|
|
I have two nitpicks: 👀
dataSize := int32(int(unsafe.Sizeof(data[0])) * len(data))
rlUpdateVertexBuffer(bufferId, unsafe.Pointer(&data[0]), dataSize, offset)It was defined by my daft but I didn't noticed it could be skipped entirely because of Besides of that it looks great. |
Co-authored-by: Omer. <100426634+BrownNPC@users.noreply.github.com>
I personaly dont like this style also you say existing style suggest that but looking at the functions right next to the ones I added they dont seem to do that // FramebufferAttach - Attach texture/renderbuffer to a framebuffer
func FramebufferAttach(fboId uint32, texId uint32, attachType int32, texType int32, mipLevel int32) {}
// LoadShaderCode - Load shader from code strings
func LoadShaderCode(vsCode string, fsCode string) uint32 {}
// LoadShaderProgram - Load custom shader program
func LoadShaderProgram(vShaderId uint32, fShaderId uint32) uint32 {}
// ComputeShaderDispatch - Dispatch compute shader (equivalent to *draw* for graphics pilepine)
func ComputeShaderDispatch(groupX uint32, groupY uint32, groupZ uint32) {} |
Co-authored-by: Sławomir Kur <gethiox@gmail.com>
Fair enough, I just saw* somewhere nearby that types were consolidated but it seems it is some mix of both, signatures are relatively not very long as they are now, t's not important anyway. *Probably just this one: 😂 func LoadTextureDepth(width, height int32, useRenderBuffer bool) uint32 {} |
|
This is nice. Thank you all, I am merging this. |


Summery
Add Go bindings for rlgl vertex buffer management functions (
LoadVertexBuffer,LoadVertexBufferElement,UpdateVertexBuffer,UpdateVertexBufferElements)Add Go bindings for vertex attribute configuration (
SetVertexAttribute,SetVertexAttributeDefault)Add Go bindings for draw array functions (
DrawVertexArray,DrawVertexArrayElements,DrawVertexArrayInstanced,DrawVertexArrayElementsInstanced)Implementations for both cgo and purego backends
-- Note: I dont actually know how purego works so just copied what I saw the other implementation looked like (someone else might want to take a look)
Include an example (
examples/others/rlgl_vertex_buffer) demonstrating indexed and non-indexed draws with animated vertex colors-- Note: It dosnt test the Instanced version of
DrawVertexArrayInstanced,DrawVertexArrayElementsInstancedbut I would be surprised if they don't work. Also I did this change to speed up my entity renderer to get over 400K entities on screen, so I'll be testing those when I implement this into that projectExample