You are browsing as a guest. Sign up (or log in) to start making projects!

Light

@Light

Joined June 4th, 2026

  • 30Devlogs
  • 1Projects
  • 0Ships
  • 0Votes
I'm a 15 years old guy from Italy. I'm curious about how low-level systems work, and this summer I took the opportunity to explore them with Vulkan and C++ with my project called Eve.
Open comments for this post

5h 37m 44s logged

DEVLOG 30 - Compute Shaders

Hi Stardance!
 
Intro
I’ve finally implemented compute shaders—as the title suggests—alongside conducting some tests and reorganizing the project.
 
Compute Shader Support
I’ve added support for compute shaders, meaning they can now be used just like any other type of pass, whether graphics or transfer.
 
Optimizations
I’ve also utilized compute shaders internally within the engine to speed up matrix calculations; here are the results:
With 100,000 entities using instanced rendering, I’m getting 800 FPS, up from the previous 250.
With 1,000,000 entities using instanced rendering, I’m getting around 90 FPS, up from the previous 30.
With 100,000 entities using independent draw calls, I’m getting around 90 FPS, up from the previous 30.
I’d say the results speak for themselves, and I’m quite happy with these optimizations. As mentioned in the previous devlog, testing was performed on my PC, which is equipped with a Ryzen 5 5600X and an RX 6600.
 
Swapchain & CMake
I also made slight modifications to the swapchain recreation code to make it faster and reduce stuttering.
Finally, I tidied up the CMakeLists file to make it more organized and prepare for the release.
 
The video shows a scene with 1M entities rendered in real-time using instanced draw call at 90 FPS

0
0
8
Open comments for this post

8h 4m 2s logged

DEVLOG 29 - 1M Entities!

Hi Stardance!
 
Intro
Since the last day, I’ve been working alone and have run into several issues… Most of them have been resolved.
 
Some Tests
So I started running some tests to check the speed of my new rendering system, and with 100,000 entities drawn to the screen with a single draw call, I was getting 30 FPS. After parallelizing the code a bit and reducing the copies, the results are now better:
100,000 entities with single draw calls get 50-60 FPS.
100,000 entities with a single instanced draw call get 230-250 FPS.
1,000,000 entities with a single instanced draw call get 30 FPS.
 
What Need To Be Optimized
Actually, I could optimize even more and double these FPS, but the current problem is that I’m calculating the matrices on the CPU side, so I’m doing a lot of calculations that currently take up a good portion of the necessary CPU time, about 50%. The idea would be to move all the work to the GPU in the future with compute shaders, thus significantly reducing the CPU load. I’ll see if I can do this… Maybe in a while, when I add full support for compute shaders. (I have a Ryzen 5 5600 X and an RX 6600)
 
Rendering System Time:
Not counting the matrices, the rendering cycle along with the Render Graph takes about 2-2.5 ms, almost all of which is spent on the Render Graph. I have also fixed some problems with Swapchain Recreation when the window is minimized.
 
Problems With RenderDoc
Five of these eight hours have been pure frustration, as I don’t understand why, but the app crashes on RenderDoc. Normally, on my PC it starts and works, but on RenderDoc it crashes… I’ve tried every way to understand the problem, but unfortunately I haven’t been able to. I’ll also test on other PCs to better understand the problem. Unfortunately, I don’t have time on my side, so I’ve had to put the issue aside for now…

0
0
7
Open comments for this post

10h 13m 34s logged

DEVLOG 28 - New Render System

Hi Stardance!
 
Intro
I think this devlog is consistent with the next one I’m working on, since the work I’ve done in the last few hours isn’t quite finished, as there are still some bugs to resolve…
 
Why a new render system?
So far, I’ve been working on improving Eve’s rendering system. Instead of having to manually create buffers and the like, Eve has a completely automated system similar to the one used by Unity. For example, when you try to draw an object, you need things like object transformation matrices. If you then need more data, the limited size of push constants is no longer enough.
 
Per Instance Params
Eve, therefore, with its new rendering system, revolutionizes the way we write code, reduces bugs, and simplifies it. For each draw call sent to the GPU, the engine sends a buffer called InstanceParams to the GPU, which contains, for each drawing instance, its model matrix and its inverse. These matrices can then be retrieved using simple, custom functions ready for use in the shader.
 
