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

Eve

  • 34 Devlogs
  • 277 Total hours

Eve is a cross-platform, optimized, modern graphics engine, built to learn Vulkan and C++.

Open comments for this post

2h 48m 52s logged

DEVLOG 34 - Icon, Windows & More

Hi Stardance!
 
Intro
The release is getting close, so I’m tidying up a few final details; I’ve finally turned my attention to a slightly more relaxed and lighthearted part of the project.
 
Engine Settings
I added a method to start the engine that accepts configuration settings for how the app runs. You can also choose whether or not to enable VSync at runtime.
 
Window Class
I added a window management class that allows for various operations, such as toggling fullscreen mode and manually changing the resolution, position, title, etc. I also included a method to set the window icon using a PNG image.
 
Eve’s Icon
I also spent some time on Canva trying to create an icon—a logo—for Eve. After several attempts, I settled on the one in the attachment.

0
0
1
Open comments for this post

4h 2m 21s logged

DEVLOG 33 - Meshes & Fixes

Hi Stardance!
 
Intro
We’re getting closer to the release build of Eve! I have to say that with this devlog, the engine is practically finished and ready for shipping; I just need to tweak a few final details and we’re all set…
 
Meshes:
In this devlog, I worked on the mesh system—fleshing it out and making it functional—and added standard meshes to Eve: a cube, a UV sphere, an icosphere, a plane, and a quad.
 
Some Bugs Fixed
While implementing this, the program kept crashing for no apparent reason; after two hours of debugging, I found the error in the Render Graph—caused by the order of Eve’s internal passes—which is now fixed.
 
Internal Shaders Organization
I also embedded the engine’s internal shaders directly into the executable, so there’s no need to remember to include them alongside the executable when creating an app.
 
The video shows the same UI as the previous devlog—featuring controls for color, rotation, cube position, and direct light direction—but this time, you can change the meshes of the “thousand-odd” cubes at runtime using the standard Eve meshes listed above, via a simple UI dropdown menu.

0
0
9
Open comments for this post

7h 53m 10s logged

DEVLOG 32 - UI Support

Hi Stardance,
 
Intro
I’ve been working on the UI lately, as I wanted to give the user a way to interact with the engine beyond just mouse and keyboard input.
 
The UI in Eve
I opted for Dear ImGui and created a lightweight wrapper that allows it to run within Eve. I stripped away some of the boilerplate code associated with the standard Begin and End calls, making it slightly simpler and safer to use than raw ImGui. I also added support for dockable windows.
 
The video shows a scene with 1,000 cubes; thanks to the UI, you can modify their color, position, and rotation. You can also change the direction of the direct light. The code required by this UI is just five lines.

0
0
2
Open comments for this post

17m 51s logged

DEVLOG 31 - It’s not my fault!

Hi Stardance!
 
Unlike the other devlogs, this one is very quick and simple.
 
A Little Recap
In devlog 29, I encountered some issues with RenderDoc, even though the app kept running fine and caused no problems outside of RenderDoc itself.
 
What I Did
After doing some online research, I discovered that RenderDoc has issues with AMD drivers, which can cause the app to crash. So, I decided to try AMD’s Radeon Developer Tools (a profiler similar to RenderDoc, designed specifically for AMD GPUs). No errors were reported and everything works perfectly, so the problem most likely lies in how AMD drivers interface with RenderDoc.

0
0
31
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
9
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
Open comments for this post

11h 13m 38s logged

DEVLOG 10 - Working On The Vulkan Wrapper

 
For now, I’m focused on creating a wrapper for Vulkan to make it simpler and less repetitive to use. I don’t want to create a wrapper that abstracts too much, thus losing control; instead, I’m creating a fairly thin wrapper.
 
I’m currently restructured the Application class a bit and divided some of its parts into three “modules.” I’m structuring this using the Factory Pattern. I’ve created three Builders: one for the window and SDL initialization, one for creating a Vulkan context—VulkanInstace, PhysicalDevice, LogicalDevice, and the graphics queue—and the last one for building the swapchain. I chose this approach because I consider it flexible and I don’t want everything to fit into one huge monolith; I prefer having multiple modular classes.
 
In addition to these more or less complete builders, I’m focusing on creating a system that can allow shaders to see and manipulate the data stored in VRAM. This is usually done with DescriptorSets, which have been the standard for years. They’re more or less like pointers to objects in VRAM so shaders can access them. In reality, they’re fat pointers because they contain other information besides the address. However, this approach is good but not optimal because it introduces CPU overhead. Since the draw calls may not share the same materials and therefore require different textures, a new DescriptorSet must be bound before each draw call, which, with many draw calls, incurs CPU overhead.
 
