Program Listing for File VulkanRenderer.ixx#
↰ Return to documentation for file (Src/GraphicsEngineVulkan/renderer/VulkanRenderer.ixx)
module;
#include <optional>
#include <string>
#include <glm/glm.hpp>
#include <algorithm>
#include <cstdint>
#include <memory>
#include <span>
#include <vector>
#include <vulkan/vulkan.hpp>
#include "renderer/PathTracingHistory.hpp"
export module kataglyphis.vulkan.renderer;
import kataglyphis.vulkan.as_manager;
import kataglyphis.vulkan.buffer_manager;
import kataglyphis.vulkan.buffer;
import kataglyphis.vulkan.camera;
import kataglyphis.vulkan.descriptor_set_group;
import kataglyphis.vulkan.device;
import kataglyphis.vulkan.frame_capture;
import kataglyphis.vulkan.frame_sync;
import kataglyphis.vulkan.gpu_timing;
import kataglyphis.vulkan.global_ubo;
import kataglyphis.vulkan.frustum;
import kataglyphis.vulkan.gui;
import kataglyphis.vulkan.gui_renderer_shared_vars;
import kataglyphis.vulkan.gui_scene_shared_vars;
import kataglyphis.vulkan.instance;
import kataglyphis.vulkan.path_tracing;
import kataglyphis.vulkan.post_stage;
import kataglyphis.vulkan.rasterizer;
import kataglyphis.vulkan.deferred_rasterizer;
import kataglyphis.vulkan.raytracing;
import kataglyphis.vulkan.cascaded_shadow_map;
import kataglyphis.vulkan.sky_box;
import kataglyphis.vulkan.clouds;
import kataglyphis.vulkan.scene_ubo;
import kataglyphis.vulkan.swapchain;
import kataglyphis.vulkan.scene;
import kataglyphis.vulkan.texture;
import kataglyphis.vulkan.window;
export namespace Kataglyphis {
class VulkanRenderer
{
public:
VulkanRenderer(Kataglyphis::Frontend::Window *window,
Scene *scene,
Kataglyphis::Frontend::GUI *gui,
Camera *camera);
void drawFrame(const GUISceneSharedVars &guiSceneSharedVars);
void updateUniforms(Scene *scene_data,
Camera *camera_data,
const GUISceneSharedVars &guiSceneSharedVars);
std::optional<uint32_t> addModel(const std::string &modelPath, const glm::mat4 &modelMatrix);
[[nodiscard]] bool isModelLoadPending() const { return scene != nullptr && scene->isModelLoadPending(); }
// Reads/mutates the caller-owned scene settings directly (no GUI pointer)
// so this can be driven by anything that produces a GUISceneSharedVars,
// not only a live GUI object. GUIRendererSharedVars (renderer-only state:
// rasterization mode, hot-reload trigger, ...) is unrelated to scene
// settings and still comes from the `gui` member.
void updateStateDueToUserInput(GUISceneSharedVars &guiSceneSharedVars);
void finishAllRenderCommands();
bool hasDeviceLost() const { return device_lost_detected; }
// True once any frame has hit a fatal (non-recoverable) error - a failed
// wait/submit or an invalid sync handle - that forced the window closed.
// Distinct from hasDeviceLost(): a failed submit can leave a live device
// that App::run() still tears down normally, but the run itself must
// still be reported as a failure.
[[nodiscard]] bool hasFatalFrameError() const { return fatal_frame_error; }
bool supportsHardwareRaytracing() const;
// -- headless frame capture (test / tooling instrumentation)
//
// The capture is recorded *inside* the frame's own command buffer, right
// after the post stage has transitioned the swapchain image to
// ePresentSrcKHR and before the present. Copying the image after
// vkQueuePresentKHR would mean touching an image owned by the presentation
// engine, so instead the copy is armed one frame ahead:
//
// renderer->requestFrameCapture();
// ... one normal frame (updateUniforms + drawFrame) ...
// auto pixels = renderer->takeCapturedFrame(width, height);
//
// takeCapturedFrame() waits on the fence of the frame that recorded the
// copy, so it never calls waitIdle and never blocks the frame path.
// Returns tightly packed RGBA8 (swizzled from BGRA when the swapchain uses
// a B8G8R8A8 format), or an empty vector when nothing was captured.
bool supportsFrameCapture() const;
void requestFrameCapture();
std::vector<uint8_t> takeCapturedFrame(uint32_t &outWidth, uint32_t &outHeight);
void cleanUp();
void recreateSwapChain();
void reprovisionPerImageResources();
~VulkanRenderer();
private:
void shaderHotReload();
// -- decomposed updateStateDueToUserInput handlers
void handleShaderHotReloadRequest(
Kataglyphis::VulkanRendererInternals::FrontendShared::GUIRendererSharedVars &guiRendererSharedVars);
void handleRasterizationModeChange(
Kataglyphis::VulkanRendererInternals::FrontendShared::GUIRendererSharedVars &guiRendererSharedVars);
void handleShadowResolutionChange(
GUISceneSharedVars &guiSceneSharedVars);
// Tears down and re-creates dirShadowMap against the resolution/cascade
// count the GUI currently holds and the swapchain's current image count.
// Shared by handleShadowResolutionChange (GUI-driven resolution change)
// and reprovisionPerImageResources (swapchain image count change) - both
// need the exact same cleanUp()+init()+createGraphicsPipeline() sequence,
// just triggered by a different condition.
void reinitShadowMapForCurrentSettings();
void handleModelTransformChange(
GUISceneSharedVars &guiSceneSharedVars);
void handleModelReloadRequest(
GUISceneSharedVars &guiSceneSharedVars);
// The rasterization mode whose offscreen texture the post (and RT) input
// descriptors currently point at. The mode branch in record_commands is
// consulted every frame, but the DESCRIPTORS were written once at init -
// so switching to Deferred recorded a deferred frame into a texture the
// post pass never sampled, and the presented image silently stayed
// forward. Tracked so updateStateDueToUserInput can rebind on change.
Kataglyphis::VulkanRendererInternals::FrontendShared::RasterizationMode lastBoundRasterizationMode{
Kataglyphis::VulkanRendererInternals::FrontendShared::RasterizationMode::Forward
};
// helper class for managing our buffers
VulkanBufferManager vulkanBufferManager;
// Vulkan instance, stores all per-application states
VulkanInstance instance;
// surface defined on windows as WIN32 window system, Linux f.e. X11, MacOS
// also their own
vk::SurfaceKHR surface{};
void create_surface();
std::shared_ptr<VulkanDevice> device;
VulkanSwapChain vulkanSwapChain;
Kataglyphis::Frontend::Window *window;
Scene *scene;
Kataglyphis::Frontend::GUI *gui;
Camera *camera;
// -- pools
bool record_commands(uint32_t image_index, const GUISceneSharedVars &guiSceneSharedVars);
// Returns the offscreen colour texture of whichever raster path is active
// this frame (forward vs deferred), reading the mode once. The RT/PT and
// post input descriptors, and the RT/PT passes, all target this texture.
Texture &activeOffscreenTexture(uint32_t index);
// Records the active raster path (forward Rasterizer or deferred
// DeferredRasterizer) for this frame and publishes its visibility stats.
// Extracted verbatim from record_commands; the frustum is passed in so its
// deliberate post-shadow-pass ordering stays visible at the call site.
void recordRasterPass(vk::CommandBuffer &commandBuffer,
uint32_t image_index,
std::span<const vk::DescriptorSet> rasterizer_descriptor_sets,
const std::optional<FrustumPlanes> &camera_frustum);
// The single predicate deciding whether recordRaytracingOrPathTracing will
// actually dispatch this frame. Called from both record_commands (to skip
// the raster pass) and recordRaytracingOrPathTracing (to gate the RT/PT
// dispatch), so the two can never disagree. The TLAS-null term is
// load-bearing: during the async model-load window RT/PT is requested but
// cannot dispatch yet, so the raster pass must still run.
[[nodiscard]] bool raytracingOwnsFrame(uint32_t image_index);
// Records the ray-tracing or path-tracing pass (whichever the GUI selected)
// when hardware RT is available and the TLAS is built, including the
// path-tracing accumulation-reset bookkeeping. Extracted verbatim from
// record_commands.
void recordRaytracingOrPathTracing(vk::CommandBuffer &commandBuffer, uint32_t image_index);
void create_command_pool();
void cleanUpCommandPools();
vk::CommandPool graphics_command_pool{};
// uniform buffers
VulkanRendererInternals::GlobalUBO globalUBO{};
std::vector<VulkanBuffer> globalUBOBuffer;
std::vector<void*> globalUBOMapped;
VulkanRendererInternals::SceneUBO sceneUBO{};
std::vector<VulkanBuffer> sceneUBOBuffer;
std::vector<void*> sceneUBOMapped;
void create_uniform_buffers();
// Returns false (and leaves the UBOs untouched) if image_index is out of
// range for the currently sized UBO vectors.
bool update_uniform_buffers(uint32_t image_index);
void cleanUpUBOs();
std::vector<vk::CommandBuffer> command_buffers;
void create_command_buffers();
Kataglyphis::VulkanRendererInternals::Raytracing raytracingStage;
Kataglyphis::VulkanRendererInternals::Rasterizer rasterizer;
Kataglyphis::VulkanRendererInternals::DeferredRasterizer deferredRasterizer;
Kataglyphis::VulkanRendererInternals::PathTracing pathTracing;
// Path-tracing temporal accumulation: ONE persistent full-float history
// image (deliberately not per swapchain image - it must survive across
// frames), the number of frames accumulated since the last reset, and the
// key (camera view, light, quality) that history was accumulated against
// - any change to it invalidates the history.
Texture pathTracingAccumulation;
uint32_t pathTracingAccumulatedFrames{ 0 };
Kataglyphis::VulkanRendererInternals::PathTracingHistoryKey pathTracingLastHistory{};
void createPathTracingAccumulationResources();
Kataglyphis::VulkanRendererInternals::PostStage postStage;
// Atmospheric & Lighting Additions
Kataglyphis::SkyBox skyBox;
Kataglyphis::Clouds clouds;
Kataglyphis::CascadedShadowMap dirShadowMap;
// Set at the end of reinitShadowMapForCurrentSettings(): a re-init leaves
// every swapchain image but the next one drawFrame() acquires holding
// stale (or default-constructed) light matrices, because dirShadowMap's
// own seed loop runs before updateUniforms() has recomputed cascadeData
// for the new settings. Consumed (and cleared) by update_uniform_buffers(),
// the first point in the frame where cascadeData is guaranteed fresh.
bool lightMatricesNeedFullReseed{ false };
// One image view per swapchain image, for the skybox framebuffers - the
// depth attachment is a single loop-invariant handle (PostStage owns just
// one depth buffer) and is passed to SkyBox separately, not through this.
std::vector<vk::ImageView> swapchainImageViews();
// -- synchronization
// All frame-sync primitives (current_frame, frame_sync_count, the per-frame
// image_available/in_flight_fences and the per-swapchain-image
// render_finished_by_image/images_in_flight_fences) live in FrameSync; see
// kataglyphis.vulkan.frame_sync for why the sizing split is load-bearing.
FrameSync frameSync;
void createSynchronization();
void cleanUpSync();
// -- per-pass GPU timing (timestamp query pool); see
// kataglyphis.vulkan.gpu_timing for the layout invariants (the per-image
// slice sizing and the "never read an unrecorded slice" guard are
// load-bearing).
GpuTimingSubsystem gpuTiming;
// -- headless frame capture (see requestFrameCapture above); see
// kataglyphis.vulkan.frame_capture for the arm/record/bind/take lifecycle.
FrameCapture frameCapture;
Kataglyphis::VulkanRendererInternals::ASManager asManager;
VulkanBuffer objectDescriptionBuffer;
void create_object_description_buffer();
// Discards and re-uploads the object-description buffer. The scene-changed
// paths (finishModelLoad / addModel / updateStateDueToUserInput) all do this
// identical pair before rebuilding acceleration structures and descriptors;
// the surrounding AS/descriptor steps differ per path and stay inline.
void rebuildObjectDescriptions();
void updateObjectDescriptionDescriptorSets();
// -- descriptor set groups (each owns one layout + pool + per-swapchain-
// image sets; see kataglyphis.vulkan.descriptor_set_group)
DescriptorSetGroup sharedRenderDescriptors;
void createSharedRenderDescriptorResources();
void updateTexturesInSharedRenderDescriptorSet();
void updateUBODescriptorSets();
DescriptorSetGroup postDescriptors;
void create_post_descriptor_resources();
void updatePostDescriptorSets();
DescriptorSetGroup gbufferDescriptors;
void create_gbuffer_descriptor_resources();
void updateGBufferDescriptorSets();
DescriptorSetGroup raytracingDescriptors;
void createRaytracingDescriptorResources();
void update_raytracing_descriptor_set(uint32_t image_index);
void updateRaytracingDescriptorSets();
// The three raytracingDescriptors.write* calls (TLAS, output image,
// accumulation image) shared verbatim by update_raytracing_descriptor_set
// (per-image) and updateRaytracingDescriptorSets (all images).
void writeRaytracingDescriptorsForImage(uint32_t image_index);
void finishModelLoad();
// The single entry point every scene-changing path (finishModelLoad,
// addModel, handleModelTransformChange, handleModelReloadRequest) must go
// through: rebuilds the object-description buffer, rebuilds the
// acceleration structure (BLAS+TLAS via createASForScene when geometry
// changed, TLAS-only via createTLAS for a transform-only change), then
// refreshes every descriptor set that could be bound to stale data. A
// caller that rebuilds the AS without also going through this helper is
// exactly the bug this consolidates away - see BuildIntegrity's
// AccelerationStructureRebuildsGoThroughTheSceneChangeHelper gate.
void refreshAfterSceneChange(bool rebuildBottomLevel);
void updateAllDescriptorSets();
void cleanUpDescriptorResources();
void initDescriptorResources();
bool checkChangedFramebufferSize();
bool device_lost_detected{ false };
bool fatal_frame_error{ false };
};
}// namespace Kataglyphis