Per Render View Data
For camera data, the process is similar. For each RenderView object, Eve allocates these objects on the GPU in a contiguous buffer from which the view, perspective, VP, and other matrices can be accessed.
 
Custom Draw Info Data
If you want to send custom data to the shader, each drawing function offers a pointer called DrawInfo that allows you to send data to the shader, which can then be accessed. If set to null, the shader will not send anything.
 
GLM & A Flycam
I also created a flycam and deprecated the Eve math package to use the glm one. Honestly, I want to focus on Eve and not the math, which is why I chose this option.
 
Next Step:
I still have some bugs to fix regarding making more than one draw call, but I’m working on it.

0
0
55
Open comments for this post

7h 29m 35s logged

DEVLOG 27 - Material System

Intro
After some code rewrites, such as the separation between shaders and materials, the materials system now works. It’s very similar to how it works in Unity. I’ve figured out how to set things like material properties (floats, integers, vectors, etc.) directly from C++ using their names, without manually binding code.
 
How Vulkan Handle All Of This?
In Vulkan, you can’t set a shader parameter directly with a name. You need to create a buffer on the GPU, specifically a UBO, typically used for materials, bind it using a descriptor, and then bind it on the CPU. All of this is hidden by Eve, making the code easier and faster to write.
 
What It Does?
Basically, once the shader is compiled, it’s analyzed and checked for a struct called Properties. If it exists, this struct will be used for the material properties. Each field within it will be settable directly from the CPU using its name. Eve, now knowing the size of the struct and the field offsets, creates a buffer for the materials (actually, one for each frame in flight) and uses it to write everything a material might need, thus allowing for very simple parameter setting.
 
How To Use It:
The material provides an ID via the GetPropertyUBOId function, which must always be retrieved at each frame since it is not the same from frame to frame. This ID can be sent to the shader via push constant, which then allows access to the material’s property UBO, which contains all the values ​​set on the CPU side.
 
A simple cube rotating on itself using the new Material System and simple direct lighting

0
0
13
Open comments for this post

4h 51m 55s logged

DEVLOG 26 - Input System

 
Hi Stardance!
 
The New Input System
Since I couldn’t interact much with the graphics engine at the moment, I chose to create an Input System that currently only supports keyboard and mouse. It has methods like IsKeyDown(...), GetMouseState(...), IsMouseUp(...), and similar ones. It’s quite simple, but it allows you to interact with the world, for example by creating a camera or a moving player.
 
The Cube
For fun and to test a bit, I tried creating a scene with a slightly rotated cube with direct light, obviously using my Render Graph, which allows you to do all this in about 150 lines of code (CPU side) instead of having to manually write barriers, configure passes, and many other repetitive tasks.
 
I’m still using glm for matrix math, but I’m trying to reach a more stable level with the Eve package.

0
0
12
Open comments for this post

7h 10m 25s logged

DEVLOG 25 - Eve is “Multi PC”

 
Hi everyone!
What’s changed since last time?

 
More Device Supported
As promised, Eve now uses Vulkan 1.2, so it can be used on multiple devices. Obviously, the device requires that, in addition to supporting Vulkan 1.2, it also supports some extensions that became core in Vulkan 1.3.
 
Tests On Other PCs
I then tried running the app on the only PC I had at home, but it crashed. I then tried updating the graphics driver, but that didn’t help; unfortunately, that PC is too old. I then asked a friend of mine to test the app. After fixing some issues, it started, but it would crash instantly. After hours of debugging, I found the problem: the problem was in resource allocation within the Render Graph, which only occurred with integrated GPUs (my heap selection algorithm for integrated GPUs was wrong). It’s now fixed. I also added my checks for failed Vulkan or VMA calls in places where they were missing, so now everything should work properly.
 
Math Packet In Eve
I also added a small mathematical component to Eve, including Vector2,3,4; Vector2Int,3Int,4Int; Quaternion; and Matrix4x4. I must say that quaternions were difficult to understand at first, but once you understand how they work, it’s very easy to visualize them in your head.
 
 
The first attachment shows Eve running on the PC of this friend of mine, whom I thank so much for his help, and the second image shows Eve running on my PC.

0
0
10
Open comments for this post

4h 34m 48s logged

DEVLOG 24 - Error Manager, Copies & More

 
Intro
Hi Stardance!
Lately, I’ve been focusing mainly on improving error handling returned by Vulkan and fixing some code here and there.
 
