https://github.com/PacktPublishing/Mastering-Graphics-Programming-with-Vulkan.
https://github.com/PacktPublishing/
Windows 操作步骤:
因为仓库代码里包含了一些方便使用glTF模型的子模块,为了确保子模块正确初始化,使用如下命令来克隆仓库代码
git clone --recurse-submodules https://github.com/PacktPublishing/Mastering-Graphics-Programming-with-Vulkan
然后使用如下的bootstrap 脚本下载glTF资源python ./bootstrap.py
本章涉及的内容
本章代码
https://github.com/PacktPublishing/Mastering-Graphics-Programming-with-Vulkan/tree/main/source/chapter1.
vulkan 官方文档
https://vulkan.lunarg.com/doc/sdk/latest/windows/getting_started.html
When we started thinking about the code to be used, the objective was clear: there was the need for something lightweight, simple, and basic enough to give us the possibility to build upon it. A fully fledged library would have been too much.
There are different great libraries out there, such as Sokol (https://github.com/floooh/sokol) or BGFX (https://github.com/bkaradzic/bgfx), and a few more, but they all have some drawbacks that seemed problematic.
Sokol, for example, even though it is a great library, does not support the Vulkan API, and has an interface still based on older graphics APIs (such as OpenGL and D3D11).
BGFX is a more complete library, but it is a little too generic and feature-fledged to give us the possibility to build upon it.
After some research, we leaned toward the Hydra Engine – a library that Gabriel developed in the last couple of years as code to experiment with and write articles on rendering.
Here are some advantages of starting from the Hydra Engine (https://github.com/JorenJoestar/DataDrivenRendering) and evolving it into the Raptor Engine:
The Raptor Engine is created with a layer-based mentality for code, in which a layer can interact only with lower ones.
There are three layers in Raptor:
What if we need user input to move the camera, say with a mouse or gamepad?
Based on this decision, we created GameCamera in the application layer, which contains the input code, takes the user input, and modifies the camera as needed.
This upper layer bridging will be used in other areas of the code and will be explained when needed.
The foundation layer is a set of different classes that behave as fundamental bricks for everything needed in the framework.
The classes are very specialized and cover different types of needs, but they are required to build the rendering code written in this book. They range from data structures to file operations, logging, and string processing.
While similar data structures are provided by the C++ standard library, we have decided to write our own as we only need a subset of functionality in most cases. It also allows us to carefully control and track memory allocations.
We traded some comfort (that is, automatic release of memory on destruction) for more fine-tuned control over memory lifetime and better compile times. These all-important data structures are used for separate needs and will be used heavily in the graphics layer.
We will briefly go over each foundational block to help you get accustomed to them.
Let’s start with memory management (source/raptor/foundation/memory.hpp).
One key API decision made here is to have an explicit allocation model, so for any dynamically allocated memory, an allocator will be needed. This is reflected in all classes through the code base.
This foundational brick defines the main allocator API used by the different allocators that can be used throughout the code.
There is HeapAllocator, based on the tlsf allocator, a fixed-size linear allocator, a malloc-based allocator, a fixed-size stack allocator, and a fixed-size double stack allocator.
While we will not cover memory management techniques here, as it is less relevant to the purpose of this book, you can glimpse a more professional memory management mindset in the code base.
Let’s start with memory management (source/raptor/foundation/memory.hpp).
One key API decision made here is to have an explicit allocation model, so for any dynamically allocated memory, an allocator will be needed. This is reflected in all classes through the code base.
This foundational brick defines the main allocator API used by the different allocators that can be used throughout the code.
There is HeapAllocator, based on the tlsf allocator, a fixed-size linear allocator, a malloc-based allocator, a fixed-size stack allocator, and a fixed-size double stack allocator.
While we will not cover memory management techniques here, as it is less relevant to the purpose of this book, you can glimpse a more professional memory management mindset in the code base.
Hash maps (source/raptor/foundation/hash_map.hpp) are another fundamental data structure, as they boost search operation performance, and they are used extensively in the code base: every time there is the need to quickly find an object based on some simple search criteria (search the texture by name), then a hash map is the de facto standard data structure.
The sheer volume of information about hash maps is huge and out of the scope of this book, but recently a good all-round implementation of hash maps was documented and shared by Google inside their Abseil library (code available here: https://github.com/abseil/abseil-cpp).
The Abseil hash map is an evolution of the SwissTable hash map, storing some extra metadata per entry to quickly reject elements, using linear probing to insert elements, and finally, using Single Instruction Multiple Data (SIMD) instructions to quickly test more entries.
For a good overview of the ideas behind the Abseil hash map implementation, there are a couple of nice articles that can be read. They can be found here:
Article 1: https://gankra.github.io/blah/hashbrown-tldr/
Article 2: https://blog.waffles.space/2018/12/07/deep-dive-into-hashbrown/
Article 1 is a good overview of the topic and Article 2 goes a little more in-depth about the implementation.
Next, we will look at file operations (source/raptor/foundation/file.hpp).
Another common set of operations performed in an engine is file handling, for example, to read a texture, a shader, or a text file from the hard drive.
These operations follow a similar pattern to the C file APIs, such as file_open being similar to the fopen function (https://www.cplusplus.com/reference/cstdio/fopen/).
In this set of functions, there are also the ones needed to create and delete a folder, or some utilities such as extrapolating the filename or the extension of a path.
For example, to create a texture, you need to first open the texture file in memory, then send it to the graphics layer to create a Vulkan representation of it to be properly usable by the GPU.
Serialization (source/raptor/foundation/blob_serialization.hpp), the process of converting human-readable files to a binary counterpart, is also present here.
The topic is vast, and there is not as much information as it deserves, but a good starting point is the article https://yave.handmade.network/blog/p/2723-how_media_molecule_does_serialization, or https://jorenjoestar.github.io/post/serialization_for_games.
We will use serialization to process some human-readable files (mostly JSON files) into more custom files as they are needed.
The process is done to speed up loading files, as human-readable formats are great for expressing things and can be modified, but binary files can be created to suit the application’s needs.
This is a fundamental step in any game-related technology, also called asset baking.
For the purpose of this code, we will use a minimal amount of serialization, but as with memory management, it is a topic to have in mind when designing any performant code.
Logging (source/raptor/foundation/log.hpp) is the process of writing some user-defined text to both help understand the flow of the code and debug the application.
It can be used to write the initialization steps of a system or to report some error with additional information so it can be used by the user.
Provided with the code is a simple logging service, providing the option of adding user-defined callbacks and intercepting any message.
An example of logging usage is the Vulkan debug layer, which will output any warning or error to the logging service when needed, giving the user instantaneous feedback on the application’s behavior.
Next, we will look at strings (source/raptor/foundation/string.hpp).
Strings are arrays of characters used to store text. Within the Raptor Engine, the need to have clean control of memory and a simple interface added the need for custom-written string code.
The main class provided is the StringBuffer class, which lets the user allocate a maximum fixed amount of memory, and within that memory, perform typical string operations: concatenation, formatting, and substrings.
A second class provided is the StringArray class, which allows the user to efficiently store and track different strings inside a contiguous chunk of memory.
This is used, for example, when retrieving a list of files and folders. A final utility string class is the StringView class, used for read-only access to a string.
Next is time management (source/raptor/foundation/time.hpp).
When developing a custom engine, timing is very important, and having some functions to help calculate different timings is what the time management functions do.
For example, any application needs to calculate a time difference, used to advance time and calculations in various aspects, often known as delta time.
This will be manually calculated in the application layer, but it uses the time functions to do it. It can be also used to measure CPU performance, for example, to pinpoint slow code or gather statistics when performing some operations.
Timing methods conveniently allow the user to calculate time durations in different units, from seconds down to milliseconds.
One last utility area is process execution (source/raptor/foundation/process.hpp) – defined as running any external program from within our own code.
In the Raptor Engine, one of the most important usages of external processes is the execution of Vulkan’s shader compiler to convert GLSL shaders to SPIR-V format, as seen at https://www.khronos.org/registry/SPIR-V/specs/1.0/SPIRV.html. The Khronos specification is needed for shaders to be used by Vulkan.
We have been through all the different utilities building blocks (many seemingly unrelated) that cover the basics of a modern rendering engine.
These basics are not graphics related by themselves, but they are required to build a graphical application that gives the final user full control of what is happening and represents a watered-down mindset of what modern game engines do behind the scenes.
Next, we will introduce the graphics layer, where some of the foundational bricks can be seen in action and represent the most important part of the code base developed for this book.
Once again, the API design comes from Hydra as follows:
We define graphics resources as anything residing on the GPU, such as the following:
The usage of graphics resources is the core of any type of rendering algorithm.
Therefore, GpuDevice (source/chapter1/graphics/gpu_device.hpp) is the gateway to creating rendering algorithms.