From 79cb6805c4f378c78143e64e05f0ef32a00d0850 Mon Sep 17 00:00:00 2001 From: bloeys Date: Sun, 26 May 2024 15:03:00 +0400 Subject: [PATCH] Scalar uniform array in uniform buffers --- buffers/uniform_buffer.go | 104 ++++++++++++++++++++++++++++++++------ main.go | 46 +++++++++++++++++ 2 files changed, 135 insertions(+), 15 deletions(-) diff --git a/buffers/uniform_buffer.go b/buffers/uniform_buffer.go index 926292f..8cf451f 100755 --- a/buffers/uniform_buffer.go +++ b/buffers/uniform_buffer.go @@ -13,12 +13,18 @@ import ( type UniformBufferFieldInput struct { Id uint16 Type ElementType + // Count should be set in case this field is an array of type `[Count]Type`. + // Count=0 is valid and is equivalent to Count=1, which means the type is NOT an array, but a single field. + Count uint16 } type UniformBufferField struct { Id uint16 AlignedOffset uint16 - Type ElementType + // Count should be set in case this field is an array of type `[Count]Type`. + // Count=0 is valid and is equivalent to Count=1, which means the type is NOT an array, but a single field. + Count uint16 + Type ElementType } type UniformBuffer struct { @@ -49,6 +55,9 @@ func (ub *UniformBuffer) addFields(fields []UniformBufferFieldInput) (totalSize for i := 0; i < len(fields); i++ { f := fields[i] + if f.Count == 0 { + f.Count = 1 + } existingFieldType, ok := fieldIdToTypeMap[f.Id] assert.T(!ok, "Uniform buffer field id is reused within the same uniform buffer. FieldId=%d was first used on a field with type=%s and then used on a different field with type=%s\n", f.Id, existingFieldType.String(), f.Type.String()) @@ -61,17 +70,23 @@ func (ub *UniformBuffer) addFields(fields []UniformBufferFieldInput) (totalSize // // To get the nearest boundary larger than the offset we can: // offset + (boundary - alignErr) == 100 + (16 - 4) == 112; 112 % 16 == 0, meaning its a boundary - alignmentBoundary := f.Type.GlStd140AlignmentBoundary() + // + // Note that arrays of scalars/vectors are always aligned to 16 bytes, like a vec4 + var alignmentBoundary uint16 = 16 + if f.Count == 1 { + alignmentBoundary = f.Type.GlStd140AlignmentBoundary() + } + alignmentError := alignedOffset % alignmentBoundary if alignmentError != 0 { alignedOffset += alignmentBoundary - alignmentError } - newField := UniformBufferField{Id: f.Id, Type: f.Type, AlignedOffset: alignedOffset} + newField := UniformBufferField{Id: f.Id, Type: f.Type, AlignedOffset: alignedOffset, Count: f.Count} ub.Fields = append(ub.Fields, newField) // Prepare aligned offset for the next field - alignedOffset = newField.AlignedOffset + uint16(f.Type.GlStd140BaseAlignment()) + alignedOffset = newField.AlignedOffset + alignmentBoundary*f.Count } return uint32(alignedOffset) @@ -166,37 +181,62 @@ func (ub *UniformBuffer) SetStruct(inputStruct any) { ubField := &ub.Fields[i] valField := structVal.Field(i) - if valField.Kind() == reflect.Pointer { + kind := valField.Kind() + if kind == reflect.Pointer { valField = valField.Elem() } + var elementType reflect.Type + isArray := kind == reflect.Slice || kind == reflect.Array + if isArray { + elementType = valField.Type().Elem() + } else { + elementType = valField.Type() + } + + if isArray { + assert.T(valField.Len() == int(ubField.Count), "ubo field of id=%d is an array/slice field of length=%d but got input of length=%d\n", ubField.Id, ubField.Count, valField.Len()) + } + typeMatches := false writeIndex = int(ubField.AlignedOffset) switch ubField.Type { case DataTypeUint32: - t := valField.Type() - typeMatches = t.Name() == "uint32" + typeMatches = elementType.Name() == "uint32" if typeMatches { - Write32BitIntegerToByteBuf(buf, &writeIndex, uint32(valField.Uint())) + + if isArray { + Write32BitIntegerSliceToByteBufWithAlignment(buf, &writeIndex, 16, valField.Slice(0, valField.Len()).Interface().([]uint32)) + } else { + Write32BitIntegerToByteBuf(buf, &writeIndex, uint32(valField.Uint())) + } } case DataTypeFloat32: - t := valField.Type() - typeMatches = t.Name() == "float32" + typeMatches = elementType.Name() == "float32" if typeMatches { - WriteF32ToByteBuf(buf, &writeIndex, float32(valField.Float())) + + if isArray { + WriteF32SliceToByteBufWithAlignment(buf, &writeIndex, 16, valField.Slice(0, valField.Len()).Interface().([]float32)) + } else { + WriteF32ToByteBuf(buf, &writeIndex, float32(valField.Float())) + } } case DataTypeInt32: - t := valField.Type() - typeMatches = t.Name() == "int32" + typeMatches = elementType.Name() == "int32" if typeMatches { - Write32BitIntegerToByteBuf(buf, &writeIndex, uint32(valField.Int())) + + if isArray { + Write32BitIntegerSliceToByteBufWithAlignment(buf, &writeIndex, 16, valField.Slice(0, valField.Len()).Interface().([]int32)) + } else { + Write32BitIntegerToByteBuf(buf, &writeIndex, uint32(valField.Int())) + } } case DataTypeVec2: @@ -282,6 +322,23 @@ func Write32BitIntegerToByteBuf[T uint32 | int32](buf []byte, startIndex *int, v *startIndex += 4 } +func Write32BitIntegerSliceToByteBufWithAlignment[T uint32 | int32](buf []byte, startIndex *int, alignmentPerField int, vals []T) { + + assert.T(*startIndex+len(vals)*alignmentPerField <= len(buf), "failed to write uint32/int32 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerField, *startIndex, len(buf), len(vals)*alignmentPerField) + + for i := 0; i < len(vals); i++ { + + val := vals[i] + + buf[*startIndex] = byte(val) + buf[*startIndex+1] = byte(val >> 8) + buf[*startIndex+2] = byte(val >> 16) + buf[*startIndex+3] = byte(val >> 24) + + *startIndex += alignmentPerField + } +} + func WriteF32ToByteBuf(buf []byte, startIndex *int, val float32) { assert.T(*startIndex+4 <= len(buf), "failed to write float32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d", *startIndex, len(buf)) @@ -298,7 +355,7 @@ func WriteF32ToByteBuf(buf []byte, startIndex *int, val float32) { func WriteF32SliceToByteBuf(buf []byte, startIndex *int, vals []float32) { - assert.T(*startIndex+4 <= len(buf), "failed to write slice of float32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", *startIndex, len(buf), len(vals)*4) + assert.T(*startIndex+len(vals)*4 <= len(buf), "failed to write slice of float32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", *startIndex, len(buf), len(vals)*4) for i := 0; i < len(vals); i++ { @@ -313,6 +370,23 @@ func WriteF32SliceToByteBuf(buf []byte, startIndex *int, vals []float32) { } } +func WriteF32SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerField int, vals []float32) { + + assert.T(*startIndex+len(vals)*alignmentPerField <= len(buf), "failed to write slice of float32 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerField, *startIndex, len(buf), len(vals)*alignmentPerField) + + for i := 0; i < len(vals); i++ { + + bits := math.Float32bits(vals[i]) + + buf[*startIndex] = byte(bits) + buf[*startIndex+1] = byte(bits >> 8) + buf[*startIndex+2] = byte(bits >> 16) + buf[*startIndex+3] = byte(bits >> 24) + + *startIndex += alignmentPerField + } +} + func ReflectValueMatchesUniformBufferField(v reflect.Value, ubField *UniformBufferField) bool { if v.Kind() == reflect.Pointer { diff --git a/main.go b/main.go index e12a5e0..e394f3f 100755 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "reflect" "runtime" "runtime/pprof" "strconv" @@ -676,6 +677,11 @@ func (g *Game) Init() { func testUbos() { + xx := []int{1, 2, 3, 4} + xx2 := [4]int{1, 2, 3, 4} + fmt.Printf("XX: %v; Kind: %v; Elem Type: %v\n", reflect.ValueOf(xx), reflect.ValueOf(xx).Type().Kind(), reflect.ValueOf(xx).Type().Elem().Kind()) + fmt.Printf("XX: %v; Kind: %v; Elem Type: %v\n", reflect.ValueOf(xx2), reflect.ValueOf(xx).Kind(), reflect.ValueOf(xx).Type().Elem().Kind()) + ubo := buffers.NewUniformBuffer([]buffers.UniformBufferFieldInput{ {Id: 0, Type: buffers.DataTypeFloat32}, // 04 00 {Id: 1, Type: buffers.DataTypeVec3}, // 16 16 @@ -728,6 +734,46 @@ func testUbos() { gl.GetBufferSubData(gl.UNIFORM_BUFFER, 48, 16, gl.Ptr(&m2.Data[0][0])) fmt.Printf("x=%f; x2=%f; v3=%s; m2=%s\n", x, x2, v.String(), m2.String()) + + // + // Ubo2 + // + type TestUBO2 struct { + F32 float32 + V3 gglm.Vec3 + F32Slice []float32 + I32 int32 + I32Slice []int32 + } + + s2 := TestUBO2{ + F32: 1.5, + V3: gglm.Vec3{Data: [3]float32{11, 22, 33}}, + F32Slice: []float32{-1, -2, -3, -4}, + I32: 55, + I32Slice: []int32{41, 42, 43, 44, 45}, + } + + ubo2 := buffers.NewUniformBuffer([]buffers.UniformBufferFieldInput{ + {Id: 0, Type: buffers.DataTypeFloat32}, + {Id: 1, Type: buffers.DataTypeVec3}, + {Id: 2, Type: buffers.DataTypeFloat32, Count: 4}, + {Id: 3, Type: buffers.DataTypeInt32}, + {Id: 4, Type: buffers.DataTypeInt32, Count: 5}, + }) + + ubo2.SetStruct(s2) + + var someInt32 int32 + fArr := [4 * 4]float32{} + i32Arr := [5 * 4]int32{} + gl.GetBufferSubData(gl.UNIFORM_BUFFER, 0, 4, gl.Ptr(&x)) + gl.GetBufferSubData(gl.UNIFORM_BUFFER, 16, 12, gl.Ptr(&v.Data[0])) + gl.GetBufferSubData(gl.UNIFORM_BUFFER, 32, 16*4, gl.Ptr(&fArr[0])) + gl.GetBufferSubData(gl.UNIFORM_BUFFER, 32+16*4, 4, gl.Ptr(&someInt32)) + gl.GetBufferSubData(gl.UNIFORM_BUFFER, 32+16*4+16, 16*5, gl.Ptr(&i32Arr[0])) + + fmt.Printf("f32=%f; v3=%s; f32Slice=%v; i32=%d; i32Arr=%v\n", x, v.String(), fArr, someInt32, i32Arr) } func (g *Game) initFbos() {