New Error Manager System
I’ve created a system responsible for error handling so that when an error occurs, such as a failure to allocate memory due to fragmentation or any other critical error, the app places a text file with information about the error in a folder called logs (created on the fly if it doesn’t exist). Immediately afterward, an error message is displayed on the screen explaining what happened. When the user presses the OK button, the app closes in the pop-up by calling std::abort();.
 
Some Test On Other PCs
I tried running Eve outside of my PC on two other PCs, but since they’re a bit older, they don’t have support for Vulkan 1.4, so the app can’t be started.
 
Swapchain Improvements
The code for creating the swapchain has been improved somewhat, making it more secure and robust.
 
New Copy Support
I’ve added support for buffer and texture copy operations between resources that aren’t the same type (one transient and one persistent).
 
Next Step:
I want to make Eve a little more portable, so in the next few hours I’ll try using Vulkan 1.2, trying not to mess with the code. If necessary, I might resort to using Vulkan 1.3; I’ll see…

0
0
13
Open comments for this post

3h 54m 5s logged

DEVLOG 23 - Instanced Draw & Some Fixes

 
Intro
Devlog time!!
In the last devlog, I had some issues with instanced drawing and reading a position buffer.
 
What Was The Problem?
I worked hard, but I managed to figure out where the problem was and fix it. (The problem was due to a for loop responsible for creating the address buffers. I was saving pointers outside the for loop with structs allocated inside the for loop; therefore, the pointers were pointing to corrupted memory or nothing.)
 
Render Graph Fixes
I fixed several things in the Render Graph that could cause future problems. I added small guards within the Render Graph so that if the user does something wrong, the application doesn’t crash unexpectedly. For example, previously, if the user set a texture to display that they’d never used (never involved in a transfer operation, never used as a graphic attachment), the program would crash. With these simple guards, everything is solved.
 
Easier Resource Usage Declaration
I’ve also added the ability to easily declare textures and buffers usage in the Graphics Pass class with dedicated methods like UseBufferReadOnlyVertex(...), UseTextureVertex(...), and others.
 
The image shows a grid of 20x20 quad meshes drawn with a single draw call instead of 400 using instanced draw.

0
0
22
Open comments for this post

10h 30m 27s logged

DEVLOG 22 - ECS Bugs Fixed, Graphics Files Reorganized

Hi Stardance!
 
What I Did
In this dev log, I fixed some of the last bugs the ECS code, so it now works and is very efficient. I ran several tests destroying, creating, and moving entities from one archtype to another, and everything works. I then worked on the project structure of the graphics part by fixing the graphics include files.
 
What Went Wrong
These updates mentioned so far have been successful. Two hours ago, I added a method in the graphics pass responsible for instanced drawing, so I took advantage of the opportunity to create a buffer to read the instance positions to see if it worked. I still don’t know why, but it doesn’t work. The application freezes, and sometimes the driver crashes (it scared me a little… :/ ). The Validation Layers don’t report anything wrong… So I need to figure out what’s wrong and fix it.
 
The image shows some of the methods available after fixing the folders. The methods are in the Graphics class in the include folder.

0
0
11
Open comments for this post

10h 9m 5s logged

DEVLOG 21 - ECS Refactoring

 
Intro
Over the past ten hours, I’ve worked extensively on the ECS, making it more robust, easier to use, more secure, and more efficient. What’s changed:
 

  • New Way To Record Entity Commands
    Entity Command Pools are still used, but they’re managed not by the user but by the Entity Manager. The old CreateCommandPool() method no longer exists. Now, to execute a command on entities, you make a request to the EntityManager, which takes the command and places it in an EntityCommandPool of its choosing. The EntityCommandPool is chosen based on the SystemId provided by the engine with methods like Start, Awake, Update, etc. This reduces complexity and allows for systems that can run natively in parallel on multiple threads.
     
  • Improved Efficiency
    Internally, the Entity Manager has improved the way to compact batches after command execution, making it simpler and faster. Instead, iterating over all batches only iterates on the entities that are no longer valid, and those slots are refilled with the components of the valid entities at the end of the last batches. Batch creation and destruction is handled automatically by the EntityManager.
     
  • New Way To Query Tables
    The way to find tables with certain requirements has improved; the old method required registering a query and then returning a “ticket” used to access the query results. The operation is now simpler and more unified: The EntityManager is asked for all tables that meet the QueryInfo structure requirements, such as the archtype. The EntityManager checks whether the query already exists; if it doesn’t, it calculates it on the fly and returns it.
     
  • Queries Updated Only When Needed
    Queries are updated only when tables are created/destroyed; otherwise, they remain - static and aren’t updated, saving calculations.
     
    The next step will be to verify if everything works, since I haven’t tested anything yet because I’m forced to do this devlog due to the ten-hour time limit. :/
     
     
    Sorry about the picture, it’s the same, I didn’t have time to do anything else
0
0
5
Open comments for this post

5h 48m 46s logged

DEVLOG 20 - A Better Triangle

 
Intro
Hi Stardance!
This triangle was created with Render Graph. The entire process is divided into two steps: the first draws the triangle on a user-selected image, and the second takes that image, samples it, and draws it on the swapchain so the triangle can be seen on screen.
 
Clean and Simple API
The beauty of this is evident in the photo (third attachment). You only need to write a few lines of code to achieve all this compared to the complexity and number of lines of code required by Vulkan. Render Graph thus creates all the resources needed for synchronization, such as barriers, and attempts to alias memory whenever possible. It also leverages Resource Pooling, so images aren’t destroyed and recreated at every frame. Instead, if they have identical requirements, they are reused, avoiding unnecessary Vulkan calls.
 
MSAA Deprecated
MSAA images have been deprecated because, from a desktop computer perspective, they aren’t worth using because they require a lot of computational resources. To achieve anti-aliasing, it’s best to use post-processing passes that use FXAA (Fast Approximate Anti-Aliasing) or TAA(Temporal Anti-Aliasing).
 
There are probably some bugs still present in the Render Graph despite the many already fixed; I’ll work on them.

2
0
33
Open comments for this post

9h 52m 40s logged

DEVLOG 19 - Colors!

 
Hi Stardance!
I’ve made some progress and finally drew something on the screen!
 
Improvements on the Swapchain Pass
I’ve improved the Render Graph a bit by cleaning up old methods and making all the improvements discussed in the last devlog, such as improving the rendering code for the swapchain. Normally, you would set the 2D texture to display on the screen using the RenderGraph::SetPresentTexture(); In reality, this texture isn’t displayed directly on the screen but is first sampled and copied to the swapchain, so the Render Graph adds a hidden pass for this. When this texture hasn’t been assigned to any texture, the swapchain will be colored with the clear value set to black and sent to the screen.
 
Push Constants Supported
I’ve added the ability to use Vulkan Push Constants. Push Constants are data packets that can be sent to the GPU in parallel with the drawing command. Their size is very limited, 128 bytes, but their transfer speed is extremely fast. A hardware limitation is that the offset and size parameters must be multiples of four, so I used a struct called Words32 to dictate the 32-bit size and offset, not the 8-bit one. Furthermore, internally, the engine checks the state of the previous push constant for each push constant update, so as not to make unnecessary updates and to register the push constant command only when necessary, and only partially for the relevant bytes.
 
Shaders Improvements
I improved the shader code a bit by reducing the arguments needed to create a shader, and I also unified the layout; all shaders use the same VkPipelineLayout.
 
The Problem and The Solution
When I was writing the shaders to get these things on screen, I ran into errors telling me I was assigning a buffer descriptor to Descriptor Set 0 with binding 0, even though they’re actually textures. However, in the shader, I wasn’t requesting any of this, since the vertex data was written directly into the shader. After some research, I discovered that Slang shaders were creating bindings for these arrays outside the shader. To fix this, I simply moved the arrays into the vertex shader function, marking them with const.
 
And note that all these images/videos use the swapchain pass, so this was all just a test. Now I’ll have to try declaring a real pass with Render Graph and try to draw something, hoping everything works correctly :/

0
0
7
Open comments for this post

10h 1m 26s logged

DEVLOG 18 - More Refactoring

 
Hi Stardance!
I don’t know what to say about this devlog, since I’ve written a lot of code for my specific implementation… I’ll try to describe what I did without being too boring.
 
Why I refactored again?
As discussed in the previous devlog, I felt the code wasn’t “nice” or at least not very tidy, and honestly, I don’t like that very much, so I rewrote the Render Graph (partially) for the third time. Now, the Render Graph only manages single-frame resources, while all multi-frame resources are managed by a separate class called TransientResourcePool, which is therefore responsible for storing the resources.
 
