Skip to content

Add rlgl vertex draw bindings#537

Merged
gen2brain merged 13 commits intogen2brain:masterfrom
TyGrze:rlgl-vertex-draw-bindings
Feb 17, 2026
Merged

Add rlgl vertex draw bindings#537
gen2brain merged 13 commits intogen2brain:masterfrom
TyGrze:rlgl-vertex-draw-bindings

Conversation

@TyGrze
Copy link
Contributor

@TyGrze TyGrze commented Feb 12, 2026

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, DrawVertexArrayElementsInstanced but 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 project

Example

OS Arch Linux (rolling)
Kernel 6.18.6-arch1-1
Arch x86_64
CPU AMD Ryzen AI 7 350
GPU AMD Radeon 860M
Go 1.25.6 (linux/amd64)
image

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).
@gethiox
Copy link
Contributor

gethiox commented Feb 12, 2026

Pretty neat feature addition.
The only thing that bothers me is exposed unsafe.Pointer in its api.

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 buffer argument as []float32? Preferably size int32 argument should be calculated in that function as well and removed from a function signature.

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 size argument is being effectively calculated as len(quadPositions)*4? quadPositions is a slice of 12 defined elements but for some reason api expect size = 48 for this buffer, is this correct?

I may don't know what I say, so from the perspective of raylib-go library user, that's why I think size should be handled internally.
Also, don't mind me, I'm just a tourist that for the time keeps an eye on this repository 👀

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 12, 2026

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.
However, I haven't looked into Go's implementation of generics, so I went with the simple solution of handing it a pointer to the data and telling it how long it is. (And this just directly matches how the C code works in rlgl)

	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)

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 12, 2026

I also thought about just having it be a byte[], but the whole reason for me wanting to make the OpenGL calls myself is because currently when rendering 400K entities I am still CPU bound.

60% of my CPU time is going into Raylib's DrawMeshInstanced function which is just converting from one type to another. In this case, a Matrix4x4 to floats.

image

And the other major part (20%) is the malloc and dealloc calls due to DrawMeshInstanced creating the VBO on the CPU side every time it is called. I want to be able to reuse the VBO in my renderer and just update it, to hopefully no longer be CPU bound. Thats 80% of my CPU time on things I can optimize out fairly simply by just managing the VBOs myself.

Ultimately my thought is, if someone is wanting to make direct OpenGL calls, it's most likely because they want complete control of the communication between the CPU and GPU, and any other solution just doesn't make sense. (With the exception of using generics, but again I dont know anything about Golangs generics and I dont know what kind of performance hit it will have as well)

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 12, 2026

@gethiox Looks like you know a thing or two about Go generics https://github.com/gethiox/rotational-velocidensity-buffer?tab=readme-ov-file
Is this something you think is possible here? (AKA using generics to remove the unsafe pointer and size parameter from the API

@gethiox
Copy link
Contributor

gethiox commented Feb 12, 2026

Thanks for the explanation, I roughly got an idea what's going on.

So for the Float32[] you copied I had to multiple by 4 as float 32's are 4 byts long.

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}})
}
size: 4, value: []float32{1, 2, 3, 4}
size: 2, value: []uint16{0x1, 0x2, 0x3, 0x4}
size: 1, value: []byte{0x1, 0x2, 0x3, 0x4}
size: 8, value: []rl.Vector2{rl.Vector2{X:1, Y:2}}
size: 12, value: []rl.Vector3{rl.Vector3{X:1, Y:2, Z:3}}
size: 16, value: []rl.Vector4{rl.Vector4{X:1, Y:2, Z:3, W:4}}

The downside is you have to specify a concrete list of accepted types.

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.

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 12, 2026

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,
		})

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 12, 2026

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...

@gethiox
Copy link
Contributor

gethiox commented Feb 12, 2026