What I’m doing to solve this problem is looking towards a more modern and efficient approach called Bindless. Basically, instead of having multiple small DescriptorSets, you have one huge global DescriptorSet that contains all the resources used by the application. So we could have a sort of array containing all the samplers, then another containing all the textures, another containing all the buffers of a specific type, etc.
Then we just need to bind this DescriptorSet at the start of rendering, which will be used for the entire rendering process, and pass indices to the shader that will tell it which resources to use from the DescriptorSet.
 
I’m a little slow in developing all this because I’ve never used Vulkan; in fact, I’m still learning. The second reason is that Vulkan is very complex and there’s a lot of boilerplate code.
 
I still have to finish the ECS system since I don’t yet have a centralized approach for obtaining the IDs and GenerationIds of new entities, which are verified and therefore safely managed internally by the engine. But I think I’ll do it later since I’m a little tired of fiddling with entities and want to do something different for a while.

0
0
3
Open comments for this post

4h 21m 28s logged

DEVLOG 9 - 100K Entities

 
After the last devlog, in which I rendered 1K entities, I tried increasing the number of entities to a 15x15x15 cube, or 3375 entities, and I saw the frame rate drop unexpectedly, 25 FPS. I was wondering if this was normal or not… Apparently, it wasn’t… I tried to figure out the problem, but it wasn’t easy.
 
I tried using a tool called RenderDoc that lets you see what’s happening on the GPU, so all the calls, data passed to the GPU, buffers, textures, etc. That information also included the time, and the GPU indicated it was taking about 16 ms, which was quite strange.
 
I started to suspect Vulkan’s validation layers. By disabling them, I got to 30 FPS or a little higher, I don’t remember exactly. Then I disabled ASan and set the swapchain to Immediate Mode so as not to be constrained by the screen refresh rate, and I got to about 100 FPS. Then I made several CPU-level optimizations at the code level by changing the way components are obtained from batches in my ECS system, specifically limiting those operations that can be avoided in the internal component iteration loop, and I got up to 140 fps. Finally, the thing that made me open my eyes and say WOW was compiling in Release and not Debug, the fps skyrocketed to around 2300… I couldn’t believe it either…
 
After doing all this, I rendered 100,000 entities and my fps were stable at 90… Incredible! Finally, just for the sake of curiosity, I tested 1,000,000 entities and got 9 fps, which is good because it means the system scales linearly.
 
This is what I’ve been doing for the last two hours. Before that, I spent some time revisiting my ECS code to make it more secure. For example, many of the Table and EntityManager methods now return references and not pointers. I’ve also made the ComponentRegistry class much more efficient by storing component information in contiguous arrays instead of lists, which therefore contain non-contiguous data.
 
Next Steps:
I need to implement a way to get verified IDs from the EntityManager, since currently the IDs are only manual and non-unique. And then I’ll need to refactor the rendering code again, making it more modular, since it’s currently all in a single class.

0
0
3
Open comments for this post

9h 37m 43s logged

DEVLOG 8 - 1K Entities

 
I implemented several things:

  • One of these is a manager that allows systems to register their functions so they can be called, for example, when Update, Start, LateUpdate, etc.
     
  • I implemented Address Sanitizer in the project to detect leaks, overflows, or out-of-bounds reads, thus uncovering some bugs. I didn’t understand why, but ASan was having trouble integrating with the project; apparently, the shaderc library was causing problems because it had a different connotation than the project. I finally solved the problem by telling CMake to use the Release version of shaderc, not Debug.
     
  • I created a test system called S_Test, which I used to verify that I could create and manage multiple entities (apparently, yes). After having several memory issues, which I resolved with the help of ASan, I also started creating a camera component so I could move freely in 3D space.
     
  • I’ve improved the security of the code; Entity Command Pools now no longer need to be created by the programmer, but by the Entity Manager itself, which manages them. This minimizes the risk of freeing memory too early, resulting in cleaner code for the game. Furthermore, the EntityCommandInfo and QueryInfo classes can now be allocated on the stack rather than the heap, resulting in a more secure program.
     
    Finally, for fun, I wanted to try creating a thousand entities, also to see if the system was capable of it and if there were any problems. I have to say, I’m quite happy with the result.
     
    Next Step:
    I still have to improve several parts of the code to make it more secure and faster, such as the Component Registry.
0
0
1
Open comments for this post

6h 4m 38s logged

DEVLOG 7 - Code Reorganization

 
I tried to reorganize the project so that the headers needed by users of the engine are visible, while the internal, and therefore unnecessary, ones are not.
 
What do I mean?
Let’s say we have a class called A. This class is used by the engine itself, but it can also be used by the developer, for example, to record commands or something similar.
So the developer will include class A in their header or source files so it can be used. But there’s a problem: Header A could include other headers that aren’t necessarily needed by the developer using the engine, thus polluting the headers.
 
How do I solve this problem?
Since I haven’t used C++ for years, I’ve been researching the problem online for months and found the following solutions:
 

  • Forward Declaration - This consists of declaring the required class in the header, in this case A, without using the include directive, which literally copies the headers included in the file containing the directive. This seems like a good method, but unfortunately the data can’t be variable by value, only by reference or pointer, since the compiler must know at compile time how much memory that specific object occupies.
     
  • Pimpl Idiom - This involves enclosing or encapsulating the information needed for the internal functioning of A in a class or struct that is hidden from the developer using the engine. This method could work and is almost standard, but personally, I don’t really like the idea of ​​having to create another structure to achieve this goal. It’s more of a workaround, since the language has significant limitations in this regard.
     
  • C++20 Modules - Modules are a new way of programming in C++ because, among other things, they solve this problem. They also reduce compilation times and avoid separating the code into two separate files (.hpp and .cpp). I tried using them, and from a compiler perspective, they work great (I’m using Clang), but from a Clangd perspective, the situation is quite different:
    While the compiler support is fairly mature, Clangd still has several issues; in fact, it reported errors everywhere. I even tried modifying the .clangd file by adding various flags, but the situation didn’t improve.
     
  • There are other methods, such as Fast Pimpl, but they aren’t as flexible.
     
    What I’m looking for is something flexible that generally works in all conditions and doesn’t introduce overhead, which Pimpl Idiom doesn’t.
     
    The final solution was to use namespaces.
    All the public headers that must be used by the developer using the engine are in the Eve/ namespace, while the internal headers that shouldn’t be used by the developer are in a namespace called Eve/Internal. If the developer uses these files, it’s their fault; they’re marked as internal to the engine and therefore shouldn’t be used.
     
    I also changed build system and switched to CMake as it is more widespread in the C++ community and this can also give an advantage in the engine configuration section for other users
0
0
2
Open comments for this post

28h 32m 28s logged

DEVLOG 6 - Crash, Crash, Crash and…. Crash

 
What I did
After spending several hours designing my ECS architecture, I realized I could do better, so I basically started from scratch and, as I mentioned in the previous devlog, I chose to use Archtypes so everything would be extremely efficient. Maybe I’m a bit too obsessed with performance…
 
The Problem
Anyway, after finishing the entire system, I started the app and it crashed. After several hours of debugging with simple print(); I thought the problem was in the memory, but I couldn’t figure out where… I finally decided to use Address Sanitizer, a tool that alerts you to out-of-bounds writes or reads, which allowed me to pinpoint the problem. I had performed a nonsensical multiplication, so the memory pointer was pointing to the wrong place, specifically to the end of the allocated memory portion, causing the metadata to be overwritten. (Apparently, when allocating memory, the program itself allocates additional memory slices that store information about the allocated memory portion.) The problem was that the app didn’t always crash, only sometimes, and I didn’t understand why. But I finally managed to get it working…
 
The Result
At the moment, this is a single entity, and since I haven’t tested the rest of the code, such as the destruction logic and entity transitions from one archtype to another, there are likely other bugs waiting for me.

0
0
4
Open comments for this post

4h 38m 28s logged

DEVLOG 5 - The ECS Architecture
 
Since I can now draw something on the screen, I’d say it’s time to start building the actual graphics engine. The first system I need to create for Eve is the management of game objects. There are two options: the first is to use the OOP programming paradigm and therefore manage game objects just like Unity does; the other, which is the one I chose, is to use the ECS (Entity, Component, System) architecture.
 
To understand why I chose ECS, you need to understand how this architecture works. As I mentioned, the word ECS is an acronym for Entity, Component, and System, which represent the three pillars on which this architecture is based. In short, entities represent objects, the essence of the object; that object simply exists, and they are usually represented by a simple numeric ID. Components, on the other hand, represent pure data; therefore, each entity can have its own components and therefore its own data. Systems, on the other hand, are the part of the code that executes the logic. Let’s take the rendering system as an example. This will iterate over all entities that have mesh components, and a transform will take their data and send it to the GPU for rendering.
 
This division of responsibilities ensures incredible efficiency, given how the CPU works, since the component data is stored contiguously in memory. This facilitates the work of a CPU component called the Hardware Prefetcher, which tries to predict which portion of RAM the CPU will use next, thus reducing cache misses.
 
There are many ready-to-use libraries for this architecture, such as Flecs, EnTT, and others. I chose to try and create my own so I could learn and understand more. Currently, I’m using, broadly speaking, the same approach Unity uses with their ECS architecture, which is part of their Unity DOTS ecosystem.
 
All entities in the game can be classified based on an ID called an Archtype, which identifies the type of entity being viewed based on its components. This way, you can have contiguous arrays and minimize cache misses, thus maximizing CPU efficiency.
 
I’m still working on it, but I have to say it’s extremely interesting…

0
0
2
Open comments for this post

4h 34m 55s logged

DEVLOG 4 - Textures!
 
I finally managed to use textures; Vulkan has a bit of boilerplate in their configuration, especially with the descriptor sets. This was the part that slowed me down a bit.
 
The code isn’t very tidy at the moment… For now, I’ve focused more on learning, so I haven’t written anything extremely tidy and readable. As I said, I’ve only learned the basics so far.
 
Now that I feel more confident about the subject, I think I’ll start writing something. I’m thinking of a framework or something simple that allows you to draw geometry on the screen without having to write hundreds of lines of code directly with Vulkan. So, more generally, I think I’ll create a layer of abstraction while still trying to maintain excellent flexibility.

0
0
1
Open comments for this post

2h 22m 53s logged

DEVLOG 3 - First Cube!!!
 
How satisfying, my first 3D object!

Since previously all vertices were written manually in the shader, I opted to learn how to create buffers to load more complex 3D geometry onto the screen.
 
I’m loading the resources directly into a specific portion of memory called Host Visible, where the CPU can also write, and the GPU’s reading isn’t as fast as it could be. If I instead allocate the memory in a portion of VRAM called Device Local, the GPU will be very fast at accessing the data, but the CPU, on the other hand, won’t be able to do anything with that memory. To modify it, you need to first allocate the data in an intermediate buffer called Staging Buffer, and then from there the data can go to Device Local memory (the GPU’s fast memory).
 
I also added some transformations and rotations with GLM to rotate the cube, and this is the result, which is nice.
 
I think I’ll try using Device Local memory by loading data to it, as discussed before, with a Staging Buffer and after that I think I’ll start experimenting with textures.

0
0
7
Open comments for this post

9h 2m 12s logged

DEVLOG 2 - Consolidation of things learned
 
Well,
What’s new? Just a square? It might not seem like much, but yes. I spent all these hours rewriting the entire code from scratch so I could understand where I had the most doubts and therefore review some concepts.
 
Synchronization was and still is the most difficult concept to grasp; understanding how the semaphores and barriers are linked together wasn’t easy for me, and I must admit I still have some doubts about it, but certainly much less than before. Given the asynchronous nature of the graphics card, you can’t program operations that will be executed in order, unlike the CPU, and this is one of the graphics card’s strengths and weaknesses. You can parallelize many operations, but you have to be careful how they are linked together and synchronize them as correctly as possible.
 
My next step, I think, will be to use buffers to give more complex geometry to the shaders, primarily for drawing 3D geometry. I also think I still have to delve deeper and better understand things like semaphores, timeline semaphores, and barriers, which are topics I feel I haven’t internalized as well as others.

0
0
4
Open comments for this post

11h 36m 55s logged

DEVLOG 1 - Hello Triangle!!!

 
Finally,
after several hours trying to understand how Vulkan works, watching and reading several tutorials, I got it, my first triangle, isn’t it cute?🫠🫠
 
I’ve figured out most of the things, but I think I need to delve a little deeper into the process of creating a pipeline with dynamic rendering and synchronization. These are the two topics that have been giving me the most trouble.
 
I think the next step will be to re-explore the code and try to create something on my own, without a guided tutorial, to see if I’ve really understood the concepts.

0
0
4

Delete project?

Are you sure you want to permanently delete this project? This action cannot be undone.

All devlogs, followers, and associated data will be removed.

Followers

Loading…