The Render Graph: A single frame responsibility
The Render Graph is still responsible for memory aliasing and resource pooling, of course, as well as generating barriers and inserting the draw, copy, and, in the future, compute commands. (I still need to improve the code for compiling commands into the Command Buffers a bit.)
 
Improvement in the searching of pooled resources
Now, searching for resources to reuse is even faster, since resources are divided by their composition, for example. For textures, the parameters for this division are height, depth, width, samples, etc. This allows you to immediately find a resource if it’s available.
 
More organization is the TransientResourcePool
The TransientResourcePool is also responsible for permanently destroying unused resources for several frames. I had written the destruction code within the TransientResourcePool, but I thought maybe it wasn’t its responsibility, but rather an existing class called MemoryBin. The MemoryBin class was configured to work only for persistent resources, but now it handles the destruction of persistent and transient resources.
 
What I need to fix:
There’s a small problem in the code: Normally, when you want to destroy a resource, it’s queued and usually assigned a countdown. This countdown is a number that corresponds to the maximum frames in flight and decreases by one every frame. When this number reaches zero, the resource can be destroyed. This works perfectly, but when, for example, the swapchain needs to be recreated because the window size has changed, the GPU needs to be put into idle mode. This means that all in-flight frames have finished, and therefore all resources queued for destruction can and must be destroyed immediately, otherwise they would pile up.
 
Otherwise, I think everything is perfect. I don’t think it will take me long to implement these little tricks, so I assume and hope :/ to start writing the code to display something on the screen soon. I’ll go back to my old, dear little triangle :)

0
0
15
Open comments for this post

11h 7m 45s logged

DEVLOG 17 - Meshes & New Architecture

 
Into
See you again, Stardance. I’ve been implementing several things lately, as the title suggests, and I think we might soon have some geometry on the screen.
 
Mesh System
I’ve been working on meshes, creating everything needed to support mesh creation, editing, and destruction. Currently, if you create a mesh, you obviously create all the necessary buffers: one for vertex positions, one for normals, one for UVs, etc. So, for example, if I load position data and the buffer doesn’t exist, a new buffer is created and a CPU-GPU upload operation is performed. However, if the buffer exists and its size is greater than the new vertex size, the buffer isn’t reallocated to a smaller size but remains the same, reducing the number of vertices used for drawing. I’m not sure how useful this option is, but I plan to add an option to specify this in the future.
 
Image Presentation Supported
I’ve also been working on how to display an image on the screen. Currently, the Render Graph has a method that, given a texture’s Transient handle, sets that texture as the image to display on the screen. Internally, the Render Graph creates a draw pass at the end of all existing passes and then samples from the presentation texture set to draw on the swapchain. The code for all this might be a little rough at the moment; I’ll improve it later.
 
The New Architecture
For a while now, I’ve felt like the code was stepping on its own toes. I had a class called MemoryRegistry that was responsible for assigning slots to resources, allocating them, and managing their lifecycle. I didn’t like this, so I decided to rewrite much of it, splitting everything into multiple classes. Now I have a class called ResourceRegistry that provides resource IDs. A class called ResourceMapper that inserts resources into their respective global descriptors. A class called ResourceTracker that manages the resource lifecycle, their last use, which stage they were in, and similar information. Until recently, the MemoryRegistry managed both persistent and transient resources, which was a mess. I’ve decided that the MemoryRegistry should only manage persistent resources, while transient resources should be managed by a class called TransientResourcePool or something similar, which should be managed by the RenderGraph. That’s exactly what I’m working on.
 
Some Stupid Bugs
I overran the timeframe because I was partially testing the new code and encountered a stupid bug that made me lose time. (I was iterating an Array starting from the last element, but I forgot to subtract one from the size of the vector assigned to the for loop index, thus causing an out-of-memory access.) I think I’ll also rewrite a good portion of the RenderGraph first to adapt it to the new architecture.
 
Advantages
These changes will make developing new features faster and less cumbersome without having to adapt to an architecture that was poorly designed from the start.
 
I swear that after I’ve done all this, I’ll render something to the screen, assuming there are no unforeseen issues ;)

0
0
12
Open comments for this post

9h 57m 13s logged

DEVLOG 16 - Blank Window, Draw Calls & More

 
The last ten hours have been very productive. As the devlog title suggests, I managed to get the classic black window, completed the code for draw calls, and also accomplished many other things.
 
Draw Calls Commands
I then completed the code for making draw calls with a simple mesh, integrating all the logic into the Render Graph. If you want to send any command to the GPU—a draw call, a copy, a dispatch, or anything else—it must necessarily go through the Render Graph. This allows me to have a central entity that can manage barriers and commands in a highly optimized, precise, and secure manner. When you want to draw something, you must declare a graphics pass. This pass has methods to execute the drawing commands, but it also has methods to set things like the texture depth/stencil to use, the target texture to draw on, etc. You can also have multiple color targets, as long as they have the same resolution.
 
Blank Window
Since I haven’t seen any tangible results on screen for a while, only code, I tried displaying a black window and then initializing the engine. The new context, window, and swapchain builders all work. The Resource Mapper, the class that manages the global descriptor set, also works. Testing the other classes will require a bit more work until I’ve got all the missing pieces, but it’s almost there.
 
Persistent Resources Support
Since creating a mesh also involves creating data and sending it to the GPU via buffers, I need a way to upload data from the CPU to the GPU. Mesh buffers are persistent, not transient. Currently, in the Render Graph code, I only had support for copying and uploading transient resources. From now on, this is also available for Persistent resources, as I’ve written the same code for them as well. I also implemented barrier generation for persistent resources so that synchronization is handled internally by the Render Graph, as with transient resources.
 
New Resource Handles
Up until now, each resource has been represented as a handle corresponding to an unsigned 32-bit number. From now on, resources are represented by two unsigned 32-bit numbers: one for the previously existing identification number and a generation number. By combining both numbers, each resource is unique, thus eliminating the possibility that if a resource is destroyed and recreated with the same ID, it could be mistaken for an older resource that no longer exists.
 
Project Cleaning
I finally cleaned up the project a bit and made it a bit more organized by dividing the files into more specific subfolders.

0
0
8
Open comments for this post

10h 3m 12s logged

DEVLOG 15 - Slang & Render Graph

 
Reorganization
So, I’ve been dividing the project into folders, making everything more organized, which wasn’t the case until now.
 
The New Shader Language: Slang
I saw that there was a more modern alternative to GLSL I could use, so I chose Slang. I created the code to create it in real time, and then I also changed the architecture of the Pipeline Builder a bit (the class responsible for creating Vulkan’s Pipeline and PipelineLayout objects).
 
Vertex Fetch, Why Did I Choose It?
I’ve been thinking for a while about forcing users of the engine to use Vertex Fetch. What is it? Well, normally when using a shader, before the Vertex Shader there’s a phase called Input Assembly that, based on the triangle indices, samples vertices from the vertex buffer, allowing them to be used directly in the Vertex Shader without manual sampling. However, this means that the layout of the vertex buffer must never change; if it were to change, you’d have to create another Pipeline object and bind it. Honestly, I don’t like this limitation very much. That’s why I removed this feature and adopted Vertex Fetch, which allows you to not specify a vertex layout, thus having as few Pipeline objects as possible. Obviously, you have to manually fetch the vertex from the respective buffer in the Vertex Shader, but frankly, it’s not a huge problem.
 
Work In Progress On The Render Graph
Another thing I’m implementing is commands that can be sent to the GPU via the Render Graph. So now you can draw, make GPU-GPU copies between buffers, between textures, and even between textures and buffers. Copies currently only support Transient resources; I’ll add the functionality for Persistent resources later. The same goes for using Compute Shaders. I’ve also added the ability to upload data from the CPU to the GPU, again for transient resources. For Persistent resources, I’ll also add this functionality later. I’m still writing the code for this, so I’m not finished yet, but I’m forced to devlog for the usual 10 hours of Stardance…

2
0
7
Open comments for this post

9h 54m 46s logged

DEVLOG 14 - I Rewrote The Memory Aliasing Algorithm

 
Intro
Unfortunately, I don’t have anything exciting to say. I’m forced to do this devlog since I’ve reached ten hours since my last devlog, so I have to publish one, otherwise my hours wouldn’t count in full. I’ll try to share what I did, the problems I encountered, and how I solved them.
 
The Problem
I might sound crazy, but after working on the Render Graph for twenty hours, I rewrote it entirely from scratch. You might ask, why? The reason I did this was Vulkan itself. I discovered that, since I’d previously relied on VMA (Vulkan Memory Allocator) to manage memory, now that I was managing memory without it, there was something I’d overlooked: a parameter called Memory Type Index. The Memory Type Index indicates where to allocate memory. There could be a Memory Type Index for the GPU’s local memory, one for the Resizable BAR, etc. So, what’s the problem if all the Render Graph resources must live exclusively in the GPU’s local memory anyway? Wouldn’t it be enough to simply add a few lines of code and everything would be solved? Unfortunately, not because this index indicates in which memory to allocate that resource, but there’s also another parameter called Memory Type Bits, which is a bitmask that indicates in which Memory Type Indexes a resource can or cannot be allocated. All resources are allocated in the same Memory Type Index, but they may have different Memory Type Bits. This means, for example, that a texture without multi-samples cannot fit in the same memory space as a texture with multi-samples. This can also happen with the aspect mask, so a color texture and a depth texture might not fit in the same place. It could also happen with the format or tiling. The same goes for buffers, but these are more flexible than textures.
 
The Solution
My Memory Aliasing algorithm was designed without taking all this into account, so I chose to rewrite a large part of the Render Graph, trying to recycle some parts. Therefore, the Aliasing algorithm now operates on “Buckets,” which are in-memory containers containing resources that can fit in the same space. Without this distinction, memory aliasing wouldn’t be possible because a resource attempting to reuse memory used by previous resources must only check among resources with the same requirements, not among all resources. Therefore, it checks per bucket, not all existing resources. This has also made the code much cleaner and more organized.

0
0
3
Open comments for this post

10h 1m 44s logged

DEVLOG 13 - Memory Aliasing

 
Recap
In the last devlog, I explained what I did and why I was creating a Render Graph. I mentioned a concept called Memory Aliasing, but I explained it briefly. I’ve been working extensively on the Render Graph these past few hours to implement the Memory Aliasing algorithm.
 
What is Memory Aliasing?
Memory Aliasing involves having two or more resources use the same portions of memory, thus sharing that memory. For example, let’s take a texture we’ll call A. This texture is used as Depth in pass 1 and read in pass 2. Another texture, called B, is used in pass 3 and pass 4 (see photo). We could allocate both resources independently, but you can clearly see that the resources don’t overlap in the passes. Texture A begins its usage cycle in pass 1 and ends in pass 2, and immediately after, in pass 3, texture B is used for the first time. What we could do is have both textures occupy the same memory space to save VRAM. This is the concept of Memory Aliasing. Obviously, since the resources share the same memory space, barriers must be created to synchronize resource access, otherwise there would be a race condition (two threads operating in parallel on the same memory space, not just for read-only operations).
 
Resource Pooling
In addition to memory aliasing, I wanted to optimize another aspect that took me some time. In Vulkan, when you want to create resources, you use vkCreateImage and vkCreateBuffer. These functions create a handle without associating physical memory with these objects. Binding occurs later, after executing the memory aliasing algorithm, which determines which memory space to assign to a resource.
This means that I have to create and destroy resource handles every frame, which apparently isn’t a light operation to perform on multiple resources. So I thought that if there was a possibility, I could reuse handles from previous frames by simply comparing whether the settings of the requested handle were the same as the one to be reused and then binding the memory.
 
The Problem and the Solution
This was the idea I had, but unfortunately it’s not feasible because Vulkan doesn’t allow memory binding to a resource more than once, so this approach wouldn’t have worked. After thinking for a while about how to solve this, I decided to include in the requirements for using old handles, in addition to the texture or buffer settings, the offset and stride of how they were bound in memory. This way, if the pass structure and usage hadn’t changed significantly during the user’s setup phase, the Memory Aliasing algorithm would always produce the same results, given the deterministic nature, making this all feasible.
 
Finally, if the resources aren’t reused for an N number of consecutive frames, they are permanently destroyed.

0
0
3
Open comments for this post

9h 58m 6s logged

DEVLOG 12 - Render Graph, Code Refactoring

 
A Little Recap
In the last devlog, I mentioned I watched the 2017 GDC conference, where they explained how the Frostbite development team had created a Render Graph. But what is a Render Graph, and what is it used for? To answer this question, we must first understand how the GPU works.
 