T type could be any

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,
	}})
}
size: 64, value: []rl.Matrix{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 rlLoadVertexBuffer expects a certain data structure according to rlSetVertexAttribute calls, Golang's types may be the correct size for things like float32 where size is guaranteed to be the same across platforms. Sure, rl.Matrix could be treated as a sequence of 64 bytes (as shown above) but I'm just not sure the fields will be always in the correct positions, I have no idea how to guarantee that on the Go<>cgo boundary.

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 12, 2026

rlLoadVertexBuffer dosnt expect anything, its just a box to hold bits. But yes that is what rlSetVertexAttribute is used for, It says where in the chunk of data GLSL can find usable data from and associate them to GLSL data types
List of all GLSL data types here:
https://registry.khronos.org/OpenGL-Refpages/gl4/html/glVertexAttribPointer.xhtml

For the generic and loading the data into the VBO it looks like unsafe.Sizeof should be able to figure out its size including any padding
"For a struct, the size includes any padding introduced by field alignment."
https://pkg.go.dev/unsafe#Sizeof

And for dealing with padding unsafe.Offsetof should work according to the docs:
"Offsetof returns the offset within the struct of the field represented by x, which must be of the form structValue.field. In other words, it returns the number of bytes between the start of the struct and the start of the field. The return value of Offsetof is a Go constant if the type of the argument x does not have variable size."
https://pkg.go.dev/unsafe#Offsetof

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 stride parameter (which unsafe.Sizeof can do) and then telling it how far the offsite is for that specific data type using the pointer parameter (which unsafe.Offsetof can do)
I got this info from the OpenGL docs
https://registry.khronos.org/OpenGL-Refpages/gl4/html/glVertexAttribPointer.xhtml

Example

type 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 unsigned byte and float
So we might want to add all the other which can be found here
https://registry.khronos.org/OpenGL/api/GL/glcorearb.h

	// GL equivalent data types
	UnsignedByte = 0x1401 // GL_UNSIGNED_BYTE
	Float        = 0x1406 // GL_FLOAT

@gethiox
Copy link
Contributor

gethiox commented Feb 12, 2026

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 reflect.Ptr, reflect.Slice or reflect.String, so my concern may be invalid, sorry about that.

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.

@gethiox
Copy link
Contributor

gethiox commented Feb 12, 2026

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.

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 12, 2026

@gethiox Ya I like that generic implementation for the LoadVertexBuffer, Ill take a look at the code and see where we can use generics to make the API easier.

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
@BrownNPC
Copy link
Contributor

BrownNPC commented Feb 14, 2026

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
}

@BrownNPC
Copy link
Contributor

BrownNPC commented Feb 14, 2026

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.

@BrownNPC
Copy link
Contributor

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.

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.

@BrownNPC
Copy link
Contributor

BrownNPC commented Feb 14, 2026

Here is an example I came up with. I implemented SetVertexAttributes that auto binds vertex arrays:

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

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 14, 2026

@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.

@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 14, 2026

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?

@BrownNPC
Copy link
Contributor

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

@BrownNPC
Copy link
Contributor

@TyGrze I've opened the PR.
TyGrze#1

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).
@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 16, 2026

Merged in @BrownNPC's reflixtion wrapper for SetVertexAttribute

And created a instancing example to test the remaining API endpoints

@gen2brain I think this feature is done.

Example

image

@gethiox
Copy link
Contributor

gethiox commented Feb 16, 2026

I have two nitpicks: 👀

  • some variable types could be consolidated (stride int32, offset int32 -> stride, offset int32), existing style suggest that.
  • zero-value variable allocation for unsafe.Sizeof could be avoided where we reach for data[0] anyway:
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 len(data) == 0 check.

Besides of that it looks great.

Copy link
Contributor

@BrownNPC BrownNPC left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the unnecessary allocations. You should be able to merge these from github itself.

TyGrze and others added 2 commits February 16, 2026 17:05
Co-authored-by: Omer. <100426634+BrownNPC@users.noreply.github.com>
@TyGrze
Copy link
Contributor Author

TyGrze commented Feb 16, 2026

  • some variable types could be consolidated (stride int32, offset int32 -> stride, offset int32), existing style suggest that.

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>
@gethiox
Copy link
Contributor

gethiox commented Feb 16, 2026

I personaly dont like this style

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 {}

@TyGrze TyGrze requested a review from gethiox February 16, 2026 22:36
@gen2brain
Copy link
Owner

This is nice. Thank you all, I am merging this.

@gen2brain gen2brain merged commit 2c5f1b2 into gen2brain:master Feb 17, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants