No content was provided.
Exanite.Engine
Dev Logs
Latest post - 6/6/2024, 12:01 AM
Today I experimented with working with the DirectX shader compiler from the C++ side, mainly to debug an issue I had when trying to compile shaders in my engine on Linux.
With the C++ version, I'm able to both successfully compile an empty shader and get an error when I provide an invalid shader. With the C# version, I simply get an "Internal compiler error" message regardless of the input shader. Not really helpful.
So far I see a few minor differences between the C++ and C# code, but nothing that jumps out to me as being the problem. I'm guessing it's due to string encoding differences or something obscure.
Shader reflection is done! I can now easily set shader variables. 🎉 Previously it involved a lot of brittle wiring (changing a variable name would require you to check and update 4 other places in the code).
Visually nothing has changed, but getting this done is a big step forward to making my Vulkan renderer easy to use. Internally though, there's like 4-5 layers of code that is reading the variable names from the shader code and assembling them in a way that is easy to work with, for both Vulkan and for me.
I finally have the ability to render textures in my Vulkan renderer! It took about a month to get here, but this means that I'm almost ready to switch over from using Diligent Graphics (the rendering framework I was previously using) to my own custom Vulkan renderer 🥳.
I'm also really surprised at how smooth the journey has been. Vulkan takes a lot of work to set up (I'm at around 3.5k lines of code right now), but it's a very consistent and high quality API overall. The documentation is very detailed, and once you get used to the error messages, the Vulkan validation layer is also very useful for catching any mistakes I make.
Today I finished integrating the DXC shader compiler into my Vulkan engine. I also added an include handler so #include ""
statements get properly resolved and replaced with the requested file. This is shown in the first image.
This was a two day effort which involved learning how Windows COM (Component Object Model) worked. It's effectively a way for C++ objects to communicate with objects in languages other than C++. In my case, C# and C++.
To make DXC ask my engine for the file to be included, I had to create a virtual method table that matches COM's expected format. The second image below shows the virtual table for a IUnknown object, which handles object type casting and reference counting.
More information in #Exanite.Engine (Custom game engine)
I finally got a triangle to draw in Vulkan!
Yesterday, I finished up my engine's modular content loading system. The engine can now automatically find and load content from different C# projects. This will eventually become part of the engine's modding system.
Today, I started by working on the actual content of the game, but got distracted and added an FPS counter and duration support to my gizmos system instead. Gizmos now have a duration, which lets the draw calls for the gizmos "linger" for a certain amount of time instead of instantly disappearing.
(More details in #Exanite.Engine (Custom game engine) 😁)
Today I added a custom MS Build task to my engine.
The task is used during the "development" mode of my game where content is still unpackaged and not combined into a single folder. To make development easier, the game needs to directly load content from the raw content folders and treat them as if they were a single folder.
This task generates the following file, which contains the raw content folders in the order that they should be loaded in:
I started polishing up the behavior of the UI today.
The cursor now changes based on what it is hovered over and is also hidden when the cursor hovers over the game world. I also switched to using SDL's key events directly, which fixed an issue where holding down the tab key doesn't continuously navigate through the UI.
The text box also works with Window's copy-paste history, which wasn't working before. Updating SDL did the trick. Apparently the bug was fixed "recently" (I don't think I've updated SDL in half a year 😅).
UI input is almost fully implemented! 😄
The hardest part was figuring out how to route input events from SDL to Avalonia and what keys correspond to what keys. A human might call the '
key the quote or apostrophe key, but Avalonia's predecessor WPF called it the Oem7
key, which wasn't at all cryptic.
You'll notice in the video that I can't type in the text box yet. That's because I need to implement text input and focus management.
Focus management hopefully should be easy since Avalonia should cover most of it. Text input is needed because it's different from keyboard key input. A naive implementation might involve just checking for keys that are pressed (if the A
key is pressed, output an "A"), but that very quickly falls apart with more complex cases (capitalization, caps lock, non-English characters). Fortunately SDL should cover most of it.
Today I implemented physics query drawing and fixed a few issues I had in my physics query code.
This video shows me testing the different query methods and making sure that they work.
The player has raycasts radiating from them and two shapecasts pointing towards the front. Each of the drones have a overlap box query, but they always show up as green since the boxes detect the drones as well. There two overlap tests not attached to the drones. You'll notice that these turn green when the player or a drone goes through them.
Collider drawing is now implemented! 😁
This was a bit more involved than I thought it would be since JoltPhysicsSharp doesn't allow me to access collider data after I create the physics body. One way to do it is to modify JoltPhysicsSharp to allow me to access that data, but in this case, it was easier to save the collider data from when I create the physics body.
This video shows me messing around with compound colliders.
Here's the finished gizmos system!
Meshes can be manually drawn by setting the mesh topology and calling AddVertex. They can also be drawn by using the DrawCube/Sphere/Circle/Mesh methods. The predefined meshes also come in Solid, Wire, and SolidAndWire variants.
Compared to my implementation in Unity, I have added vertex colors, which allow you to blend colors per vertex. Previously, I only allowed colors to be defined per mesh.
Ayy gizmos are working! I still need to add the in ability to quickly draw shapes like rectangles and circles, but that just involves writing the code to generate the mesh. I'll likely be using my gizmos for placeholder visual effects as well, similar to the aim line that you see in the video.
Since Gizmos require modifying meshes (or constantly destroying/creating new meshes -- not efficient), I worked on improving my Mesh class's API.
Previously the vertex/index buffers were untyped. This was because I made the mesh class before I made my Buffer<T> class and I was using Diligent Engine's raw handles. My Buffer class also has helper methods for modifying the data contained in the buffer.
Today I finished adding in the rest of the physics query methods. I also started working on a Gizmos system to make it easier to visualize things.
I also added analyzers to catch common problems in my code. Currently I make sure I use the ref
keyword consistently and that I don't default construct structs (this means everything is zeroed or null, which can cause issues).
I spent a good amount of time yesterday and today experimenting with C#'s source generators feature. For context, C#'s source generators are similar to other code generators in that they generate code, but C#'s source generators are tightly integrated into C#'s workflow and constantly run in the background. That means the generated code is updated almost immediately instead of you having to run a "generate code" command or function.
As part of this, I rewrote Arch ECS's source generators and trimmed the code down to only the parts I needed. As a mostly unrelated decision, I also decided to replace Arch ECS with Myriad ECS today. That involved rewriting the source generators that I had just rewrote. It's not a bad thing though and since I was familiar with the code already, rewriting the code was very quick.
Replacing Arch overall was extremely easy. This is because most ECS libraries are similar and Myriad was additionally inspired by Arch. I personally switched over to Myriad because it's safer -- it's much harder to cause undefined behavior. In Arch, almost everything was potentially unsafe and can crash the program.
Today I implemented an input action system similar to Unity's new input system. This greatly simplifies adding new key binds. The image below shows it in use in Gravitational Tetris.
Ayy, I got Avalonia to finally render something.
It was due to a different kind of synchronization issue, this time on the CPU instead of the GPU. Avalonia submits them by queuing them using a Dispatcher (this allows non-main threads to submit commands to be executed on the main thread). This Dispatcher is normally called every frame by Avalonia, but since I was using a custom engine, that Dispatcher was never being ran.
Once I figured out that this was the issue, fixing it was fairly straightforward: provide my own Dispatcher and run it during my engine's render loop.
Bindings have been created! I can now access the Vulkan side of Diligent Engine. Avalonia also uses Vulkan so this allows me to connect the two parts together. I now have a texture that is shared between those two parts and that should let me render the UI with Avalonia and display it with Diligent.
Theoretically, that's the first of three hard parts of making Avalonia work with my engine. I'm currently stuck on trying to anything to render. All I have is a black screen right now and I'll probably have to do some research to figure out why.
The image below shows the texture being created using Diligent, then being shared to Avalonia.
Integrating Avalonia into my engine has proven to be quite involved.
I'm currently setting up Avalonia so that it uses the same Vulkan device context that my engine uses. This is the same thing that Estragonia, the Avalonia-Godot integration I'm referencing, uses. This should let me access the texture memory used by Avalonia. I'm not sure how Diligent Engine, the rendering framework I'm using, will effect this though.
After that I'll need to hook up a decent amount of events, but hopefully that should be relatively straightforward. Things like input and copy-paste handling I'll have to implement as well, but my goal right now is to just have something render.
Speaking of which, today I experimented with using Avalonia UI in my game engine and refactored my code so that each Window that is created has their own Swapchain.
For context, Avalonia UI is a C# UI framework. It's meant to be used on its own, but with some work, it can be made to work within my engine (I'm using a Avalonia-Godot integration as reference). I haven't done that yet, so currently my engine and Avalonia are separate and two windows are created. Ideally, Avalonia UI renders into my engine.
Moving on, I previously had it so that exactly one swapchain has to be created. This was because when I initialize my engine, I require a window, swapchain, and graphics device to be created together. Now the graphics device can be created on its own. This change was mainly to clean up the data flow in my engine, but it also means I theoretically can have any number of windows in my game engine (0 included).