The Difference: CPU & GPU
First, a GPU, unlike a CPU, performs all its operations in parallel. When we write code for the CPU, we say: do this operation, then this, then that, and so on. The CPU performs its operations synchronously, in sequential order. The GPU, however, doesn’t work that way. If we tell the GPU that we want to draw three objects, we can send it three draw calls, which we’ll call A, B, and C. These draw calls are sent in order, so first A, then B, and then C. The order in which the commands are sent doesn’t correspond to the order in which they are executed, because the GPU could execute the commands in this order: C, B, A, or B, A, C, or A, C, B, and so on. This is because, while operation A might be in progress, operation B might start executing, so both commands will use the same resources and therefore run into a race condition if both operations are not read-only.
 
The Problem
From this perspective, the GPU is very powerful because it tries to stay constantly busy, eliminating dead time. However, this greatly complicates things on the synchronization side. Let’s take two operations: operation A that writes an image and operation B that reads the image. How can we be sure that the GPU executes operation A first and not B? Who can assure us that the GPU, while executing A, doesn’t have to execute B? The answer to all these questions is no one.
 
How Graphics APIs Handle All Of This
In graphics APIs like OpenGL, all of this is hidden from the programmer, so they don’t have to worry about it. But in graphics APIs like Vulkan and DirectX 12, this is the programmer’s responsibility. The programmer must write a barrier and tell the GPU not to touch the image from operation B before A has finished. For a few barriers, this is feasible, but as soon as you write more complex applications, making the code sufficiently organized, non-repetitive, and fast to develop becomes very challenging. This is why the concept of a Render Graph exists.
 
What A Render Graph Does
A Render Graph allows the programmer to specify which steps the GPU should execute, which operations resources like images and textures will perform, and in what order. The Frame Graph looks at this graph constructed by the programmer at each frame and internally understands where barriers and sometimes semaphores are needed (if we’re working with multiple queues, that’s not the case for me). Another responsibility of the Frame Graph is to internally allocate and manage resources that only exist within a single frame, called transient resources. Finally, I’m implementing Memory Aliasing. This reduces the memory used by resources: for example, if resource A is used for the last time in pass 3 and resource B for the first time in pass 4, they can share the same physical memory, thus reducing the overall memory requirement.
 
Bonus
I’ve also restructured the texture creation methods, making them simpler and allowing for fewer specifications. The before and after is a good result… The same goes for 1D and 3D textures.

0
0
5
Open comments for this post

10h 14m 38s logged

DEVLOG 11 - BDA, Pipeline, Shaders

 
In the last devlog, I explained how I wanted to structure my bindless descriptor set. With that approach, I had a buffer for each data type. This approach works, but it’s a bit cumbersome, so I switched to using Buffer Device Addresses (BDAs). The idea behind it is: Create buffers, around VRAM, placing the buffers within VRAM memory pools. Instead of saving the physical data inside the descriptor buffer, we can only save pointers to the buffers, so I’ll have a buffer of pointers to the buffers scattered across VRAM. Since the pointers take up 8 bytes, managing the buffer is very simple, as it will be very light. So, essentially, I separate the physical data of the buffers from the shader binding logic.
 
Rather than using namespaces to hide the internal implementation, I chose to use Hadles, which are simply numbers that identify that resource. So I could have a BufferHandle, ImageHandle, SamplerHandle, PipelineHandle, and so on.
 
I chose not to call the structure that manages all this with names that might be reminiscent of the Vulkan API, but instead called this system Resource Mapper. At the moment, I think I’ve done something wrong with the code: Since the buffers are simply exposed via a 32-bit index, internally the buffer index corresponds to the index of the vector in which the Vulkan buffer handles and all the allocation information are allocated. I think I’ve erred in saving the buffers in vector indices that don’t match their exposed handle index outside of Eve. This causes access to incorrect buffers or buffers that don’t even exist, causing UBs or crashes. I still need to verify this issue and, if it occurs, I’ll have to fix it.
 
Furthermore, yesterday I added all the code responsible for creating the graphics pipeline and compiling the shaders. This code is still a bit rough and needs improvement.
 
Furthermore, since I don’t want the user to have to manage synchronization outside of the engine, I’ve been thinking about how to structure a Render Graph. I saw Frostbite’s presentation at GDC 2017 where they explained how their Render Graph works and that gave me some insights.

0
0
2
Loading more…

Followers

Loading…