-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
Wondering how facade
works? This page provides a high level overview of exactly that!
The general idea is to have the library manage as much as possible without compromising having a stateless per-frame API. In other words, beyond the fundamental states of rendering, windowing, and input, the library remains stateless from the user's point of view. It's the application's responsibility to load shaders, run the event loop, and track state between frames.
facade
uses the following third-party dependencies:
Built from source:
- fmt: Text formatting
- GLM: Linear algebra
- STB (Image): Image decompression
- GLFW: Windowing and input
- Vulkan Memory Allocator: Vulkan memory allocation
- SPIR-V Cross: Shader reflection
- Dear ImGui: GUI / editor
- djson: JSON parser
Loaded dynamically:
- Vulkan loader (
libvulkan1.so
/vulkan1.dll
)
facade
needs to perform some work to initialize, which are, in short:
- Initialize GLFW (and check for Vulkan support)
- Create a GLFW window
- Load Vulkan function pointers
-
Engine
: Create a Vulkan Instance, Device, andVma
instance -
Renderer
: Create a Vulkan Swapchain,RenderPass
,Pipes
, double buffer ofRenderFrame
s, and initialize Dear ImGui
Small RAII wrapper over the VMA library, provides APIs to make Vulkan Images / Buffers and transition image layouts. facade
uses one "universal" Vulkan Queue for all command submissions.
Contains the synchronization resources, command buffers, and framebuffers to perform a render pass.
Pipeline factory and cache. facade
uses SPIR-V Cross to inspect shaders and build Vulkan Descriptor Set Layouts for them. The "spec" for a pipeline is hashed and all pipeline variants (one per Vulkan Render Pass) stored within it. Shaders are abstracted to a std::string
id and pre-compiled SPIR-V code at this layer, it is the application's responsibility to ensure all shaders referenced by various Material
s have been successfully pre-loaded into the Renderer
.
Each Pipeline thus owns its descriptor set layout, and compatible Vulkan Descriptor Set Pools to generate Vulkan Descriptor Sets for each object to draw using that pipeline. This implies that a single descriptor set cannot be shared across multiple pipelines.
Each DescriptorSet
abstraction owns double buffered Buffer
s that can be written to during draws (UBOs / SSBOs). An externally owned Buffer
can also be bound to a set without having to write to its internal buffers, but the user must manage rotation in such cases; this can be useful for updating scene data / that common to all objects being drawn, instead of writing the same data to all their internal buffers.
Owns and manages all the resources mentioned above, specifically the Vulkan Swapchain and Render Pass, and provides a configurable number of secondary Vulkan Command Buffers per frame. Provides APIs to add shaders, start the next frame (acquire a swapchain image), build and bind pipelines via their specs, execute a render pass and submit the recorded command buffers. Also enables changing certain characteristics of the swapchain, eg Vulkan Present Mode / Vulkan Image Format (ColourSpace
).
facade
uses two-stage command buffer recording: a number of secondary command buffers are returned to the user for individual draws (one per thread per frame) upon acquiring an image. These are then executed inside a render pass by the primary command buffer for the frame before queue submission. All image layout transitions are explicitly carried out (instead of relying on the driver / render pass) using granular sync. Synchronization for arbitrary commands (eg transfers) is intentionally not granular, and a Cmd
's destructor will block until its queue submission has completed execution. The ability to batch multiple groups into a single submit and / or moving out the submit fence instead of blocking on destruction may be integrated in future versions.
Central user-facing API, initializes and owns everything above, as well as the active Scene
. Does not own the event loop, but provides an API to poll the window and obtain its latest Glfw::State
. Offers an API to load a GLTF scene from a given path asynchronously, notify when done, and obtain the load progress at any time.
The engine uses one command buffer per frame (no multi-threaded rendering), and executes the render pass in one shot (not split across multiple functions).
Refer to Scene Structure for more info on how a scene is laid out. The logic to render a Scene
is abstracted into SceneRenderer
, an instance of which is owned by Engine
. It itself owns resources like a 1x1 white Texture
, UBOs and SSBOs for the scene view and lights, etc. The process of rendering a scene is:
- Write scene view and lights to buffers
- For each root node, call
render
, passing a unit parent transformation matrix:- If the node has a
Mesh
attached, draw it - For each child of the node, call
render
, passingparent * node.transform.matrix()
as the parent transformation matrix
- If the node has a
Drawing a Mesh
:
- For each
Primitive
:- Obtain its material (or use a default
LitMaterial
) - Bind the corresponding
Pipeline
- Update set 0 to point to the scene view buffers
- Dispatch writing other sets to the material
- Write textures to set 1
- Write material data to set 2
- Obtain its
StaticMesh
- Create the instance matrices
- Update render frame stats
- Execute Draw Call
- Obtain its material (or use a default
Draw Call:
- Write instance matrices to a vertex buffer
- Bind
StaticMesh
vertices, indices (if not empty), and instance matrices to pipeline vertex buffers - Issue draw call (indexed or otherwise)
All Vulkan resources (Texture
s, Buffer
s, etc) can be created fully asynchronously: this is what enables async loading of entire Scene
s. Access to individual resources is not synchronized. facade
uses an internal device level mutex to lock queue submissions and other shared accesses, and uses dedicated Vulkan Command Pools per ad-hoc Cmd
command buffers so they can be used on any thread.
GLFW is categorically not thread safe, all calls to the library (direct or indirect) must occur exactly on the thread that created the window (on MacOSX only the main thread). This includes calling Engine::poll()
. facade
assumes usage of the same thread for polling and rendering, which is why it's able to offer a unified "virtual frame" across both domains.