SDL3 GPU, SDL Shadercross, gLTF, and Beef
SDL3 is a rather new library and implements its GPU module, which makes it easier to build cross-platform 3D applications. Here we will…
SDL3 GPU, SDL Shadercross, gLTF, and Beef

SDL3 is a rather new library and implements its GPU module, which makes it easier to build cross-platform 3D applications. Here we will explore how to load a gLTF mesh using SDL3 GPU, something that, to my knowledge, has not been done before. It is not something very exotic magic, and looks very similar to well-known approaches. This article will also explore the Beef programming language.
Key takeaways
This is what you will learn in this article, regardless of whether you are interested in the Beef programming language or not:
· How to load and display a gLTF file using SDL3 GPU.
· How to compile SDL shadercross.
Of course, I will also write about challenges I encountered and how I solved them.
Table of contents
- Stuff that I already wrote before in this article
- Beef programming language
- Making a gLTF loader, in Beef
- How to compile Shadercross, unrelated to Beef
- gLTF and SDL3 GPU, how to render a gLTF file in Beef
- Future work
- Conclusions
Beef programming language
I was going to talk about C++, but I found this derives too much from the actual topic, so let’s keep this short! Simply put, the Beef programming language (henceforth referred to as “Beef”) is “what if the D programming language actually was good?” Now, Mister Beef himself would punch me hard for this description, for Beef bears little resemblance to the D programming language. If anything, Beef is more like C# and is inspired by some other languages, such as C, C++, Rust, Swift, and Go.
The key feature of Beef is that there is no garbage collection, no reference counting and no JIT runtime. Such features usually have some performance overhead, so using Beef in high-performance applications would be more beneficial, reducing the overhead. Moreover, Beef can directly call C AND C++ libraries, so it is less needed to make custom wrappers and such things. Finally, Beef has a debugger and memory leak checker, letting you find errors faster.
For game development, Beef can therefore pose as a viable alternative to other programming languages. It is still in beta, and those who are interested in checking out the language will find that using the latest nightly build would be preferable. This is because the nightly build has the latest features and bug fixes. In other words, it is a rapidly evolving language.
Making a gLTF loader
I don’t know how it goes, but read a text and you learn one thing, see an image and you learn two things, watch a video and learn three things, and do something yourself and learn thousands of things. I found that gLTF is a leading standard for loading 3D meshes, but some engines, for whatever reason, try to make their own mesh format. Usually, they have a converter that converts gLTF to their format! Looking at a gLTF file, and it turns out it is just a JSON file in a specified format, it has some weird numbers, but this excellent gLTF tutorial sheds some light on this: https://github.com/KhronosGroup/glTF-Tutorials/blob/main/gltfTutorial/README.md
I started out copying an Odin (another of those programming languages) gLTF implementation, and porting this line by line. Actually, it was kind of a waste of time, since parsing a gLTF file is very easy! It comes with one or several binary buffer files (the bin ones) that are the mech data in binary form. This is the tricky part, since Buffers refer to a specific part of these binary files, and you need to know what is vertex positions are, what is normal, UV coordinates, and so on.
Another issue is that buffer values can be in six different types: uint8, int8, uint16, int16, uint32, and float. Additionally, those values are packed as either scalars, Vector2, Vector3, Vector4 / Matrix2, Matrix3x3, and Matrix4x4. That’s a lot of combinations! Fortunately, the second problem is easily solved by storing everything sequentially. For the first problem, however, it was trickier.
int start_byte = res[idx].byte_offset + buffer_view.byte_offset;
int bytesCount = res[idx].count;
// By storing bytes sequentially, we do not have to make special structures
// for vector2, 3, 4...
switch (res[idx].type)
{
case .Vector2:
bytesCount *= 2;
break;
case .Vector3:
bytesCount *= 3;
break;
case .Vector4:
case .Matrix2:
bytesCount *= 4;
break;
case .Matrix3:
bytesCount *= 9;
break;
case .Matrix4:
bytesCount *= 16;
break;
default: break;
}
// ...
private static void GetAccessorDataFromBuffer<T>(Span<uint8> buffer, int startPos, int byteCount, int byteSize, List<T> accessorData)
{
int endPos = startPos + (byteCount * byteSize);
// Here we put the buffer data in a flat list
// If we, for example, have a Vector4, then we
// loop through the data 4 times longer
for (int sliceidx = startPos;
sliceidx < endPos && sliceidx < buffer.Length;
sliceidx += byteSize)
{
Span<uint8> bytespan = buffer.Slice(sliceidx, byteSize);
accessorData.Add(*(T*)bytespan.Ptr);
}
}
First, I tried to store the values in an enum. In Beef, enums are almost like unions, allowing you to store almost everything in them. A popular json implementation does just this, using a BumpAllocator (no, I don’t really know what this is) to store values in different formats in an enum. Clearly, I did not know my way around those BumpAllocators, so I tried a different approach.
// A structure such would require making a new list in one place
// and delete when you are done, this is very easy to forget
// or use a BumpAllocator to "shift" responsiblity
enum AccessorData {
case .Byte(List<uint8> bytes);
case .Unsigned_Short(List<uint16> ushorts);
// ...
case .Float(List<float> floats);
}
Template magic, I attempted to make a virtual version of the accessor data that could be passed around everywhere, and then an implementation of this virtual accessor data that takes a template type of the accessor data type. Except this did not work. So in the end, I had to give up and think of the simplest solution. Six lists, one for each type! It is wasteful, but it works.
// Template magic, sadly did not work the way I would like
interface IAccessorData
{
public void getList<T>(List<T> mylist);
}
class AccessorData<T> : List<T>, IAccessorData
{
public void getList<T2>(List<T2> mylist) where T2 : operator explicit T
{
for (let ac in this)
{
mylist.Add((.)ac);
}
}
}
class Accessor
{
// ...
// Yey, six lists!
public List<int8> accessorDataByte;
public List<uint8> accessorDataUnsignedByte;
public List<int16> accessorDataShort;
public List<uint16> accessorDataUnsignedShort;
public List<uint32> accessorDataUnsignedInt;
public List<float> accessorDataFloat;
// ...
public ~this()
{
// ...
// Make sure we delete the lists and deleting the object
delete accessorDataByte;
delete accessorDataUnsignedByte;
delete accessorDataShort;
delete accessorDataUnsignedShort;
delete accessorDataUnsignedInt;
delete accessorDataFloat;
// ...
}
// ...
// I like to have the methods that call "new" in the same class
// to avoid other places to make new lists, or even make
// double "new" or something like that
public void createUnsignedByteAccessor()
{
if (accessorDataUnsignedByte == null)
{
accessorDataUnsignedByte = new List<uint8>();
}
}
public void createByteAccessor()
{
if (accessorDataByte == null)
{
accessorDataByte = new List<int8>();
}
}
// ...
}
// ...
// So here we first call the methods to create a new respective
// list of the given type. This accessor data class has the responsibility
// to create/delete the lists.
// (res[idx]) is Accessor object
switch (res[idx].component_type)
{
case .Unsigned_Byte:
res[idx].createUnsignedByteAccessor();
GetAccessorDataFromBuffer<uint8>(buffer, start_byte, bytesCount, sizeof(uint8), res[idx].accessorDataUnsignedByte);
break;
case .Byte:
res[idx].createByteAccessor();
GetAccessorDataFromBuffer<int8>(buffer, start_byte, bytesCount, sizeof(int8), res[idx].accessorDataByte);
break;
// ...
}
Making this gTLF loader taught me a lot, not only about gLTF but also about Beef and more high-end programming in general!
Shadercross
If you use SDL3 GPU, you need to provide at least 3 different kinds of shaders. This is because SDL3 GPU, like most frameworks under the hood, calls Vulkan, Metal, or DirectX. So, how do you get those shaders? The answer is SDL_shadercross! The problem is, there are zero instructions on how to build this thing. So first you need to download the repo from here https://github.com/libsdl-org/SDL_shadercross/tree/main, then on Windows, go to the external directory and run the Get-GitModules.ps1 script. It will download the DirectXShaderCompiler and SPIRV.
Now, you need to compile DirectXShaderCompiler and SPIRV-Cross, so open a “Developer Command Prompt for VS”. Of course, you need to install Visual Studio first! Once you’ve got the Developer Command Prompt, cd to the DirectXShaderCompiler in the shadercross directory and run “cmake -B build .” It will make a build directory with a vs project inside. Open the vs project, choose build (you might want to change to Release first.)
Do the same thing with SPIRV-Cross, the other SPIRV stuff seems not to be needed. Developer Command Prompt for VS, “cmake -B build .”, open the vs project and compile (in Release if you want.)
Okay, almost done, now if you still have the Developer Command Prompt open, cd out to the shader cross main directory. Now you need to point cmake the DirectXShaderCompiler and SPIRV-Cross you just made. Moreover, we need to know about SDL3 at this point. Also, you need to tell that SPIRV-Cross is not shared, this is important for this cmake to work. Do this with this command:
cmake -B build . -DCMAKE_PREFIX_PATH="C:\path\to\SDL3\cmake;C:\path\to\SDL_shadercross-main\ external\DirectXShaderCompiler\build;C:\path\to\SDL_shadercross-main\ external\SPIRV-Cross\build" -DSDLSHADERCROSS_SPIRVCROSS_SHARED=OFF
If everything works, you should have a nice vs project in your build directory now. I had to manually set it to some dxcompiler libraries in Project settings for this to compile. Then to run, I needed to manually copy dll files from dxcompiler and SDL3. I can’t believe that there are no instructions for this anywhere, but hopefully this will help you out. Any problems, don’t hesitate to ask, but I cannot guarantee that I can solve those!
gLTF and SDL3 GPU
What was I talking about now again? Oh right! SDL3 GPU and gLTF, I am sorry, I got slightly deviated by the shadercross. First, I found some really excellent SDL3 GPU examples here: https://github.com/TheSpydog/SDL_gpu_examples/tree/main Those have helped me a lot. So, the first thing I would like to talk about is the gLTF buffers; it is actually possible to load them directly into the GPU! However, I did not do this for three reasons.
Firstly, it is tricky to do this; the gLTF data is a wide sequence of vertex positions, normals, UV coordinates, and other things. Animations are also stored in the buffers, for example. It is not easy to tell the GPU where is vertex positions, normals, and so on are. You would probably need a customized shader to do this.
Secondly, it gets complicated very quickly and is hard to debug. You would keep getting confused about what values you are working with. Finally, it is actually not faster, even if I still cannot convince myself that it is the case. Surely, would it not be faster to skip all transformation steps and load the data sequentially into the buffer? What if we have separate buffers then, one for vertex positions, one for normals, and one for uvs? No, apparently it this is not the GPU way to do things.
So instead, we do the textbook thing and make a structure for everything a vertex needs. Vertex positions, normal, and UVs, they all go into this structure. Every item in the vertex buffer will then be one of this structure. This means that we from the gLTF now need to take some values out from our accessor buffer and then first store them individually for each mesh.
// First we read the buffers here
private static void uri_parse(List<uint8> bytes, String uristr, String gltf_dir)
{
int type_idx = uristr.IndexOf(':');
if (type_idx == -1)
{
// Check if this is possible file and if so load it
File.ReadAll(scope $"{gltf_dir}/{uristr}", bytes);
}
// ...
}
// Then we do a lot of JSON parsing, move
// attributes from one structure to another...
// After that, run accessorsDataParse as we talked about earlier
// in the article
// When gTLF parsing is done, then we use a helper function
// to reformat the data
class GLTFGPUData
{
public List<float[3]> positions;
public List<float[3]> normals;
public List<float[4]> colors;
public List<float[2]> uvs;
public List<uint16> indices;
public static Result<void, GLTFError> ConvertToGPUData(GLTFData gltfData, List<GLTFGPUData> gpuData)
{
// Look in the meshes place, and get the accessors to the meshes out
for (let mesh in gltfData.meshes)
{
let meshGPUData = new GLTFGPUData();
for (let primitive in mesh.primitives)
{
meshGPUData.indices.AddRange(gltfData.accessors[(int)primitive.indices]
.accessorDataUnsignedShort.GetEnumerator());
if (primitive.attributes.ContainsKey(.Position))
{
let accessorData = gltfData.accessors[(int)primitive.attributes[.Position]]
.accessorDataFloat;
for (int posFragment = 0; posFragment < accessorData.Count; posFragment += 3)
{
meshGPUData.positions.Add(float[3](accessorData[posFragment],
accessorData[posFragment + 1],
accessorData[posFragment + 2]));
}
}
// ...
}
}
}
}
I made lists of three floats for the vertex position, three floats for normal, and two floats for uvs. I was not done here; I still needed to loop through these lists later on and make one structure element for each vertex.
// We are not done with the data, we still need to put them
// in a structure that matches the shader!
struct PositionColorVertex
{
public float x, y, z;
public float r, g, b, a;
}
// ...
// Later, when setting up the SDL GPU buffer
int i = 0;
for (; i < glftgpu[0].positions.Count; i++) {
transferBufferData[i] = PositionColorVertex(
glftgpu[0].positions[i][0],
glftgpu[0].positions[i][1],
glftgpu[0].positions[i][2],
1.0f,
0.5f,
0.7f,
1.0f);
}
My findings were that I just got a dark screen with nothing on. Here, Renderdoc was to rescue: https://renderdoc.org/docs/index.html. This is an excellent tool to find problems in shaders and GPU code. My problem was rather simple: to use a UniformBuffer I need to tell SDL that first by setting shaderInfo.num_uniform_buffers to the number of uniform buffers I have.
Phew, this was a lot to take in! You can see my gLTF loading example here: https://github.com/tomwjerry/beef_gltf
Future
I am done with my gLTF loader, it might not be so performant, but it works. Future improvements would be to reconsider whether I can improve the JSON loader. It is good for general purposes, but in gLTF it is possible to make certain assumptions about the data format.
Also, when parsing the JSON data, it is possible to directly load it to the final format; thus, we would rather directly parse the gLTF data than use some JSON structure intermediate steps. Another way to increase the performance would be to rethink the complex loops. Right now, the worst case is O(n⁴) complexity, so this needs to be addressed.
As you can see in the image, the fish looks rather blandly colored, and when you try to replace it with some other mesh, it will have the same color, and maybe only a small part of it is rendered. Clearly, more work is needed to load textures, multiple parts of a model, and scenes. Finally, it would be interesting to expand beyond this to go a small renderer/game framework. This is too much of an assignment for me, but with some help, I am sure we can get there.
Conclusions
Today, we have learned a little about Beef, SDL3 GPU, and gLTF and explored the possibilities. SDL3 is a very interesting framework, rivalling many of the existing 3D frameworks out there. There is yet to see any actual games made using it, but it is just a question of time.
Let me know if there is any interest in further work, whatever it is in Beef language, such as tutorials or the like, or making a game engine in SDL3!
메타데이터
- post_id
- ee51e6155fc4
- slug
- sdl3-gpu-sdl-shadercross-gltf-and-beef-ee51e6155fc4
- url
- https://medium.com/@tom.bobbas/sdl3-gpu-sdl-shadercross-gltf-and-beef-ee51e6155fc4
- canonical_url
- https://medium.com/@tom.bobbas/sdl3-gpu-sdl-shadercross-gltf-and-beef-ee51e6155fc4
- author_url
- https://medium.com/@tom.bobbas
- status
- ok
- fetched_at
- 2026-06-17 08:20:12